Skip to main content

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//! ```
33use crate::core::alignment;
34use crate::core::border;
35use crate::core::keyboard;
36use crate::core::keyboard::key;
37use crate::core::layout;
38use crate::core::mouse;
39use crate::core::renderer;
40use crate::core::text;
41use crate::core::theme::palette;
42use crate::core::touch;
43use crate::core::widget;
44use crate::core::widget::operation::accessible::{Accessible, Role};
45use crate::core::widget::operation::focusable::{self, Focusable};
46use crate::core::widget::tree::{self, Tree};
47use crate::core::window;
48use crate::core::{
49    Background, Border, Color, Element, Event, Layout, Length, Pixels, Rectangle, Shadow, Shell,
50    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/// ```
85pub struct Toggler<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer>
86where
87    Theme: Catalog,
88    Renderer: text::Renderer,
89{
90    is_toggled: bool,
91    on_toggle: Option<Box<dyn Fn(bool) -> Message + 'a>>,
92    label: Option<text::Fragment<'a>>,
93    width: Length,
94    size: f32,
95    text_size: Option<Pixels>,
96    line_height: text::LineHeight,
97    alignment: text::Alignment,
98    text_shaping: text::Shaping,
99    wrapping: text::Wrapping,
100    spacing: f32,
101    font: Option<Renderer::Font>,
102    class: Theme::Class<'a>,
103    last_status: Option<Status>,
104}
105
106impl<'a, Message, Theme, Renderer> Toggler<'a, Message, Theme, Renderer>
107where
108    Theme: Catalog,
109    Renderer: text::Renderer,
110{
111    /// The default size of a [`Toggler`].
112    pub const DEFAULT_SIZE: f32 = 16.0;
113
114    /// Creates a new [`Toggler`].
115    ///
116    /// It expects:
117    ///   * a boolean describing whether the [`Toggler`] is checked or not
118    ///   * An optional label for the [`Toggler`]
119    ///   * a function that will be called when the [`Toggler`] is toggled. It
120    ///     will receive the new state of the [`Toggler`] and must produce a
121    ///     `Message`.
122    pub fn new(is_toggled: bool) -> Self {
123        Toggler {
124            is_toggled,
125            on_toggle: None,
126            label: None,
127            width: Length::Shrink,
128            size: Self::DEFAULT_SIZE,
129            text_size: None,
130            line_height: text::LineHeight::default(),
131            alignment: text::Alignment::Default,
132            text_shaping: text::Shaping::default(),
133            wrapping: text::Wrapping::default(),
134            spacing: Self::DEFAULT_SIZE / 2.0,
135            font: None,
136            class: Theme::default(),
137            last_status: None,
138        }
139    }
140
141    /// Sets the label of the [`Toggler`].
142    pub fn label(mut self, label: impl text::IntoFragment<'a>) -> Self {
143        self.label = Some(label.into_fragment());
144        self
145    }
146
147    /// Sets the message that should be produced when a user toggles
148    /// the [`Toggler`].
149    ///
150    /// If this method is not called, the [`Toggler`] will be disabled.
151    pub fn on_toggle(mut self, on_toggle: impl Fn(bool) -> Message + 'a) -> Self {
152        self.on_toggle = Some(Box::new(on_toggle));
153        self
154    }
155
156    /// Sets the message that should be produced when a user toggles
157    /// the [`Toggler`], if `Some`.
158    ///
159    /// If `None`, the [`Toggler`] will be disabled.
160    pub fn on_toggle_maybe(mut self, on_toggle: Option<impl Fn(bool) -> Message + 'a>) -> Self {
161        self.on_toggle = on_toggle.map(|on_toggle| Box::new(on_toggle) as _);
162        self
163    }
164
165    /// Sets the size of the [`Toggler`].
166    pub fn size(mut self, size: impl Into<Pixels>) -> Self {
167        self.size = size.into().0;
168        self
169    }
170
171    /// Sets the width of the [`Toggler`].
172    pub fn width(mut self, width: impl Into<Length>) -> Self {
173        self.width = width.into();
174        self
175    }
176
177    /// Sets the text size o the [`Toggler`].
178    pub fn text_size(mut self, text_size: impl Into<Pixels>) -> Self {
179        self.text_size = Some(text_size.into());
180        self
181    }
182
183    /// Sets the text [`text::LineHeight`] of the [`Toggler`].
184    pub fn line_height(mut self, line_height: impl Into<text::LineHeight>) -> Self {
185        self.line_height = line_height.into();
186        self
187    }
188
189    /// Sets the horizontal alignment of the text of the [`Toggler`]
190    pub fn alignment(mut self, alignment: impl Into<text::Alignment>) -> Self {
191        self.alignment = alignment.into();
192        self
193    }
194
195    /// Sets the [`text::Shaping`] strategy of the [`Toggler`].
196    pub fn shaping(mut self, shaping: text::Shaping) -> Self {
197        self.text_shaping = shaping;
198        self
199    }
200
201    /// Sets the [`text::Wrapping`] strategy of the [`Toggler`].
202    pub fn wrapping(mut self, wrapping: text::Wrapping) -> Self {
203        self.wrapping = wrapping;
204        self
205    }
206
207    /// Sets the spacing between the [`Toggler`] and the text.
208    pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self {
209        self.spacing = spacing.into().0;
210        self
211    }
212
213    /// Sets the [`Renderer::Font`] of the text of the [`Toggler`]
214    ///
215    /// [`Renderer::Font`]: crate::core::text::Renderer
216    pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
217        self.font = Some(font.into());
218        self
219    }
220
221    /// Sets the style of the [`Toggler`].
222    #[must_use]
223    pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
224    where
225        Theme::Class<'a>: From<StyleFn<'a, Theme>>,
226    {
227        self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
228        self
229    }
230
231    /// Sets the style class of the [`Toggler`].
232    #[cfg(feature = "advanced")]
233    #[must_use]
234    pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
235        self.class = class.into();
236        self
237    }
238}
239
240#[derive(Debug, Clone, Default)]
241struct State<P: text::Paragraph> {
242    is_focused: bool,
243    focus_visible: bool,
244    label: widget::text::State<P>,
245}
246
247impl<P: text::Paragraph> focusable::Focusable for State<P> {
248    fn is_focused(&self) -> bool {
249        self.is_focused
250    }
251
252    fn focus(&mut self) {
253        self.is_focused = true;
254        self.focus_visible = true;
255    }
256
257    fn unfocus(&mut self) {
258        self.is_focused = false;
259        self.focus_visible = false;
260    }
261}
262
263impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
264    for Toggler<'_, Message, Theme, Renderer>
265where
266    Theme: Catalog,
267    Renderer: text::Renderer,
268{
269    fn tag(&self) -> tree::Tag {
270        tree::Tag::of::<State<Renderer::Paragraph>>()
271    }
272
273    fn state(&self) -> tree::State {
274        tree::State::new(State::<Renderer::Paragraph>::default())
275    }
276
277    fn size(&self) -> Size<Length> {
278        Size {
279            width: self.width,
280            height: Length::Shrink,
281        }
282    }
283
284    fn layout(
285        &mut self,
286        tree: &mut Tree,
287        renderer: &Renderer,
288        limits: &layout::Limits,
289    ) -> layout::Node {
290        let limits = limits.width(self.width);
291
292        layout::next_to_each_other(
293            &limits,
294            if self.label.is_some() {
295                self.spacing
296            } else {
297                0.0
298            },
299            |_| {
300                let size = if renderer::CRISP {
301                    let scale_factor = renderer.scale_factor().unwrap_or(1.0);
302
303                    (self.size * scale_factor).round() / scale_factor
304                } else {
305                    self.size
306                };
307
308                layout::Node::new(Size::new(2.0 * size, size))
309            },
310            |limits| {
311                if let Some(label) = self.label.as_deref() {
312                    let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
313
314                    widget::text::layout(
315                        &mut state.label,
316                        renderer,
317                        limits,
318                        label,
319                        widget::text::Format {
320                            width: self.width,
321                            height: Length::Shrink,
322                            line_height: self.line_height,
323                            size: self.text_size,
324                            font: self.font,
325                            align_x: self.alignment,
326                            align_y: alignment::Vertical::Top,
327                            shaping: self.text_shaping,
328                            wrapping: self.wrapping,
329                            ellipsis: text::Ellipsis::None,
330                        },
331                    )
332                } else {
333                    layout::Node::new(Size::ZERO)
334                }
335            },
336        )
337    }
338
339    fn operate(
340        &mut self,
341        tree: &mut Tree,
342        layout: Layout<'_>,
343        _renderer: &Renderer,
344        operation: &mut dyn widget::Operation,
345    ) {
346        let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
347
348        operation.accessible(
349            None,
350            layout.bounds(),
351            &Accessible {
352                role: Role::Switch,
353                label: self.label.as_deref(),
354                toggled: Some(self.is_toggled),
355                disabled: self.on_toggle.is_none(),
356                ..Accessible::default()
357            },
358        );
359
360        if self.on_toggle.is_some() {
361            operation.focusable(None, layout.bounds(), state);
362        } else {
363            state.unfocus();
364        }
365    }
366
367    fn update(
368        &mut self,
369        tree: &mut Tree,
370        event: &Event,
371        layout: Layout<'_>,
372        cursor: mouse::Cursor,
373        _renderer: &Renderer,
374        shell: &mut Shell<'_, Message>,
375        _viewport: &Rectangle,
376    ) {
377        let Some(on_toggle) = &self.on_toggle else {
378            return;
379        };
380
381        match event {
382            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
383            | Event::Touch(touch::Event::FingerPressed { .. }) => {
384                let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
385
386                if cursor.is_over(layout.bounds()) {
387                    state.is_focused = true;
388                    state.focus_visible = false;
389
390                    shell.publish(on_toggle(!self.is_toggled));
391                    shell.capture_event();
392                } else {
393                    state.is_focused = false;
394                    state.focus_visible = false;
395                }
396            }
397            Event::Keyboard(keyboard::Event::KeyPressed {
398                key: keyboard::Key::Named(key::Named::Space),
399                ..
400            }) => {
401                let state = tree.state.downcast_ref::<State<Renderer::Paragraph>>();
402
403                if state.is_focused {
404                    shell.publish(on_toggle(!self.is_toggled));
405                    shell.capture_event();
406                }
407            }
408            Event::Keyboard(keyboard::Event::KeyPressed {
409                key: keyboard::Key::Named(key::Named::Escape),
410                ..
411            }) => {
412                let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
413                if state.is_focused {
414                    state.is_focused = false;
415                    state.focus_visible = false;
416                    shell.capture_event();
417                }
418            }
419            _ => {}
420        }
421
422        let current_status = if self.on_toggle.is_none() {
423            Status::Disabled {
424                is_toggled: self.is_toggled,
425            }
426        } else {
427            let state = tree.state.downcast_ref::<State<Renderer::Paragraph>>();
428
429            if state.focus_visible {
430                Status::Focused {
431                    is_toggled: self.is_toggled,
432                }
433            } else if cursor.is_over(layout.bounds()) {
434                Status::Hovered {
435                    is_toggled: self.is_toggled,
436                }
437            } else {
438                Status::Active {
439                    is_toggled: self.is_toggled,
440                }
441            }
442        };
443
444        if let Event::Window(window::Event::RedrawRequested(_now)) = event {
445            self.last_status = Some(current_status);
446        } else if self
447            .last_status
448            .is_some_and(|status| status != current_status)
449        {
450            shell.request_redraw();
451        }
452    }
453
454    fn mouse_interaction(
455        &self,
456        _tree: &Tree,
457        layout: Layout<'_>,
458        cursor: mouse::Cursor,
459        _viewport: &Rectangle,
460        _renderer: &Renderer,
461    ) -> mouse::Interaction {
462        if cursor.is_over(layout.bounds()) {
463            if self.on_toggle.is_some() {
464                mouse::Interaction::Pointer
465            } else {
466                mouse::Interaction::NotAllowed
467            }
468        } else {
469            mouse::Interaction::default()
470        }
471    }
472
473    fn draw(
474        &self,
475        tree: &Tree,
476        renderer: &mut Renderer,
477        theme: &Theme,
478        defaults: &renderer::Style,
479        layout: Layout<'_>,
480        _cursor: mouse::Cursor,
481        viewport: &Rectangle,
482    ) {
483        let mut children = layout.children();
484        let toggler_layout = children.next().unwrap();
485
486        let style = theme.style(
487            &self.class,
488            self.last_status.unwrap_or(Status::Disabled {
489                is_toggled: self.is_toggled,
490            }),
491        );
492
493        if self.label.is_some() {
494            let label_layout = children.next().unwrap();
495            let state: &State<Renderer::Paragraph> = tree.state.downcast_ref();
496
497            crate::text::draw(
498                renderer,
499                defaults,
500                label_layout.bounds(),
501                state.label.raw(),
502                crate::text::Style {
503                    color: style.text_color,
504                },
505                viewport,
506            );
507        }
508
509        let scale_factor = renderer.scale_factor().unwrap_or(1.0);
510        let bounds = toggler_layout.bounds();
511
512        let border_radius = style
513            .border_radius
514            .unwrap_or_else(|| border::Radius::new(bounds.height / 2.0));
515
516        renderer.fill_quad(
517            renderer::Quad {
518                bounds,
519                border: Border {
520                    radius: border_radius,
521                    width: style.background_border_width,
522                    color: style.background_border_color,
523                },
524                shadow: style.shadow,
525                ..renderer::Quad::default()
526            },
527            style.background,
528        );
529
530        let toggle_bounds = {
531            // Try to align toggle to the pixel grid
532            let bounds = if renderer::CRISP {
533                (bounds * scale_factor).round()
534            } else {
535                bounds
536            };
537
538            let padding = (style.padding_ratio * bounds.height).round();
539
540            Rectangle {
541                x: bounds.x
542                    + if self.is_toggled {
543                        bounds.width - bounds.height + padding
544                    } else {
545                        padding
546                    },
547                y: bounds.y + padding,
548                width: bounds.height - (2.0 * padding),
549                height: bounds.height - (2.0 * padding),
550            } * (1.0 / scale_factor)
551        };
552
553        renderer.fill_quad(
554            renderer::Quad {
555                bounds: toggle_bounds,
556                border: Border {
557                    radius: border_radius,
558                    width: style.foreground_border_width,
559                    color: style.foreground_border_color,
560                },
561                ..renderer::Quad::default()
562            },
563            style.foreground,
564        );
565    }
566}
567
568impl<'a, Message, Theme, Renderer> From<Toggler<'a, Message, Theme, Renderer>>
569    for Element<'a, Message, Theme, Renderer>
570where
571    Message: 'a,
572    Theme: Catalog + 'a,
573    Renderer: text::Renderer + 'a,
574{
575    fn from(
576        toggler: Toggler<'a, Message, Theme, Renderer>,
577    ) -> Element<'a, Message, Theme, Renderer> {
578        Element::new(toggler)
579    }
580}
581
582/// The possible status of a [`Toggler`].
583#[derive(Debug, Clone, Copy, PartialEq, Eq)]
584pub enum Status {
585    /// The [`Toggler`] can be interacted with.
586    Active {
587        /// Indicates whether the [`Toggler`] is toggled.
588        is_toggled: bool,
589    },
590    /// The [`Toggler`] is being hovered.
591    Hovered {
592        /// Indicates whether the [`Toggler`] is toggled.
593        is_toggled: bool,
594    },
595    /// The [`Toggler`] has keyboard focus.
596    Focused {
597        /// Indicates whether the [`Toggler`] is toggled.
598        is_toggled: bool,
599    },
600    /// The [`Toggler`] is disabled.
601    Disabled {
602        /// Indicates whether the [`Toggler`] is toggled.
603        is_toggled: bool,
604    },
605}
606
607/// The appearance of a toggler.
608#[derive(Debug, Clone, Copy, PartialEq)]
609pub struct Style {
610    /// The background [`Color`] of the toggler.
611    pub background: Background,
612    /// The width of the background border of the toggler.
613    pub background_border_width: f32,
614    /// The [`Color`] of the background border of the toggler.
615    pub background_border_color: Color,
616    /// The foreground [`Color`] of the toggler.
617    pub foreground: Background,
618    /// The width of the foreground border of the toggler.
619    pub foreground_border_width: f32,
620    /// The [`Color`] of the foreground border of the toggler.
621    pub foreground_border_color: Color,
622    /// The text [`Color`] of the toggler.
623    pub text_color: Option<Color>,
624    /// The border radius of the toggler.
625    ///
626    /// If `None`, the toggler will be perfectly round.
627    pub border_radius: Option<border::Radius>,
628    /// The ratio of separation between the background and the toggle in relative height.
629    pub padding_ratio: f32,
630    /// The [`Shadow`] of the toggler.
631    pub shadow: Shadow,
632}
633
634/// The theme catalog of a [`Toggler`].
635pub trait Catalog: Sized {
636    /// The item class of the [`Catalog`].
637    type Class<'a>;
638
639    /// The default class produced by the [`Catalog`].
640    fn default<'a>() -> Self::Class<'a>;
641
642    /// The [`Style`] of a class with the given status.
643    fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
644}
645
646/// A styling function for a [`Toggler`].
647///
648/// This is just a boxed closure: `Fn(&Theme, Status) -> Style`.
649pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
650
651impl Catalog for Theme {
652    type Class<'a> = StyleFn<'a, Self>;
653
654    fn default<'a>() -> Self::Class<'a> {
655        Box::new(default)
656    }
657
658    fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
659        class(self, status)
660    }
661}
662
663/// The default style of a [`Toggler`].
664pub fn default(theme: &Theme, status: Status) -> Style {
665    let palette = theme.palette();
666
667    let background = match status {
668        Status::Active { is_toggled }
669        | Status::Hovered { is_toggled }
670        | Status::Focused { is_toggled } => {
671            if is_toggled {
672                palette.primary.base.color
673            } else {
674                palette.background.strong.color
675            }
676        }
677        Status::Disabled { is_toggled } => {
678            if is_toggled {
679                palette.background.strong.color
680            } else {
681                palette.background.weak.color
682            }
683        }
684    };
685
686    let foreground = match status {
687        Status::Active { is_toggled } | Status::Focused { is_toggled } => {
688            if is_toggled {
689                palette.primary.base.text
690            } else {
691                palette.background.base.color
692            }
693        }
694        Status::Hovered { is_toggled } => {
695            if is_toggled {
696                Color {
697                    a: 0.5,
698                    ..palette.primary.base.text
699                }
700            } else {
701                palette.background.weak.color
702            }
703        }
704        Status::Disabled { .. } => palette.background.weakest.color,
705    };
706
707    let page_bg = palette.background.base.color;
708    let accent = palette.primary.strong.color;
709
710    let (background_border_width, background_border_color) = match status {
711        Status::Focused { is_toggled } => {
712            let widget_bg = if is_toggled {
713                palette.primary.base.color
714            } else {
715                palette.background.strong.color
716            };
717            (2.0, palette::focus_border_color(widget_bg, accent, page_bg))
718        }
719        _ => (0.0, Color::TRANSPARENT),
720    };
721
722    let shadow = match status {
723        Status::Focused { .. } => palette::focus_shadow(accent, page_bg),
724        _ => Shadow::default(),
725    };
726
727    Style {
728        background: background.into(),
729        foreground: foreground.into(),
730        foreground_border_width: 0.0,
731        foreground_border_color: Color::TRANSPARENT,
732        background_border_width,
733        background_border_color,
734        text_color: None,
735        border_radius: None,
736        padding_ratio: 0.1,
737        shadow,
738    }
739}
740
741#[cfg(test)]
742mod tests {
743    use super::*;
744    use crate::core::widget::operation::focusable::Focusable;
745
746    type TestState = State<()>;
747
748    #[test]
749    fn focusable_trait() {
750        let mut state = TestState::default();
751        assert!(!state.is_focused());
752        assert!(!state.focus_visible);
753        state.focus();
754        assert!(state.is_focused());
755        assert!(state.focus_visible);
756        state.unfocus();
757        assert!(!state.is_focused());
758        assert!(!state.focus_visible);
759    }
760}