cosmic/widget/
radio.rs

1//! Create choices using radio buttons.
2use crate::{Theme, theme};
3use iced::border;
4use iced_core::event::{self, Event};
5use iced_core::layout;
6use iced_core::mouse;
7use iced_core::overlay;
8use iced_core::renderer;
9use iced_core::touch;
10use iced_core::widget::tree::Tree;
11use iced_core::{
12    Border, Clipboard, Element, Layout, Length, Pixels, Rectangle, Shell, Size, Vector, Widget,
13};
14
15use iced_widget::radio as iced_radio;
16pub use iced_widget::radio::Catalog;
17
18pub fn radio<'a, Message: Clone, V, F>(
19    label: impl Into<Element<'a, Message, Theme, crate::Renderer>>,
20    value: V,
21    selected: Option<V>,
22    f: F,
23) -> Radio<'a, Message, crate::Renderer>
24where
25    V: Eq + Copy,
26    F: FnOnce(V) -> Message,
27{
28    Radio::new(label, value, selected, f)
29}
30
31/// A circular button representing a choice.
32///
33/// # Example
34/// ```no_run
35/// # type Radio<'a, Message> =
36/// #     cosmic::widget::Radio<'a, Message, cosmic::Renderer>;
37/// #
38/// # use cosmic::widget::text;
39/// # use cosmic::iced::widget::column;
40/// #[derive(Debug, Clone, Copy, PartialEq, Eq)]
41/// pub enum Choice {
42///     A,
43///     B,
44///     C,
45///     All,
46/// }
47///
48/// #[derive(Debug, Clone, Copy)]
49/// pub enum Message {
50///     RadioSelected(Choice),
51/// }
52///
53/// let selected_choice = Some(Choice::A);
54///
55/// let a = Radio::new(
56///     text::heading("A"),
57///     Choice::A,
58///     selected_choice,
59///     Message::RadioSelected,
60/// );
61///
62/// let b = Radio::new(
63///     text::heading("B"),
64///     Choice::B,
65///     selected_choice,
66///     Message::RadioSelected,
67/// );
68///
69/// let c = Radio::new(
70///     text::heading("C"),
71///     Choice::C,
72///     selected_choice,
73///     Message::RadioSelected,
74/// );
75///
76/// let all = Radio::new(
77///     column![
78///         text::heading("All"),
79///         text::body("A, B and C"),
80///     ],
81///     Choice::All,
82///     selected_choice,
83///     Message::RadioSelected
84/// );
85///
86/// let content = column![a, b, c, all];
87/// ```
88#[allow(missing_debug_implementations)]
89pub struct Radio<'a, Message, Renderer = crate::Renderer>
90where
91    Renderer: iced_core::Renderer,
92{
93    is_selected: bool,
94    on_click: Message,
95    label: Option<Element<'a, Message, Theme, Renderer>>,
96    width: Length,
97    size: f32,
98    spacing: f32,
99}
100
101impl<'a, Message, Renderer> Radio<'a, Message, Renderer>
102where
103    Message: Clone,
104    Renderer: iced_core::Renderer,
105{
106    /// The default size of a [`Radio`] button.
107    pub const DEFAULT_SIZE: f32 = 16.0;
108
109    /// Creates a new [`Radio`] button.
110    ///
111    /// It expects:
112    ///   * the value related to the [`Radio`] button
113    ///   * the label of the [`Radio`] button
114    ///   * the current selected value
115    ///   * a function that will be called when the [`Radio`] is selected. It
116    ///     receives the value of the radio and must produce a `Message`.
117    pub fn new<T, F, V>(label: T, value: V, selected: Option<V>, f: F) -> Self
118    where
119        V: Eq + Copy,
120        F: FnOnce(V) -> Message,
121        T: Into<Element<'a, Message, Theme, Renderer>>,
122    {
123        Radio {
124            is_selected: Some(value) == selected,
125            on_click: f(value),
126            label: Some(label.into()),
127            width: Length::Shrink,
128            size: Self::DEFAULT_SIZE,
129            spacing: theme::spacing().space_xs as f32,
130        }
131    }
132
133    /// Creates a new [`Radio`] button without a label.
134    ///
135    /// This is intended for internal use with the settings item builder,
136    /// where the label comes from the settings item title instead.
137    pub(crate) fn new_no_label<V, F>(value: V, selected: Option<V>, f: F) -> Self
138    where
139        V: Eq + Copy,
140        F: FnOnce(V) -> Message,
141    {
142        Radio {
143            is_selected: Some(value) == selected,
144            on_click: f(value),
145            label: None,
146            width: Length::Shrink,
147            size: Self::DEFAULT_SIZE,
148            spacing: theme::spacing().space_xs as f32,
149        }
150    }
151
152    #[must_use]
153    /// Sets the size of the [`Radio`] button.
154    pub fn size(mut self, size: impl Into<Pixels>) -> Self {
155        self.size = size.into().0;
156        self
157    }
158
159    #[must_use]
160    /// Sets the width of the [`Radio`] button.
161    pub fn width(mut self, width: impl Into<Length>) -> Self {
162        self.width = width.into();
163        self
164    }
165
166    #[must_use]
167    /// Sets the spacing between the [`Radio`] button and the text.
168    pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self {
169        self.spacing = spacing.into().0;
170        self
171    }
172}
173
174impl<Message, Renderer> Widget<Message, Theme, Renderer> for Radio<'_, Message, Renderer>
175where
176    Message: Clone,
177    Renderer: iced_core::Renderer,
178{
179    fn children(&self) -> Vec<Tree> {
180        if let Some(label) = &self.label {
181            vec![Tree::new(label)]
182        } else {
183            vec![]
184        }
185    }
186
187    fn diff(&mut self, tree: &mut Tree) {
188        if let Some(label) = &mut self.label {
189            tree.diff_children(std::slice::from_mut(label));
190        }
191    }
192    fn size(&self) -> Size<Length> {
193        Size {
194            width: self.width,
195            height: Length::Shrink,
196        }
197    }
198
199    fn layout(
200        &mut self,
201        tree: &mut Tree,
202        renderer: &Renderer,
203        limits: &layout::Limits,
204    ) -> layout::Node {
205        if let Some(label) = &mut self.label {
206            layout::next_to_each_other(
207                &limits.width(self.width),
208                self.spacing,
209                |_| layout::Node::new(Size::new(self.size, self.size)),
210                |limits| {
211                    label
212                        .as_widget_mut()
213                        .layout(&mut tree.children[0], renderer, limits)
214                },
215            )
216        } else {
217            layout::Node::new(Size::new(self.size, self.size))
218        }
219    }
220
221    fn operate(
222        &mut self,
223        tree: &mut Tree,
224        layout: Layout<'_>,
225        renderer: &Renderer,
226        operation: &mut dyn iced_core::widget::Operation<()>,
227    ) {
228        if let Some(label) = &mut self.label {
229            label.as_widget_mut().operate(
230                &mut tree.children[0],
231                layout.children().nth(1).unwrap(),
232                renderer,
233                operation,
234            );
235        }
236    }
237
238    fn update(
239        &mut self,
240        tree: &mut Tree,
241        event: &Event,
242        layout: Layout<'_>,
243        cursor: mouse::Cursor,
244        renderer: &Renderer,
245        clipboard: &mut dyn Clipboard,
246        shell: &mut Shell<'_, Message>,
247        viewport: &Rectangle,
248    ) {
249        if let Some(label) = &mut self.label {
250            label.as_widget_mut().update(
251                &mut tree.children[0],
252                event,
253                layout.children().nth(1).unwrap(),
254                cursor,
255                renderer,
256                clipboard,
257                shell,
258                viewport,
259            );
260        }
261
262        if !shell.is_event_captured() {
263            match event {
264                Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
265                | Event::Touch(touch::Event::FingerLifted { .. }) => {
266                    if cursor.is_over(layout.bounds()) {
267                        shell.publish(self.on_click.clone());
268                        shell.capture_event();
269                        return;
270                    }
271                }
272                _ => {}
273            }
274        }
275    }
276
277    fn mouse_interaction(
278        &self,
279        tree: &Tree,
280        layout: Layout<'_>,
281        cursor: mouse::Cursor,
282        viewport: &Rectangle,
283        renderer: &Renderer,
284    ) -> mouse::Interaction {
285        let interaction = if let Some(label) = &self.label {
286            label.as_widget().mouse_interaction(
287                &tree.children[0],
288                layout.children().nth(1).unwrap(),
289                cursor,
290                viewport,
291                renderer,
292            )
293        } else {
294            mouse::Interaction::default()
295        };
296
297        if interaction == mouse::Interaction::default() {
298            if cursor.is_over(layout.bounds()) {
299                mouse::Interaction::Pointer
300            } else {
301                mouse::Interaction::default()
302            }
303        } else {
304            interaction
305        }
306    }
307
308    fn draw(
309        &self,
310        tree: &Tree,
311        renderer: &mut Renderer,
312        theme: &Theme,
313        style: &renderer::Style,
314        layout: Layout<'_>,
315        cursor: mouse::Cursor,
316        viewport: &Rectangle,
317    ) {
318        let is_mouse_over = cursor.is_over(layout.bounds());
319
320        let custom_style = if is_mouse_over {
321            theme.style(
322                &(),
323                iced_radio::Status::Hovered {
324                    is_selected: self.is_selected,
325                },
326            )
327        } else {
328            theme.style(
329                &(),
330                iced_radio::Status::Active {
331                    is_selected: self.is_selected,
332                },
333            )
334        };
335
336        let (dot_bounds, label_layout) = if self.label.is_some() {
337            let mut children = layout.children();
338            let dot_bounds = children.next().unwrap().bounds();
339            (dot_bounds, children.next())
340        } else {
341            (layout.bounds(), None)
342        };
343
344        {
345            let size = dot_bounds.width;
346            let dot_size = 6.0;
347
348            renderer.fill_quad(
349                renderer::Quad {
350                    bounds: dot_bounds,
351                    border: Border {
352                        radius: (size / 2.0).into(),
353                        width: custom_style.border_width,
354                        color: custom_style.border_color,
355                    },
356                    ..renderer::Quad::default()
357                },
358                custom_style.background,
359            );
360
361            if self.is_selected {
362                renderer.fill_quad(
363                    renderer::Quad {
364                        bounds: Rectangle {
365                            x: dot_bounds.x + (size - dot_size) / 2.0,
366                            y: dot_bounds.y + (size - dot_size) / 2.0,
367                            width: dot_size,
368                            height: dot_size,
369                        },
370                        border: border::rounded(dot_size / 2.0),
371                        ..renderer::Quad::default()
372                    },
373                    custom_style.dot_color,
374                );
375            }
376        }
377
378        if let (Some(label), Some(label_layout)) = (&self.label, label_layout) {
379            label.as_widget().draw(
380                &tree.children[0],
381                renderer,
382                theme,
383                style,
384                label_layout,
385                cursor,
386                viewport,
387            );
388        }
389    }
390
391    fn overlay<'b>(
392        &'b mut self,
393        tree: &'b mut Tree,
394        layout: Layout<'b>,
395        renderer: &Renderer,
396        viewport: &Rectangle,
397        translation: Vector,
398    ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
399        self.label.as_mut()?.as_widget_mut().overlay(
400            &mut tree.children[0],
401            layout.children().nth(1).unwrap(),
402            renderer,
403            viewport,
404            translation,
405        )
406    }
407
408    fn drag_destinations(
409        &self,
410        state: &Tree,
411        layout: Layout<'_>,
412        renderer: &Renderer,
413        dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles,
414    ) {
415        if let Some(label) = &self.label {
416            label.as_widget().drag_destinations(
417                &state.children[0],
418                layout.children().nth(1).unwrap(),
419                renderer,
420                dnd_rectangles,
421            );
422        }
423    }
424}
425
426impl<'a, Message, Renderer> From<Radio<'a, Message, Renderer>>
427    for Element<'a, Message, Theme, Renderer>
428where
429    Message: 'a + Clone,
430    Renderer: 'a + iced_core::Renderer,
431{
432    fn from(radio: Radio<'a, Message, Renderer>) -> Element<'a, Message, Theme, Renderer> {
433        Element::new(radio)
434    }
435}