iced_widget/
toggler.rs

1//! Togglers let users make binary choices by toggling a switch.
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::toggler;
9//!
10//! struct State {
11//!    is_checked: bool,
12//! }
13//!
14//! enum Message {
15//!     TogglerToggled(bool),
16//! }
17//!
18//! fn view(state: &State) -> Element<'_, Message> {
19//!     toggler(state.is_checked)
20//!         .label("Toggle me!")
21//!         .on_toggle(Message::TogglerToggled)
22//!         .into()
23//! }
24//!
25//! fn update(state: &mut State, message: Message) {
26//!     match message {
27//!         Message::TogglerToggled(is_checked) => {
28//!             state.is_checked = is_checked;
29//!         }
30//!     }
31//! }
32//! ```
33//! Show toggle controls using togglers.
34#[cfg(feature = "a11y")]
35use std::borrow::Cow;
36
37use iced_runtime::core::border::Radius;
38
39use crate::core::alignment;
40use crate::core::event;
41use crate::core::layout;
42use crate::core::mouse;
43use crate::core::renderer;
44use crate::core::text;
45use crate::core::touch;
46use crate::core::widget::tree::{self, Tree};
47use crate::core::widget::{self, Id};
48use crate::core::{
49    id, Border, Clipboard, Color, Element, Event, Layout, Length, Pixels,
50    Rectangle, Shell, Size, Theme, Widget,
51};
52
53/// A toggler widget.
54///
55/// # Example
56/// ```no_run
57/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
58/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
59/// #
60/// use iced::widget::toggler;
61///
62/// struct State {
63///    is_checked: bool,
64/// }
65///
66/// enum Message {
67///     TogglerToggled(bool),
68/// }
69///
70/// fn view(state: &State) -> Element<'_, Message> {
71///     toggler(state.is_checked)
72///         .label("Toggle me!")
73///         .on_toggle(Message::TogglerToggled)
74///         .into()
75/// }
76///
77/// fn update(state: &mut State, message: Message) {
78///     match message {
79///         Message::TogglerToggled(is_checked) => {
80///             state.is_checked = is_checked;
81///         }
82///     }
83/// }
84/// ```
85#[allow(missing_debug_implementations)]
86pub struct Toggler<
87    'a,
88    Message,
89    Theme = crate::Theme,
90    Renderer = crate::Renderer,
91> where
92    Theme: Catalog,
93    Renderer: text::Renderer,
94{
95    id: Id,
96    label_id: Option<Id>,
97    #[cfg(feature = "a11y")]
98    name: Option<Cow<'a, str>>,
99    #[cfg(feature = "a11y")]
100    description: Option<iced_accessibility::Description<'a>>,
101    #[cfg(feature = "a11y")]
102    labeled_by_widget: Option<Vec<iced_accessibility::accesskit::NodeId>>,
103    is_toggled: bool,
104    on_toggle: Option<Box<dyn Fn(bool) -> Message + 'a>>,
105    label: Option<text::Fragment<'a>>,
106    width: Length,
107    size: f32,
108    text_size: Option<Pixels>,
109    text_line_height: text::LineHeight,
110    text_alignment: alignment::Horizontal,
111    text_shaping: text::Shaping,
112    text_wrapping: text::Wrapping,
113    spacing: f32,
114    font: Option<Renderer::Font>,
115    class: Theme::Class<'a>,
116}
117
118impl<'a, Message, Theme, Renderer> Toggler<'a, Message, Theme, Renderer>
119where
120    Theme: Catalog,
121    Renderer: text::Renderer,
122{
123    /// The default size of a [`Toggler`].
124    pub const DEFAULT_SIZE: f32 = 16.0;
125
126    /// Creates a new [`Toggler`].
127    ///
128    /// It expects:
129    ///   * a boolean describing whether the [`Toggler`] is checked or not
130    ///   * An optional label for the [`Toggler`]
131    pub fn new(is_toggled: bool) -> Self {
132        Toggler {
133            id: Id::unique(),
134            label_id: None,
135            #[cfg(feature = "a11y")]
136            name: None,
137            #[cfg(feature = "a11y")]
138            description: None,
139            #[cfg(feature = "a11y")]
140            labeled_by_widget: None,
141            is_toggled,
142            on_toggle: None,
143            label: None,
144            width: Length::Shrink,
145            size: Self::DEFAULT_SIZE,
146            text_size: None,
147            text_line_height: text::LineHeight::default(),
148            text_alignment: alignment::Horizontal::Left,
149            text_wrapping: text::Wrapping::default(),
150            spacing: Self::DEFAULT_SIZE / 2.0,
151            text_shaping: text::Shaping::Advanced,
152            font: None,
153            class: Theme::default(),
154        }
155    }
156
157    /// Sets the label of the [`Toggler`].
158    pub fn label(mut self, label: impl text::IntoFragment<'a>) -> Self {
159        self.label = Some(label.into_fragment());
160        self.label_id = Some(Id::unique());
161        self
162    }
163
164    /// Sets the message that should be produced when a user toggles
165    /// the [`Toggler`].
166    ///
167    /// If this method is not called, the [`Toggler`] will be disabled.
168    pub fn on_toggle(
169        mut self,
170        on_toggle: impl Fn(bool) -> Message + 'a,
171    ) -> Self {
172        self.on_toggle = Some(Box::new(on_toggle));
173        self
174    }
175
176    /// Sets the message that should be produced when a user toggles
177    /// the [`Toggler`], if `Some`.
178    ///
179    /// If `None`, the [`Toggler`] will be disabled.
180    pub fn on_toggle_maybe(
181        mut self,
182        on_toggle: Option<impl Fn(bool) -> Message + 'a>,
183    ) -> Self {
184        self.on_toggle = on_toggle.map(|on_toggle| Box::new(on_toggle) as _);
185        self
186    }
187
188    /// Sets the size of the [`Toggler`].
189    pub fn size(mut self, size: impl Into<Pixels>) -> Self {
190        self.size = size.into().0;
191        self
192    }
193
194    /// Sets the width of the [`Toggler`].
195    pub fn width(mut self, width: impl Into<Length>) -> Self {
196        self.width = width.into();
197        self
198    }
199
200    /// Sets the text size o the [`Toggler`].
201    pub fn text_size(mut self, text_size: impl Into<Pixels>) -> Self {
202        self.text_size = Some(text_size.into());
203        self
204    }
205
206    /// Sets the text [`text::LineHeight`] of the [`Toggler`].
207    pub fn text_line_height(
208        mut self,
209        line_height: impl Into<text::LineHeight>,
210    ) -> Self {
211        self.text_line_height = line_height.into();
212        self
213    }
214
215    /// Sets the horizontal alignment of the text of the [`Toggler`]
216    pub fn text_alignment(mut self, alignment: alignment::Horizontal) -> Self {
217        self.text_alignment = alignment;
218        self
219    }
220
221    /// Sets the [`text::Shaping`] strategy of the [`Toggler`].
222    pub fn text_shaping(mut self, shaping: text::Shaping) -> Self {
223        self.text_shaping = shaping;
224        self
225    }
226
227    /// Sets the [`text::Wrapping`] strategy of the [`Toggler`].
228    pub fn text_wrapping(mut self, wrapping: text::Wrapping) -> Self {
229        self.text_wrapping = wrapping;
230        self
231    }
232
233    /// Sets the spacing between the [`Toggler`] and the text.
234    pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self {
235        self.spacing = spacing.into().0;
236        self
237    }
238
239    /// Sets the [`Renderer::Font`] of the text of the [`Toggler`]
240    ///
241    /// [`Renderer::Font`]: crate::core::text::Renderer
242    pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
243        self.font = Some(font.into());
244        self
245    }
246
247    /// Sets the style of the [`Toggler`].
248    #[must_use]
249    pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
250    where
251        Theme::Class<'a>: From<StyleFn<'a, Theme>>,
252    {
253        self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
254        self
255    }
256
257    /// Sets the style class of the [`Toggler`].
258    #[cfg(feature = "advanced")]
259    #[must_use]
260    pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
261        self.class = class.into();
262        self
263    }
264
265    #[cfg(feature = "a11y")]
266    /// Sets the name of the [`Toggler`].
267    pub fn name(mut self, name: impl Into<Cow<'a, str>>) -> Self {
268        self.name = Some(name.into());
269        self
270    }
271
272    #[cfg(feature = "a11y")]
273    /// Sets the description of the [`Toggler`].
274    pub fn description_widget<T: iced_accessibility::Describes>(
275        mut self,
276        description: &T,
277    ) -> Self {
278        self.description = Some(iced_accessibility::Description::Id(
279            description.description(),
280        ));
281        self
282    }
283
284    #[cfg(feature = "a11y")]
285    /// Sets the description of the [`Toggler`].
286    pub fn description(mut self, description: impl Into<Cow<'a, str>>) -> Self {
287        self.description =
288            Some(iced_accessibility::Description::Text(description.into()));
289        self
290    }
291
292    #[cfg(feature = "a11y")]
293    /// Sets the label of the [`Toggler`] using another widget.
294    pub fn labeled_by_widget(
295        mut self,
296        label: &dyn iced_accessibility::Labels,
297    ) -> Self {
298        self.labeled_by_widget =
299            Some(label.label().into_iter().map(|l| l.into()).collect());
300        self
301    }
302}
303
304impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
305    for Toggler<'a, Message, Theme, Renderer>
306where
307    Theme: Catalog,
308    Renderer: text::Renderer,
309{
310    fn tag(&self) -> tree::Tag {
311        tree::Tag::of::<widget::text::State<Renderer::Paragraph>>()
312    }
313
314    fn state(&self) -> tree::State {
315        tree::State::new(widget::text::State::<Renderer::Paragraph>::default())
316    }
317
318    fn size(&self) -> Size<Length> {
319        Size {
320            width: self.width,
321            height: Length::Shrink,
322        }
323    }
324
325    fn layout(
326        &self,
327        tree: &mut Tree,
328        renderer: &Renderer,
329        limits: &layout::Limits,
330    ) -> layout::Node {
331        let limits = limits.width(self.width);
332
333        layout::next_to_each_other(
334            &limits,
335            self.spacing,
336            |_| layout::Node::new(crate::core::Size::new(48., 24.)),
337            |limits| {
338                if let Some(label) = self.label.as_deref() {
339                    let state = tree
340                    .state
341                    .downcast_mut::<widget::text::State<Renderer::Paragraph>>();
342
343                    widget::text::layout(
344                        state,
345                        renderer,
346                        limits,
347                        self.width,
348                        Length::Shrink,
349                        label,
350                        self.text_line_height,
351                        self.text_size,
352                        self.font,
353                        self.text_alignment,
354                        alignment::Vertical::Top,
355                        self.text_shaping,
356                        self.text_wrapping,
357                    )
358                } else {
359                    layout::Node::new(crate::core::Size::ZERO)
360                }
361            },
362        )
363    }
364
365    fn on_event(
366        &mut self,
367        _state: &mut Tree,
368        event: Event,
369        layout: Layout<'_>,
370        cursor: mouse::Cursor,
371        _renderer: &Renderer,
372        _clipboard: &mut dyn Clipboard,
373        shell: &mut Shell<'_, Message>,
374        _viewport: &Rectangle,
375    ) -> event::Status {
376        let Some(on_toggle) = &self.on_toggle else {
377            return event::Status::Ignored;
378        };
379
380        match event {
381            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
382            | Event::Touch(touch::Event::FingerPressed { .. }) => {
383                let mouse_over = cursor.is_over(layout.bounds());
384
385                if mouse_over {
386                    shell.publish(on_toggle(!self.is_toggled));
387
388                    event::Status::Captured
389                } else {
390                    event::Status::Ignored
391                }
392            }
393            _ => event::Status::Ignored,
394        }
395    }
396
397    fn mouse_interaction(
398        &self,
399        _state: &Tree,
400        layout: Layout<'_>,
401        cursor: mouse::Cursor,
402        _viewport: &Rectangle,
403        _renderer: &Renderer,
404    ) -> mouse::Interaction {
405        if cursor.is_over(layout.bounds()) {
406            if self.on_toggle.is_some() {
407                mouse::Interaction::Pointer
408            } else {
409                mouse::Interaction::NotAllowed
410            }
411        } else {
412            mouse::Interaction::default()
413        }
414    }
415
416    fn draw(
417        &self,
418        tree: &Tree,
419        renderer: &mut Renderer,
420        theme: &Theme,
421        style: &renderer::Style,
422        layout: Layout<'_>,
423        cursor: mouse::Cursor,
424        viewport: &Rectangle,
425    ) {
426        let mut children = layout.children();
427        let toggler_layout = children.next().unwrap();
428
429        if self.label.is_some() {
430            let label_layout = children.next().unwrap();
431            let state: &widget::text::State<Renderer::Paragraph> =
432                tree.state.downcast_ref();
433
434            crate::text::draw(
435                renderer,
436                style,
437                label_layout,
438                state.0.raw(),
439                crate::text::Style::default(),
440                viewport,
441            );
442        }
443
444        let bounds = toggler_layout.bounds();
445        let is_mouse_over = cursor.is_over(layout.bounds());
446
447        let status = if self.on_toggle.is_none() {
448            Status::Disabled
449        } else if is_mouse_over {
450            Status::Hovered {
451                is_toggled: self.is_toggled,
452            }
453        } else {
454            Status::Active {
455                is_toggled: self.is_toggled,
456            }
457        };
458
459        let style = theme.style(&self.class, status);
460
461        let space = style.handle_margin;
462
463        let toggler_background_bounds = Rectangle {
464            x: bounds.x,
465            y: bounds.y,
466            width: bounds.width,
467            height: bounds.height,
468        };
469
470        renderer.fill_quad(
471            renderer::Quad {
472                bounds: toggler_background_bounds,
473                border: Border {
474                    radius: style.border_radius,
475                    width: style.background_border_width,
476                    color: style.background_border_color,
477                },
478                ..renderer::Quad::default()
479            },
480            style.background,
481        );
482
483        let toggler_foreground_bounds = Rectangle {
484            x: bounds.x
485                + if self.is_toggled {
486                    bounds.width - space - (bounds.height - (2.0 * space))
487                } else {
488                    space
489                },
490            y: bounds.y + space,
491            width: bounds.height - (2.0 * space),
492            height: bounds.height - (2.0 * space),
493        };
494
495        renderer.fill_quad(
496            renderer::Quad {
497                bounds: toggler_foreground_bounds,
498                border: Border {
499                    radius: style.handle_radius,
500                    width: style.foreground_border_width,
501                    color: style.foreground_border_color,
502                },
503                ..renderer::Quad::default()
504            },
505            style.foreground,
506        );
507    }
508
509    #[cfg(feature = "a11y")]
510    /// get the a11y nodes for the widget
511    fn a11y_nodes(
512        &self,
513        layout: Layout<'_>,
514        _state: &Tree,
515        cursor: mouse::Cursor,
516    ) -> iced_accessibility::A11yTree {
517        use iced_accessibility::{
518            accesskit::{Action, NodeBuilder, NodeId, Rect, Role},
519            A11yNode, A11yTree,
520        };
521
522        let bounds = layout.bounds();
523        let is_hovered = cursor.is_over(bounds);
524        let Rectangle {
525            x,
526            y,
527            width,
528            height,
529        } = bounds;
530
531        let bounds = Rect::new(
532            x as f64,
533            y as f64,
534            (x + width) as f64,
535            (y + height) as f64,
536        );
537
538        let mut node = NodeBuilder::new(Role::Switch);
539        node.add_action(Action::Focus);
540        node.add_action(Action::Default);
541        node.set_bounds(bounds);
542        if let Some(name) = self.name.as_ref() {
543            node.set_name(name.clone());
544        }
545        match self.description.as_ref() {
546            Some(iced_accessibility::Description::Id(id)) => {
547                node.set_described_by(
548                    id.iter()
549                        .cloned()
550                        .map(|id| NodeId::from(id))
551                        .collect::<Vec<_>>(),
552                );
553            }
554            Some(iced_accessibility::Description::Text(text)) => {
555                node.set_description(text.clone());
556            }
557            None => {}
558        }
559        node.set_selected(self.is_toggled);
560        if is_hovered {
561            node.set_hovered();
562        }
563        node.add_action(Action::Default);
564        if let Some(label) = self.label.as_ref() {
565            let mut label_node = NodeBuilder::new(Role::Label);
566
567            label_node.set_name(label.clone());
568            // TODO proper label bounds for the label
569            label_node.set_bounds(bounds);
570
571            A11yTree::node_with_child_tree(
572                A11yNode::new(node, self.id.clone()),
573                A11yTree::leaf(label_node, self.label_id.clone().unwrap()),
574            )
575        } else {
576            if let Some(labeled_by_widget) = self.labeled_by_widget.as_ref() {
577                node.set_labelled_by(labeled_by_widget.clone());
578            }
579            A11yTree::leaf(node, self.id.clone())
580        }
581    }
582
583    fn id(&self) -> Option<Id> {
584        if self.label.is_some() {
585            Some(Id(iced_runtime::core::id::Internal::Set(vec![
586                self.id.0.clone(),
587                self.label_id.clone().unwrap().0,
588            ])))
589        } else {
590            Some(self.id.clone())
591        }
592    }
593
594    fn set_id(&mut self, id: Id) {
595        if let Id(id::Internal::Set(list)) = id {
596            if list.len() == 2 && self.label.is_some() {
597                self.id.0 = list[0].clone();
598                self.label_id = Some(Id(list[1].clone()));
599            }
600        } else if self.label.is_none() {
601            self.id = id;
602        }
603    }
604}
605
606impl<'a, Message, Theme, Renderer> From<Toggler<'a, Message, Theme, Renderer>>
607    for Element<'a, Message, Theme, Renderer>
608where
609    Message: 'a,
610    Theme: Catalog + 'a,
611    Renderer: text::Renderer + 'a,
612{
613    fn from(
614        toggler: Toggler<'a, Message, Theme, Renderer>,
615    ) -> Element<'a, Message, Theme, Renderer> {
616        Element::new(toggler)
617    }
618}
619
620/// The possible status of a [`Toggler`].
621#[derive(Debug, Clone, Copy, PartialEq, Eq)]
622pub enum Status {
623    /// The [`Toggler`] can be interacted with.
624    Active {
625        /// Indicates whether the [`Toggler`] is toggled.
626        is_toggled: bool,
627    },
628    /// The [`Toggler`] is being hovered.
629    Hovered {
630        /// Indicates whether the [`Toggler`] is toggled.
631        is_toggled: bool,
632    },
633    /// The [`Toggler`] is disabled.
634    Disabled,
635}
636
637/// The appearance of a toggler.
638#[derive(Debug, Clone, Copy, PartialEq)]
639pub struct Style {
640    /// The background [`Color`] of the toggler.
641    pub background: Color,
642    /// The width of the background border of the toggler.
643    pub background_border_width: f32,
644    /// The [`Color`] of the background border of the toggler.
645    pub background_border_color: Color,
646    /// The foreground [`Color`] of the toggler.
647    pub foreground: Color,
648    /// The width of the foreground border of the toggler.
649    pub foreground_border_width: f32,
650    /// The [`Color`] of the foreground border of the toggler.
651    pub foreground_border_color: Color,
652    /// The border radius of the toggler.
653    pub border_radius: Radius,
654    /// the radius of the handle of the toggler
655    pub handle_radius: Radius,
656    /// the space between the handle and the border of the toggler
657    pub handle_margin: f32,
658}
659
660/// The theme catalog of a [`Toggler`].
661pub trait Catalog: Sized {
662    /// The item class of the [`Catalog`].
663    type Class<'a>;
664
665    /// The default class produced by the [`Catalog`].
666    fn default<'a>() -> Self::Class<'a>;
667
668    /// The [`Style`] of a class with the given status.
669    fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
670}
671
672/// A styling function for a [`Toggler`].
673///
674/// This is just a boxed closure: `Fn(&Theme, Status) -> Style`.
675pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
676
677impl Catalog for Theme {
678    type Class<'a> = StyleFn<'a, Self>;
679
680    fn default<'a>() -> Self::Class<'a> {
681        Box::new(default)
682    }
683
684    fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
685        class(self, status)
686    }
687}
688
689/// The default style of a [`Toggler`].
690pub fn default(theme: &Theme, status: Status) -> Style {
691    let palette = theme.extended_palette();
692
693    let background = match status {
694        Status::Active { is_toggled } | Status::Hovered { is_toggled } => {
695            if is_toggled {
696                palette.primary.strong.color
697            } else {
698                palette.background.strong.color
699            }
700        }
701        Status::Disabled => palette.background.weak.color,
702    };
703
704    let foreground = match status {
705        Status::Active { is_toggled } => {
706            if is_toggled {
707                palette.primary.strong.text
708            } else {
709                palette.background.base.color
710            }
711        }
712        Status::Hovered { is_toggled } => {
713            if is_toggled {
714                Color {
715                    a: 0.5,
716                    ..palette.primary.strong.text
717                }
718            } else {
719                palette.background.weak.color
720            }
721        }
722        Status::Disabled => palette.background.base.color,
723    };
724
725    Style {
726        background,
727        foreground,
728        foreground_border_width: 0.0,
729        foreground_border_color: Color::TRANSPARENT,
730        background_border_width: 0.0,
731        background_border_color: Color::TRANSPARENT,
732        border_radius: Radius::from(8.0),
733        handle_radius: Radius::from(8.0),
734        handle_margin: 2.0,
735    }
736}