iced_native/widget/
radio.rs

1//! Create choices using radio buttons.
2use crate::alignment;
3use crate::event::{self, Event};
4use crate::layout;
5use crate::mouse;
6use crate::renderer;
7use crate::text;
8use crate::touch;
9use crate::widget::{self, Row, Text, Tree};
10use crate::{
11    Alignment, Clipboard, Color, Element, Layout, Length, Pixels, Point,
12    Rectangle, Shell, Widget,
13};
14
15pub use iced_style::radio::{Appearance, StyleSheet};
16
17/// A circular button representing a choice.
18///
19/// # Example
20/// ```
21/// # type Radio<Message> =
22/// #     iced_native::widget::Radio<Message, iced_native::renderer::Null>;
23/// #
24/// # use iced_native::column;
25/// #[derive(Debug, Clone, Copy, PartialEq, Eq)]
26/// pub enum Choice {
27///     A,
28///     B,
29///     C,
30///     All,
31/// }
32///
33/// #[derive(Debug, Clone, Copy)]
34/// pub enum Message {
35///     RadioSelected(Choice),
36/// }
37///
38/// let selected_choice = Some(Choice::A);
39///
40/// let a = Radio::new(
41///     "A",
42///     Choice::A,
43///     selected_choice,
44///     Message::RadioSelected,
45/// );
46///
47/// let b = Radio::new(
48///     "B",
49///     Choice::B,
50///     selected_choice,
51///     Message::RadioSelected,
52/// );
53///
54/// let c = Radio::new(
55///     "C",
56///     Choice::C,
57///     selected_choice,
58///     Message::RadioSelected,
59/// );
60///
61/// let all = Radio::new(
62///     "All of the above",
63///     Choice::All,
64///     selected_choice,
65///     Message::RadioSelected
66/// );
67///
68/// let content = column![a, b, c, all];
69/// ```
70#[allow(missing_debug_implementations)]
71pub struct Radio<Message, Renderer>
72where
73    Renderer: text::Renderer,
74    Renderer::Theme: StyleSheet,
75{
76    is_selected: bool,
77    on_click: Message,
78    label: String,
79    width: Length,
80    size: f32,
81    spacing: f32,
82    text_size: Option<f32>,
83    font: Renderer::Font,
84    style: <Renderer::Theme as StyleSheet>::Style,
85}
86
87impl<Message, Renderer> Radio<Message, Renderer>
88where
89    Message: Clone,
90    Renderer: text::Renderer,
91    Renderer::Theme: StyleSheet,
92{
93    /// The default size of a [`Radio`] button.
94    pub const DEFAULT_SIZE: f32 = 28.0;
95
96    /// The default spacing of a [`Radio`] button.
97    pub const DEFAULT_SPACING: f32 = 15.0;
98
99    /// Creates a new [`Radio`] button.
100    ///
101    /// It expects:
102    ///   * the value related to the [`Radio`] button
103    ///   * the label of the [`Radio`] button
104    ///   * the current selected value
105    ///   * a function that will be called when the [`Radio`] is selected. It
106    ///   receives the value of the radio and must produce a `Message`.
107    pub fn new<F, V>(
108        label: impl Into<String>,
109        value: V,
110        selected: Option<V>,
111        f: F,
112    ) -> Self
113    where
114        V: Eq + Copy,
115        F: FnOnce(V) -> Message,
116    {
117        Radio {
118            is_selected: Some(value) == selected,
119            on_click: f(value),
120            label: label.into(),
121            width: Length::Shrink,
122            size: Self::DEFAULT_SIZE,
123            spacing: Self::DEFAULT_SPACING, //15
124            text_size: None,
125            font: Default::default(),
126            style: Default::default(),
127        }
128    }
129
130    /// Sets the size of the [`Radio`] button.
131    pub fn size(mut self, size: impl Into<Pixels>) -> Self {
132        self.size = size.into().0;
133        self
134    }
135
136    /// Sets the width of the [`Radio`] button.
137    pub fn width(mut self, width: impl Into<Length>) -> Self {
138        self.width = width.into();
139        self
140    }
141
142    /// Sets the spacing between the [`Radio`] button and the text.
143    pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self {
144        self.spacing = spacing.into().0;
145        self
146    }
147
148    /// Sets the text size of the [`Radio`] button.
149    pub fn text_size(mut self, text_size: impl Into<Pixels>) -> Self {
150        self.text_size = Some(text_size.into().0);
151        self
152    }
153
154    /// Sets the text font of the [`Radio`] button.
155    pub fn font(mut self, font: Renderer::Font) -> Self {
156        self.font = font;
157        self
158    }
159
160    /// Sets the style of the [`Radio`] button.
161    pub fn style(
162        mut self,
163        style: impl Into<<Renderer::Theme as StyleSheet>::Style>,
164    ) -> Self {
165        self.style = style.into();
166        self
167    }
168}
169
170impl<Message, Renderer> Widget<Message, Renderer> for Radio<Message, Renderer>
171where
172    Message: Clone,
173    Renderer: text::Renderer,
174    Renderer::Theme: StyleSheet + widget::text::StyleSheet,
175{
176    fn width(&self) -> Length {
177        self.width
178    }
179
180    fn height(&self) -> Length {
181        Length::Shrink
182    }
183
184    fn layout(
185        &self,
186        renderer: &Renderer,
187        limits: &layout::Limits,
188    ) -> layout::Node {
189        Row::<(), Renderer>::new()
190            .width(self.width)
191            .spacing(self.spacing)
192            .align_items(Alignment::Center)
193            .push(Row::new().width(self.size).height(self.size))
194            .push(Text::new(&self.label).width(self.width).size(
195                self.text_size.unwrap_or_else(|| renderer.default_size()),
196            ))
197            .layout(renderer, limits)
198    }
199
200    fn on_event(
201        &mut self,
202        _state: &mut Tree,
203        event: Event,
204        layout: Layout<'_>,
205        cursor_position: Point,
206        _renderer: &Renderer,
207        _clipboard: &mut dyn Clipboard,
208        shell: &mut Shell<'_, Message>,
209    ) -> event::Status {
210        match event {
211            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
212            | Event::Touch(touch::Event::FingerPressed { .. }) => {
213                if layout.bounds().contains(cursor_position) {
214                    shell.publish(self.on_click.clone());
215
216                    return event::Status::Captured;
217                }
218            }
219            _ => {}
220        }
221
222        event::Status::Ignored
223    }
224
225    fn mouse_interaction(
226        &self,
227        _state: &Tree,
228        layout: Layout<'_>,
229        cursor_position: Point,
230        _viewport: &Rectangle,
231        _renderer: &Renderer,
232    ) -> mouse::Interaction {
233        if layout.bounds().contains(cursor_position) {
234            mouse::Interaction::Pointer
235        } else {
236            mouse::Interaction::default()
237        }
238    }
239
240    fn draw(
241        &self,
242        _state: &Tree,
243        renderer: &mut Renderer,
244        theme: &Renderer::Theme,
245        style: &renderer::Style,
246        layout: Layout<'_>,
247        cursor_position: Point,
248        _viewport: &Rectangle,
249    ) {
250        let bounds = layout.bounds();
251        let is_mouse_over = bounds.contains(cursor_position);
252
253        let mut children = layout.children();
254
255        let custom_style = if is_mouse_over {
256            theme.hovered(&self.style, self.is_selected)
257        } else {
258            theme.active(&self.style, self.is_selected)
259        };
260
261        {
262            let layout = children.next().unwrap();
263            let bounds = layout.bounds();
264
265            let size = bounds.width;
266            let dot_size = size / 2.0;
267
268            renderer.fill_quad(
269                renderer::Quad {
270                    bounds,
271                    border_radius: (size / 2.0).into(),
272                    border_width: custom_style.border_width,
273                    border_color: custom_style.border_color,
274                },
275                custom_style.background,
276            );
277
278            if self.is_selected {
279                renderer.fill_quad(
280                    renderer::Quad {
281                        bounds: Rectangle {
282                            x: bounds.x + dot_size / 2.0,
283                            y: bounds.y + dot_size / 2.0,
284                            width: bounds.width - dot_size,
285                            height: bounds.height - dot_size,
286                        },
287                        border_radius: (dot_size / 2.0).into(),
288                        border_width: 0.0,
289                        border_color: Color::TRANSPARENT,
290                    },
291                    custom_style.dot_color,
292                );
293            }
294        }
295
296        {
297            let label_layout = children.next().unwrap();
298
299            widget::text::draw(
300                renderer,
301                style,
302                label_layout,
303                &self.label,
304                self.text_size,
305                self.font.clone(),
306                widget::text::Appearance {
307                    color: custom_style.text_color,
308                },
309                alignment::Horizontal::Left,
310                alignment::Vertical::Center,
311            );
312        }
313    }
314}
315
316impl<'a, Message, Renderer> From<Radio<Message, Renderer>>
317    for Element<'a, Message, Renderer>
318where
319    Message: 'a + Clone,
320    Renderer: 'a + text::Renderer,
321    Renderer::Theme: StyleSheet + widget::text::StyleSheet,
322{
323    fn from(radio: Radio<Message, Renderer>) -> Element<'a, Message, Renderer> {
324        Element::new(radio)
325    }
326}