cosmic/widget/
radio.rs

1//! Create choices using radio buttons.
2use crate::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: 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    /// The default spacing of a [`Radio`] button.
110    pub const DEFAULT_SPACING: f32 = 8.0;
111
112    /// Creates a new [`Radio`] button.
113    ///
114    /// It expects:
115    ///   * the value related to the [`Radio`] button
116    ///   * the label of the [`Radio`] button
117    ///   * the current selected value
118    ///   * a function that will be called when the [`Radio`] is selected. It
119    ///     receives the value of the radio and must produce a `Message`.
120    pub fn new<T, F, V>(label: T, value: V, selected: Option<V>, f: F) -> Self
121    where
122        V: Eq + Copy,
123        F: FnOnce(V) -> Message,
124        T: Into<Element<'a, Message, Theme, Renderer>>,
125    {
126        Radio {
127            is_selected: Some(value) == selected,
128            on_click: f(value),
129            label: label.into(),
130            width: Length::Shrink,
131            size: Self::DEFAULT_SIZE,
132            spacing: Self::DEFAULT_SPACING,
133        }
134    }
135
136    #[must_use]
137    /// Sets the size of the [`Radio`] button.
138    pub fn size(mut self, size: impl Into<Pixels>) -> Self {
139        self.size = size.into().0;
140        self
141    }
142
143    #[must_use]
144    /// Sets the width of the [`Radio`] button.
145    pub fn width(mut self, width: impl Into<Length>) -> Self {
146        self.width = width.into();
147        self
148    }
149
150    #[must_use]
151    /// Sets the spacing between the [`Radio`] button and the text.
152    pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self {
153        self.spacing = spacing.into().0;
154        self
155    }
156}
157
158impl<Message, Renderer> Widget<Message, Theme, Renderer> for Radio<'_, Message, Renderer>
159where
160    Message: Clone,
161    Renderer: iced_core::Renderer,
162{
163    fn children(&self) -> Vec<Tree> {
164        vec![Tree::new(&self.label)]
165    }
166
167    fn diff(&mut self, tree: &mut Tree) {
168        tree.children[0].diff(&mut self.label);
169    }
170    fn size(&self) -> Size<Length> {
171        Size {
172            width: self.width,
173            height: Length::Shrink,
174        }
175    }
176
177    fn layout(
178        &self,
179        tree: &mut Tree,
180        renderer: &Renderer,
181        limits: &layout::Limits,
182    ) -> layout::Node {
183        layout::next_to_each_other(
184            &limits.width(self.width),
185            self.spacing,
186            |_| layout::Node::new(Size::new(self.size, self.size)),
187            |limits| {
188                self.label
189                    .as_widget()
190                    .layout(&mut tree.children[0], renderer, limits)
191            },
192        )
193    }
194
195    fn operate(
196        &self,
197        tree: &mut Tree,
198        layout: Layout<'_>,
199        renderer: &Renderer,
200        operation: &mut dyn iced_core::widget::Operation<()>,
201    ) {
202        self.label.as_widget().operate(
203            &mut tree.children[0],
204            layout.children().nth(1).unwrap(),
205            renderer,
206            operation,
207        );
208    }
209
210    fn on_event(
211        &mut self,
212        tree: &mut Tree,
213        event: Event,
214        layout: Layout<'_>,
215        cursor: mouse::Cursor,
216        renderer: &Renderer,
217        clipboard: &mut dyn Clipboard,
218        shell: &mut Shell<'_, Message>,
219        viewport: &Rectangle,
220    ) -> event::Status {
221        let status = self.label.as_widget_mut().on_event(
222            &mut tree.children[0],
223            event.clone(),
224            layout.children().nth(1).unwrap(),
225            cursor,
226            renderer,
227            clipboard,
228            shell,
229            viewport,
230        );
231
232        if status == event::Status::Ignored {
233            match event {
234                Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
235                | Event::Touch(touch::Event::FingerPressed { .. }) => {
236                    if cursor.is_over(layout.bounds()) {
237                        shell.publish(self.on_click.clone());
238
239                        return event::Status::Captured;
240                    }
241                }
242                _ => {}
243            }
244
245            event::Status::Ignored
246        } else {
247            status
248        }
249    }
250
251    fn mouse_interaction(
252        &self,
253        tree: &Tree,
254        layout: Layout<'_>,
255        cursor: mouse::Cursor,
256        viewport: &Rectangle,
257        renderer: &Renderer,
258    ) -> mouse::Interaction {
259        let interaction = self.label.as_widget().mouse_interaction(
260            &tree.children[0],
261            layout.children().nth(1).unwrap(),
262            cursor,
263            viewport,
264            renderer,
265        );
266
267        if interaction == mouse::Interaction::default() {
268            if cursor.is_over(layout.bounds()) {
269                mouse::Interaction::Pointer
270            } else {
271                mouse::Interaction::default()
272            }
273        } else {
274            interaction
275        }
276    }
277
278    fn draw(
279        &self,
280        tree: &Tree,
281        renderer: &mut Renderer,
282        theme: &Theme,
283        style: &renderer::Style,
284        layout: Layout<'_>,
285        cursor: mouse::Cursor,
286        viewport: &Rectangle,
287    ) {
288        let is_mouse_over = cursor.is_over(layout.bounds());
289
290        let mut children = layout.children();
291
292        let custom_style = if is_mouse_over {
293            theme.style(
294                &(),
295                iced_radio::Status::Hovered {
296                    is_selected: self.is_selected,
297                },
298            )
299        } else {
300            theme.style(
301                &(),
302                iced_radio::Status::Active {
303                    is_selected: self.is_selected,
304                },
305            )
306        };
307
308        {
309            let layout = children.next().unwrap();
310            let bounds = layout.bounds();
311
312            let size = bounds.width;
313            let dot_size = 6.0;
314
315            renderer.fill_quad(
316                renderer::Quad {
317                    bounds,
318                    border: Border {
319                        radius: (size / 2.0).into(),
320                        width: custom_style.border_width,
321                        color: custom_style.border_color,
322                    },
323                    ..renderer::Quad::default()
324                },
325                custom_style.background,
326            );
327
328            if self.is_selected {
329                renderer.fill_quad(
330                    renderer::Quad {
331                        bounds: Rectangle {
332                            x: bounds.x + (size - dot_size) / 2.0,
333                            y: bounds.y + (size - dot_size) / 2.0,
334                            width: dot_size,
335                            height: dot_size,
336                        },
337                        border: border::rounded(dot_size / 2.0),
338                        ..renderer::Quad::default()
339                    },
340                    custom_style.dot_color,
341                );
342            }
343        }
344
345        {
346            let label_layout = children.next().unwrap();
347            self.label.as_widget().draw(
348                &tree.children[0],
349                renderer,
350                theme,
351                style,
352                label_layout,
353                cursor,
354                viewport,
355            );
356        }
357    }
358
359    fn overlay<'b>(
360        &'b mut self,
361        tree: &'b mut Tree,
362        layout: Layout<'_>,
363        renderer: &Renderer,
364        translation: Vector,
365    ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
366        self.label.as_widget_mut().overlay(
367            &mut tree.children[0],
368            layout.children().nth(1).unwrap(),
369            renderer,
370            translation,
371        )
372    }
373
374    fn drag_destinations(
375        &self,
376        state: &Tree,
377        layout: Layout<'_>,
378        renderer: &Renderer,
379        dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles,
380    ) {
381        self.label.as_widget().drag_destinations(
382            &state.children[0],
383            layout.children().nth(1).unwrap(),
384            renderer,
385            dnd_rectangles,
386        );
387    }
388}
389
390impl<'a, Message, Renderer> From<Radio<'a, Message, Renderer>>
391    for Element<'a, Message, Theme, Renderer>
392where
393    Message: 'a + Clone,
394    Renderer: 'a + iced_core::Renderer,
395{
396    fn from(radio: Radio<'a, Message, Renderer>) -> Element<'a, Message, Theme, Renderer> {
397        Element::new(radio)
398    }
399}