iced_widget/
radio.rs

1//! Radio buttons let users choose a single option from a bunch of options.
2//!
3//! # Example
4//! ```no_run
5//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
6//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
7//! #
8//! use iced::widget::{column, radio};
9//!
10//! struct State {
11//!    selection: Option<Choice>,
12//! }
13//!
14//! #[derive(Debug, Clone, Copy)]
15//! enum Message {
16//!     RadioSelected(Choice),
17//! }
18//!
19//! #[derive(Debug, Clone, Copy, PartialEq, Eq)]
20//! enum Choice {
21//!     A,
22//!     B,
23//!     C,
24//!     All,
25//! }
26//!
27//! fn view(state: &State) -> Element<'_, Message> {
28//!     let a = radio(
29//!         "A",
30//!         Choice::A,
31//!         state.selection,
32//!         Message::RadioSelected,
33//!     );
34//!
35//!     let b = radio(
36//!         "B",
37//!         Choice::B,
38//!         state.selection,
39//!         Message::RadioSelected,
40//!     );
41//!
42//!     let c = radio(
43//!         "C",
44//!         Choice::C,
45//!         state.selection,
46//!         Message::RadioSelected,
47//!     );
48//!
49//!     let all = radio(
50//!         "All of the above",
51//!         Choice::All,
52//!         state.selection,
53//!         Message::RadioSelected
54//!     );
55//!
56//!     column![a, b, c, all].into()
57//! }
58//! ```
59use crate::core::alignment;
60use crate::core::border::{self, Border};
61use crate::core::event::{self, Event};
62use crate::core::layout;
63use crate::core::mouse;
64use crate::core::renderer;
65use crate::core::text;
66use crate::core::touch;
67use crate::core::widget;
68use crate::core::widget::tree::{self, Tree};
69use crate::core::{
70    Background, Clipboard, Color, Element, Layout, Length, Pixels, Rectangle,
71    Shell, Size, Theme, Widget,
72};
73
74/// A circular button representing a choice.
75///
76/// # Example
77/// ```no_run
78/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
79/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
80/// #
81/// use iced::widget::{column, radio};
82///
83/// struct State {
84///    selection: Option<Choice>,
85/// }
86///
87/// #[derive(Debug, Clone, Copy)]
88/// enum Message {
89///     RadioSelected(Choice),
90/// }
91///
92/// #[derive(Debug, Clone, Copy, PartialEq, Eq)]
93/// enum Choice {
94///     A,
95///     B,
96///     C,
97///     All,
98/// }
99///
100/// fn view(state: &State) -> Element<'_, Message> {
101///     let a = radio(
102///         "A",
103///         Choice::A,
104///         state.selection,
105///         Message::RadioSelected,
106///     );
107///
108///     let b = radio(
109///         "B",
110///         Choice::B,
111///         state.selection,
112///         Message::RadioSelected,
113///     );
114///
115///     let c = radio(
116///         "C",
117///         Choice::C,
118///         state.selection,
119///         Message::RadioSelected,
120///     );
121///
122///     let all = radio(
123///         "All of the above",
124///         Choice::All,
125///         state.selection,
126///         Message::RadioSelected
127///     );
128///
129///     column![a, b, c, all].into()
130/// }
131/// ```
132#[allow(missing_debug_implementations)]
133pub struct Radio<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer>
134where
135    Theme: Catalog,
136    Renderer: text::Renderer,
137{
138    is_selected: bool,
139    on_click: Message,
140    label: String,
141    width: Length,
142    size: f32,
143    spacing: f32,
144    text_size: Option<Pixels>,
145    text_line_height: text::LineHeight,
146    text_shaping: text::Shaping,
147    text_wrapping: text::Wrapping,
148    font: Option<Renderer::Font>,
149    class: Theme::Class<'a>,
150}
151
152impl<'a, Message, Theme, Renderer> Radio<'a, Message, Theme, Renderer>
153where
154    Message: Clone,
155    Theme: Catalog,
156    Renderer: text::Renderer,
157{
158    /// The default size of a [`Radio`] button.
159    pub const DEFAULT_SIZE: f32 = 16.0;
160
161    /// The default spacing of a [`Radio`] button.
162    pub const DEFAULT_SPACING: f32 = 8.0;
163
164    /// Creates a new [`Radio`] button.
165    ///
166    /// It expects:
167    ///   * the value related to the [`Radio`] button
168    ///   * the label of the [`Radio`] button
169    ///   * the current selected value
170    ///   * a function that will be called when the [`Radio`] is selected. It
171    ///     receives the value of the radio and must produce a `Message`.
172    pub fn new<F, V>(
173        label: impl Into<String>,
174        value: V,
175        selected: Option<V>,
176        f: F,
177    ) -> Self
178    where
179        V: Eq + Copy,
180        F: FnOnce(V) -> Message,
181    {
182        Radio {
183            is_selected: Some(value) == selected,
184            on_click: f(value),
185            label: label.into(),
186            width: Length::Shrink,
187            size: Self::DEFAULT_SIZE,
188            spacing: Self::DEFAULT_SPACING,
189            text_size: None,
190            text_line_height: text::LineHeight::default(),
191            text_shaping: text::Shaping::Advanced,
192            text_wrapping: text::Wrapping::default(),
193            font: None,
194            class: Theme::default(),
195        }
196    }
197
198    /// Sets the size of the [`Radio`] button.
199    pub fn size(mut self, size: impl Into<Pixels>) -> Self {
200        self.size = size.into().0;
201        self
202    }
203
204    /// Sets the width of the [`Radio`] button.
205    pub fn width(mut self, width: impl Into<Length>) -> Self {
206        self.width = width.into();
207        self
208    }
209
210    /// Sets the spacing between the [`Radio`] button and the text.
211    pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self {
212        self.spacing = spacing.into().0;
213        self
214    }
215
216    /// Sets the text size of the [`Radio`] button.
217    pub fn text_size(mut self, text_size: impl Into<Pixels>) -> Self {
218        self.text_size = Some(text_size.into());
219        self
220    }
221
222    /// Sets the text [`text::LineHeight`] of the [`Radio`] button.
223    pub fn text_line_height(
224        mut self,
225        line_height: impl Into<text::LineHeight>,
226    ) -> Self {
227        self.text_line_height = line_height.into();
228        self
229    }
230
231    /// Sets the [`text::Shaping`] strategy of the [`Radio`] button.
232    pub fn text_shaping(mut self, shaping: text::Shaping) -> Self {
233        self.text_shaping = shaping;
234        self
235    }
236
237    /// Sets the [`text::Wrapping`] strategy of the [`Radio`] button.
238    pub fn text_wrapping(mut self, wrapping: text::Wrapping) -> Self {
239        self.text_wrapping = wrapping;
240        self
241    }
242
243    /// Sets the text font of the [`Radio`] button.
244    pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
245        self.font = Some(font.into());
246        self
247    }
248
249    /// Sets the style of the [`Radio`] button.
250    #[must_use]
251    pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
252    where
253        Theme::Class<'a>: From<StyleFn<'a, Theme>>,
254    {
255        self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
256        self
257    }
258
259    /// Sets the style class of the [`Radio`] button.
260    #[cfg(feature = "advanced")]
261    #[must_use]
262    pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
263        self.class = class.into();
264        self
265    }
266}
267
268impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
269    for Radio<'a, Message, Theme, Renderer>
270where
271    Message: Clone,
272    Theme: Catalog,
273    Renderer: text::Renderer,
274{
275    fn tag(&self) -> tree::Tag {
276        tree::Tag::of::<widget::text::State<Renderer::Paragraph>>()
277    }
278
279    fn state(&self) -> tree::State {
280        tree::State::new(widget::text::State::<Renderer::Paragraph>::default())
281    }
282
283    fn size(&self) -> Size<Length> {
284        Size {
285            width: self.width,
286            height: Length::Shrink,
287        }
288    }
289
290    fn layout(
291        &self,
292        tree: &mut Tree,
293        renderer: &Renderer,
294        limits: &layout::Limits,
295    ) -> layout::Node {
296        layout::next_to_each_other(
297            &limits.width(self.width),
298            self.spacing,
299            |_| layout::Node::new(Size::new(self.size, self.size)),
300            |limits| {
301                let state = tree
302                    .state
303                    .downcast_mut::<widget::text::State<Renderer::Paragraph>>();
304
305                widget::text::layout(
306                    state,
307                    renderer,
308                    limits,
309                    self.width,
310                    Length::Shrink,
311                    &self.label,
312                    self.text_line_height,
313                    self.text_size,
314                    self.font,
315                    alignment::Horizontal::Left,
316                    alignment::Vertical::Top,
317                    self.text_shaping,
318                    self.text_wrapping,
319                )
320            },
321        )
322    }
323
324    fn on_event(
325        &mut self,
326        _state: &mut Tree,
327        event: Event,
328        layout: Layout<'_>,
329        cursor: mouse::Cursor,
330        _renderer: &Renderer,
331        _clipboard: &mut dyn Clipboard,
332        shell: &mut Shell<'_, Message>,
333        _viewport: &Rectangle,
334    ) -> event::Status {
335        match event {
336            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
337            | Event::Touch(touch::Event::FingerPressed { .. }) => {
338                if cursor.is_over(layout.bounds()) {
339                    shell.publish(self.on_click.clone());
340
341                    return event::Status::Captured;
342                }
343            }
344            _ => {}
345        }
346
347        event::Status::Ignored
348    }
349
350    fn mouse_interaction(
351        &self,
352        _state: &Tree,
353        layout: Layout<'_>,
354        cursor: mouse::Cursor,
355        _viewport: &Rectangle,
356        _renderer: &Renderer,
357    ) -> mouse::Interaction {
358        if cursor.is_over(layout.bounds()) {
359            mouse::Interaction::Pointer
360        } else {
361            mouse::Interaction::default()
362        }
363    }
364
365    fn draw(
366        &self,
367        tree: &Tree,
368        renderer: &mut Renderer,
369        theme: &Theme,
370        defaults: &renderer::Style,
371        layout: Layout<'_>,
372        cursor: mouse::Cursor,
373        viewport: &Rectangle,
374    ) {
375        let is_mouse_over = cursor.is_over(layout.bounds());
376        let is_selected = self.is_selected;
377
378        let mut children = layout.children();
379
380        let status = if is_mouse_over {
381            Status::Hovered { is_selected }
382        } else {
383            Status::Active { is_selected }
384        };
385
386        let style = theme.style(&self.class, status);
387
388        {
389            let layout = children.next().unwrap();
390            let bounds = layout.bounds();
391
392            let size = bounds.width;
393            let dot_size = size / 2.0;
394
395            renderer.fill_quad(
396                renderer::Quad {
397                    bounds,
398                    border: Border {
399                        radius: (size / 2.0).into(),
400                        width: style.border_width,
401                        color: style.border_color,
402                    },
403                    ..renderer::Quad::default()
404                },
405                style.background,
406            );
407
408            if self.is_selected {
409                renderer.fill_quad(
410                    renderer::Quad {
411                        bounds: Rectangle {
412                            x: bounds.x + dot_size / 2.0,
413                            y: bounds.y + dot_size / 2.0,
414                            width: bounds.width - dot_size,
415                            height: bounds.height - dot_size,
416                        },
417                        border: border::rounded(dot_size / 2.0),
418                        ..renderer::Quad::default()
419                    },
420                    style.dot_color,
421                );
422            }
423        }
424
425        {
426            let label_layout = children.next().unwrap();
427            let state: &widget::text::State<Renderer::Paragraph> =
428                tree.state.downcast_ref();
429
430            crate::text::draw(
431                renderer,
432                defaults,
433                label_layout,
434                state.0.raw(),
435                crate::text::Style {
436                    color: style.text_color,
437                },
438                viewport,
439            );
440        }
441    }
442}
443
444impl<'a, Message, Theme, Renderer> From<Radio<'a, Message, Theme, Renderer>>
445    for Element<'a, Message, Theme, Renderer>
446where
447    Message: 'a + Clone,
448    Theme: 'a + Catalog,
449    Renderer: 'a + text::Renderer,
450{
451    fn from(
452        radio: Radio<'a, Message, Theme, Renderer>,
453    ) -> Element<'a, Message, Theme, Renderer> {
454        Element::new(radio)
455    }
456}
457
458/// The possible status of a [`Radio`] button.
459#[derive(Debug, Clone, Copy, PartialEq, Eq)]
460pub enum Status {
461    /// The [`Radio`] button can be interacted with.
462    Active {
463        /// Indicates whether the [`Radio`] button is currently selected.
464        is_selected: bool,
465    },
466    /// The [`Radio`] button is being hovered.
467    Hovered {
468        /// Indicates whether the [`Radio`] button is currently selected.
469        is_selected: bool,
470    },
471}
472
473/// The appearance of a radio button.
474#[derive(Debug, Clone, Copy, PartialEq)]
475pub struct Style {
476    /// The [`Background`] of the radio button.
477    pub background: Background,
478    /// The [`Color`] of the dot of the radio button.
479    pub dot_color: Color,
480    /// The border width of the radio button.
481    pub border_width: f32,
482    /// The border [`Color`] of the radio button.
483    pub border_color: Color,
484    /// The text [`Color`] of the radio button.
485    pub text_color: Option<Color>,
486}
487
488/// The theme catalog of a [`Radio`].
489pub trait Catalog {
490    /// The item class of the [`Catalog`].
491    type Class<'a>;
492
493    /// The default class produced by the [`Catalog`].
494    fn default<'a>() -> Self::Class<'a>;
495
496    /// The [`Style`] of a class with the given status.
497    fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
498}
499
500/// A styling function for a [`Radio`].
501pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
502
503impl Catalog for Theme {
504    type Class<'a> = StyleFn<'a, Self>;
505
506    fn default<'a>() -> Self::Class<'a> {
507        Box::new(default)
508    }
509
510    fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
511        class(self, status)
512    }
513}
514
515/// The default style of a [`Radio`] button.
516pub fn default(theme: &Theme, status: Status) -> Style {
517    let palette = theme.extended_palette();
518
519    let active = Style {
520        background: Color::TRANSPARENT.into(),
521        dot_color: palette.primary.strong.color,
522        border_width: 1.0,
523        border_color: palette.primary.strong.color,
524        text_color: None,
525    };
526
527    match status {
528        Status::Active { .. } => active,
529        Status::Hovered { .. } => Style {
530            dot_color: palette.primary.strong.color,
531            background: palette.primary.weak.color.into(),
532            ..active
533        },
534    }
535}