Skip to main content

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::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/// A circular button representing a choice.
28///
29/// # Example
30/// ```no_run
31/// # type Radio<'a, Message> =
32/// #     cosmic::widget::Radio<'a, Message, cosmic::Renderer>;
33/// #
34/// # use cosmic::widget::text;
35/// # use cosmic::iced::widget::column;
36/// #[derive(Debug, Clone, Copy, PartialEq, Eq)]
37/// pub enum Choice {
38///     A,
39///     B,
40///     C,
41///     All,
42/// }
43///
44/// #[derive(Debug, Clone, Copy)]
45/// pub enum Message {
46///     RadioSelected(Choice),
47/// }
48///
49/// let selected_choice = Some(Choice::A);
50///
51/// let a = Radio::new(
52///     text::heading("A"),
53///     Choice::A,
54///     selected_choice,
55///     Message::RadioSelected,
56/// );
57///
58/// let b = Radio::new(
59///     text::heading("B"),
60///     Choice::B,
61///     selected_choice,
62///     Message::RadioSelected,
63/// );
64///
65/// let c = Radio::new(
66///     text::heading("C"),
67///     Choice::C,
68///     selected_choice,
69///     Message::RadioSelected,
70/// );
71///
72/// let all = Radio::new(
73///     column![
74///         text::heading("All"),
75///         text::body("A, B and C"),
76///     ],
77///     Choice::All,
78///     selected_choice,
79///     Message::RadioSelected
80/// );
81///
82/// let content = column![a, b, c, all];
83/// ```
84#[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    /// The default size of a [`Radio`] button.
103    pub const DEFAULT_SIZE: f32 = 16.0;
104
105    /// Creates a new [`Radio`] button.
106    ///
107    /// It expects:
108    ///   * the value related to the [`Radio`] button
109    ///   * the label of the [`Radio`] button
110    ///   * the current selected value
111    ///   * a function that will be called when the [`Radio`] is selected. It
112    ///     receives the value of the radio and must produce a `Message`.
113    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    /// Creates a new [`Radio`] button without a label.
130    ///
131    /// This is intended for internal use with the settings item builder,
132    /// where the label comes from the settings item title instead.
133    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    /// Sets the size of the [`Radio`] button.
150    pub fn size(mut self, size: impl Into<Pixels>) -> Self {
151        self.size = size.into().0;
152        self
153    }
154
155    #[must_use]
156    /// Sets the width of the [`Radio`] button.
157    pub fn width(mut self, width: impl Into<Length>) -> Self {
158        self.width = width.into();
159        self
160    }
161
162    #[must_use]
163    /// Sets the spacing between the [`Radio`] button and the text.
164    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}