1use crate::{Theme, theme};
3use iced::border;
4use iced_core::event::{self, Event};
5use iced_core::widget::tree::Tree;
6use iced_core::{
7 Border, Clipboard, Element, Layout, Length, Pixels, Rectangle, Shell, Size, Vector, Widget,
8 layout, mouse, overlay, renderer, touch,
9};
10
11use iced_widget::radio as iced_radio;
12pub use iced_widget::radio::Catalog;
13
14pub fn radio<'a, Message: Clone, V, F>(
15 label: impl Into<Element<'a, Message, Theme, crate::Renderer>>,
16 value: V,
17 selected: Option<V>,
18 f: F,
19) -> Radio<'a, Message, crate::Renderer>
20where
21 V: Eq + Copy,
22 F: FnOnce(V) -> Message,
23{
24 Radio::new(label, value, selected, f)
25}
26
27#[allow(missing_debug_implementations)]
85pub struct Radio<'a, Message, Renderer = crate::Renderer>
86where
87 Renderer: iced_core::Renderer,
88{
89 is_selected: bool,
90 on_click: Message,
91 label: Option<Element<'a, Message, Theme, Renderer>>,
92 width: Length,
93 size: f32,
94 spacing: f32,
95}
96
97impl<'a, Message, Renderer> Radio<'a, Message, Renderer>
98where
99 Message: Clone,
100 Renderer: iced_core::Renderer,
101{
102 pub const DEFAULT_SIZE: f32 = 16.0;
104
105 pub fn new<T, F, V>(label: T, value: V, selected: Option<V>, f: F) -> Self
114 where
115 V: Eq + Copy,
116 F: FnOnce(V) -> Message,
117 T: Into<Element<'a, Message, Theme, Renderer>>,
118 {
119 Radio {
120 is_selected: Some(value) == selected,
121 on_click: f(value),
122 label: Some(label.into()),
123 width: Length::Shrink,
124 size: Self::DEFAULT_SIZE,
125 spacing: theme::spacing().space_xs as f32,
126 }
127 }
128
129 pub(crate) fn new_no_label<V, F>(value: V, selected: Option<V>, f: F) -> Self
134 where
135 V: Eq + Copy,
136 F: FnOnce(V) -> Message,
137 {
138 Radio {
139 is_selected: Some(value) == selected,
140 on_click: f(value),
141 label: None,
142 width: Length::Shrink,
143 size: Self::DEFAULT_SIZE,
144 spacing: theme::spacing().space_xs as f32,
145 }
146 }
147
148 #[must_use]
149 pub fn size(mut self, size: impl Into<Pixels>) -> Self {
151 self.size = size.into().0;
152 self
153 }
154
155 #[must_use]
156 pub fn width(mut self, width: impl Into<Length>) -> Self {
158 self.width = width.into();
159 self
160 }
161
162 #[must_use]
163 pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self {
165 self.spacing = spacing.into().0;
166 self
167 }
168}
169
170impl<Message, Renderer> Widget<Message, Theme, Renderer> for Radio<'_, Message, Renderer>
171where
172 Message: Clone,
173 Renderer: iced_core::Renderer,
174{
175 fn children(&self) -> Vec<Tree> {
176 if let Some(label) = &self.label {
177 vec![Tree::new(label)]
178 } else {
179 vec![]
180 }
181 }
182
183 fn diff(&mut self, tree: &mut Tree) {
184 if let Some(label) = &mut self.label {
185 tree.diff_children(std::slice::from_mut(label));
186 }
187 }
188 fn size(&self) -> Size<Length> {
189 Size {
190 width: self.width,
191 height: Length::Shrink,
192 }
193 }
194
195 fn layout(
196 &mut self,
197 tree: &mut Tree,
198 renderer: &Renderer,
199 limits: &layout::Limits,
200 ) -> layout::Node {
201 if let Some(label) = &mut self.label {
202 layout::next_to_each_other(
203 &limits.width(self.width),
204 self.spacing,
205 |_| layout::Node::new(Size::new(self.size, self.size)),
206 |limits| {
207 label
208 .as_widget_mut()
209 .layout(&mut tree.children[0], renderer, limits)
210 },
211 )
212 } else {
213 layout::Node::new(Size::new(self.size, self.size))
214 }
215 }
216
217 fn operate(
218 &mut self,
219 tree: &mut Tree,
220 layout: Layout<'_>,
221 renderer: &Renderer,
222 operation: &mut dyn iced_core::widget::Operation<()>,
223 ) {
224 if let Some(label) = &mut self.label {
225 label.as_widget_mut().operate(
226 &mut tree.children[0],
227 layout.children().nth(1).unwrap(),
228 renderer,
229 operation,
230 );
231 }
232 }
233
234 fn update(
235 &mut self,
236 tree: &mut Tree,
237 event: &Event,
238 layout: Layout<'_>,
239 cursor: mouse::Cursor,
240 renderer: &Renderer,
241 clipboard: &mut dyn Clipboard,
242 shell: &mut Shell<'_, Message>,
243 viewport: &Rectangle,
244 ) {
245 if let Some(label) = &mut self.label {
246 label.as_widget_mut().update(
247 &mut tree.children[0],
248 event,
249 layout.children().nth(1).unwrap(),
250 cursor,
251 renderer,
252 clipboard,
253 shell,
254 viewport,
255 );
256 }
257
258 if !shell.is_event_captured() {
259 match event {
260 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
261 | Event::Touch(touch::Event::FingerLifted { .. }) => {
262 if cursor.is_over(layout.bounds()) {
263 shell.publish(self.on_click.clone());
264 shell.capture_event();
265 return;
266 }
267 }
268 _ => {}
269 }
270 }
271 }
272
273 fn mouse_interaction(
274 &self,
275 tree: &Tree,
276 layout: Layout<'_>,
277 cursor: mouse::Cursor,
278 viewport: &Rectangle,
279 renderer: &Renderer,
280 ) -> mouse::Interaction {
281 let interaction = if let Some(label) = &self.label {
282 label.as_widget().mouse_interaction(
283 &tree.children[0],
284 layout.children().nth(1).unwrap(),
285 cursor,
286 viewport,
287 renderer,
288 )
289 } else {
290 mouse::Interaction::default()
291 };
292
293 if interaction == mouse::Interaction::default() {
294 if cursor.is_over(layout.bounds()) {
295 mouse::Interaction::Pointer
296 } else {
297 mouse::Interaction::default()
298 }
299 } else {
300 interaction
301 }
302 }
303
304 fn draw(
305 &self,
306 tree: &Tree,
307 renderer: &mut Renderer,
308 theme: &Theme,
309 style: &renderer::Style,
310 layout: Layout<'_>,
311 cursor: mouse::Cursor,
312 viewport: &Rectangle,
313 ) {
314 let is_mouse_over = cursor.is_over(layout.bounds());
315
316 let custom_style = if is_mouse_over {
317 theme.style(
318 &(),
319 iced_radio::Status::Hovered {
320 is_selected: self.is_selected,
321 },
322 )
323 } else {
324 theme.style(
325 &(),
326 iced_radio::Status::Active {
327 is_selected: self.is_selected,
328 },
329 )
330 };
331
332 let (dot_bounds, label_layout) = if self.label.is_some() {
333 let mut children = layout.children();
334 let dot_bounds = children.next().unwrap().bounds();
335 (dot_bounds, children.next())
336 } else {
337 (layout.bounds(), None)
338 };
339
340 {
341 let size = dot_bounds.width;
342 let dot_size = 6.0;
343
344 renderer.fill_quad(
345 renderer::Quad {
346 bounds: dot_bounds,
347 border: Border {
348 radius: (size / 2.0).into(),
349 width: custom_style.border_width,
350 color: custom_style.border_color,
351 },
352 ..renderer::Quad::default()
353 },
354 custom_style.background,
355 );
356
357 if self.is_selected {
358 renderer.fill_quad(
359 renderer::Quad {
360 bounds: Rectangle {
361 x: dot_bounds.x + (size - dot_size) / 2.0,
362 y: dot_bounds.y + (size - dot_size) / 2.0,
363 width: dot_size,
364 height: dot_size,
365 },
366 border: border::rounded(dot_size / 2.0),
367 ..renderer::Quad::default()
368 },
369 custom_style.dot_color,
370 );
371 }
372 }
373
374 if let (Some(label), Some(label_layout)) = (&self.label, label_layout) {
375 label.as_widget().draw(
376 &tree.children[0],
377 renderer,
378 theme,
379 style,
380 label_layout,
381 cursor,
382 viewport,
383 );
384 }
385 }
386
387 fn overlay<'b>(
388 &'b mut self,
389 tree: &'b mut Tree,
390 layout: Layout<'b>,
391 renderer: &Renderer,
392 viewport: &Rectangle,
393 translation: Vector,
394 ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
395 self.label.as_mut()?.as_widget_mut().overlay(
396 &mut tree.children[0],
397 layout.children().nth(1).unwrap(),
398 renderer,
399 viewport,
400 translation,
401 )
402 }
403
404 fn drag_destinations(
405 &self,
406 state: &Tree,
407 layout: Layout<'_>,
408 renderer: &Renderer,
409 dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles,
410 ) {
411 if let Some(label) = &self.label {
412 label.as_widget().drag_destinations(
413 &state.children[0],
414 layout.children().nth(1).unwrap(),
415 renderer,
416 dnd_rectangles,
417 );
418 }
419 }
420}
421
422impl<'a, Message, Renderer> From<Radio<'a, Message, Renderer>>
423 for Element<'a, Message, Theme, Renderer>
424where
425 Message: 'a + Clone,
426 Renderer: 'a + iced_core::Renderer,
427{
428 fn from(radio: Radio<'a, Message, Renderer>) -> Element<'a, Message, Theme, Renderer> {
429 Element::new(radio)
430 }
431}