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.diff_children(std::slice::from_mut(&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        &mut 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_mut()
190                    .layout(&mut tree.children[0], renderer, limits)
191            },
192        )
193    }
194
195    fn operate(
196        &mut 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_mut().operate(
203            &mut tree.children[0],
204            layout.children().nth(1).unwrap(),
205            renderer,
206            operation,
207        );
208    }
209
210    fn update(
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    ) {
221        self.label.as_widget_mut().update(
222            &mut tree.children[0],
223            event,
224            layout.children().nth(1).unwrap(),
225            cursor,
226            renderer,
227            clipboard,
228            shell,
229            viewport,
230        );
231
232        if !shell.is_event_captured() {
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                        shell.capture_event();
240                        return;
241                    }
242                }
243                _ => {}
244            }
245        }
246    }
247
248    fn mouse_interaction(
249        &self,
250        tree: &Tree,
251        layout: Layout<'_>,
252        cursor: mouse::Cursor,
253        viewport: &Rectangle,
254        renderer: &Renderer,
255    ) -> mouse::Interaction {
256        let interaction = self.label.as_widget().mouse_interaction(
257            &tree.children[0],
258            layout.children().nth(1).unwrap(),
259            cursor,
260            viewport,
261            renderer,
262        );
263
264        if interaction == mouse::Interaction::default() {
265            if cursor.is_over(layout.bounds()) {
266                mouse::Interaction::Pointer
267            } else {
268                mouse::Interaction::default()
269            }
270        } else {
271            interaction
272        }
273    }
274
275    fn draw(
276        &self,
277        tree: &Tree,
278        renderer: &mut Renderer,
279        theme: &Theme,
280        style: &renderer::Style,
281        layout: Layout<'_>,
282        cursor: mouse::Cursor,
283        viewport: &Rectangle,
284    ) {
285        let is_mouse_over = cursor.is_over(layout.bounds());
286
287        let mut children = layout.children();
288
289        let custom_style = if is_mouse_over {
290            theme.style(
291                &(),
292                iced_radio::Status::Hovered {
293                    is_selected: self.is_selected,
294                },
295            )
296        } else {
297            theme.style(
298                &(),
299                iced_radio::Status::Active {
300                    is_selected: self.is_selected,
301                },
302            )
303        };
304
305        {
306            let layout = children.next().unwrap();
307            let bounds = layout.bounds();
308
309            let size = bounds.width;
310            let dot_size = 6.0;
311
312            renderer.fill_quad(
313                renderer::Quad {
314                    bounds,
315                    border: Border {
316                        radius: (size / 2.0).into(),
317                        width: custom_style.border_width,
318                        color: custom_style.border_color,
319                    },
320                    ..renderer::Quad::default()
321                },
322                custom_style.background,
323            );
324
325            if self.is_selected {
326                renderer.fill_quad(
327                    renderer::Quad {
328                        bounds: Rectangle {
329                            x: bounds.x + (size - dot_size) / 2.0,
330                            y: bounds.y + (size - dot_size) / 2.0,
331                            width: dot_size,
332                            height: dot_size,
333                        },
334                        border: border::rounded(dot_size / 2.0),
335                        ..renderer::Quad::default()
336                    },
337                    custom_style.dot_color,
338                );
339            }
340        }
341
342        {
343            let label_layout = children.next().unwrap();
344            self.label.as_widget().draw(
345                &tree.children[0],
346                renderer,
347                theme,
348                style,
349                label_layout,
350                cursor,
351                viewport,
352            );
353        }
354    }
355
356    fn overlay<'b>(
357        &'b mut self,
358        tree: &'b mut Tree,
359        layout: Layout<'b>,
360        renderer: &Renderer,
361        viewport: &Rectangle,
362        translation: Vector,
363    ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
364        self.label.as_widget_mut().overlay(
365            &mut tree.children[0],
366            layout.children().nth(1).unwrap(),
367            renderer,
368            viewport,
369            translation,
370        )
371    }
372
373    fn drag_destinations(
374        &self,
375        state: &Tree,
376        layout: Layout<'_>,
377        renderer: &Renderer,
378        dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles,
379    ) {
380        self.label.as_widget().drag_destinations(
381            &state.children[0],
382            layout.children().nth(1).unwrap(),
383            renderer,
384            dnd_rectangles,
385        );
386    }
387}
388
389impl<'a, Message, Renderer> From<Radio<'a, Message, Renderer>>
390    for Element<'a, Message, Theme, Renderer>
391where
392    Message: 'a + Clone,
393    Renderer: 'a + iced_core::Renderer,
394{
395    fn from(radio: Radio<'a, Message, Renderer>) -> Element<'a, Message, Theme, Renderer> {
396        Element::new(radio)
397    }
398}