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::layout;
36use crate::core::mouse;
37use crate::core::renderer;
38use crate::core::text;
39use crate::core::touch;
40use crate::core::widget;
41use crate::core::widget::tree::{self, Tree};
42use crate::core::window;
43use crate::core::{
44    Background, Border, Clipboard, Color, Element, Event, Layout, Length, Pixels, Rectangle, Shell,
45    Size, Theme, Widget,
46};
47
48/// A toggler widget.
49///
50/// # Example
51/// ```no_run
52/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
53/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
54/// #
55/// use iced::widget::toggler;
56///
57/// struct State {
58///    is_checked: bool,
59/// }
60///
61/// enum Message {
62///     TogglerToggled(bool),
63/// }
64///
65/// fn view(state: &State) -> Element<'_, Message> {
66///     toggler(state.is_checked)
67///         .label("Toggle me!")
68///         .on_toggle(Message::TogglerToggled)
69///         .into()
70/// }
71///
72/// fn update(state: &mut State, message: Message) {
73///     match message {
74///         Message::TogglerToggled(is_checked) => {
75///             state.is_checked = is_checked;
76///         }
77///     }
78/// }
79/// ```
80pub struct Toggler<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer>
81where
82    Theme: Catalog,
83    Renderer: text::Renderer,
84{
85    is_toggled: bool,
86    on_toggle: Option<Box<dyn Fn(bool) -> Message + 'a>>,
87    label: Option<text::Fragment<'a>>,
88    width: Length,
89    size: f32,
90    text_size: Option<Pixels>,
91    text_line_height: text::LineHeight,
92    text_alignment: text::Alignment,
93    text_shaping: text::Shaping,
94    text_wrapping: text::Wrapping,
95    spacing: f32,
96    font: Option<Renderer::Font>,
97    class: Theme::Class<'a>,
98    last_status: Option<Status>,
99}
100
101impl<'a, Message, Theme, Renderer> Toggler<'a, Message, Theme, Renderer>
102where
103    Theme: Catalog,
104    Renderer: text::Renderer,
105{
106    /// The default size of a [`Toggler`].
107    pub const DEFAULT_SIZE: f32 = 16.0;
108
109    /// Creates a new [`Toggler`].
110    ///
111    /// It expects:
112    ///   * a boolean describing whether the [`Toggler`] is checked or not
113    ///   * An optional label for the [`Toggler`]
114    ///   * a function that will be called when the [`Toggler`] is toggled. It
115    ///     will receive the new state of the [`Toggler`] and must produce a
116    ///     `Message`.
117    pub fn new(is_toggled: bool) -> Self {
118        Toggler {
119            is_toggled,
120            on_toggle: None,
121            label: None,
122            width: Length::Shrink,
123            size: Self::DEFAULT_SIZE,
124            text_size: None,
125            text_line_height: text::LineHeight::default(),
126            text_alignment: text::Alignment::Default,
127            text_shaping: text::Shaping::default(),
128            text_wrapping: text::Wrapping::default(),
129            spacing: Self::DEFAULT_SIZE / 2.0,
130            font: None,
131            class: Theme::default(),
132            last_status: None,
133        }
134    }
135
136    /// Sets the label of the [`Toggler`].
137    pub fn label(mut self, label: impl text::IntoFragment<'a>) -> Self {
138        self.label = Some(label.into_fragment());
139        self
140    }
141
142    /// Sets the message that should be produced when a user toggles
143    /// the [`Toggler`].
144    ///
145    /// If this method is not called, the [`Toggler`] will be disabled.
146    pub fn on_toggle(mut self, on_toggle: impl Fn(bool) -> Message + 'a) -> Self {
147        self.on_toggle = Some(Box::new(on_toggle));
148        self
149    }
150
151    /// Sets the message that should be produced when a user toggles
152    /// the [`Toggler`], if `Some`.
153    ///
154    /// If `None`, the [`Toggler`] will be disabled.
155    pub fn on_toggle_maybe(mut self, on_toggle: Option<impl Fn(bool) -> Message + 'a>) -> Self {
156        self.on_toggle = on_toggle.map(|on_toggle| Box::new(on_toggle) as _);
157        self
158    }
159
160    /// Sets the size of the [`Toggler`].
161    pub fn size(mut self, size: impl Into<Pixels>) -> Self {
162        self.size = size.into().0;
163        self
164    }
165
166    /// Sets the width of the [`Toggler`].
167    pub fn width(mut self, width: impl Into<Length>) -> Self {
168        self.width = width.into();
169        self
170    }
171
172    /// Sets the text size o the [`Toggler`].
173    pub fn text_size(mut self, text_size: impl Into<Pixels>) -> Self {
174        self.text_size = Some(text_size.into());
175        self
176    }
177
178    /// Sets the text [`text::LineHeight`] of the [`Toggler`].
179    pub fn text_line_height(mut self, line_height: impl Into<text::LineHeight>) -> Self {
180        self.text_line_height = line_height.into();
181        self
182    }
183
184    /// Sets the horizontal alignment of the text of the [`Toggler`]
185    pub fn text_alignment(mut self, alignment: impl Into<text::Alignment>) -> Self {
186        self.text_alignment = alignment.into();
187        self
188    }
189
190    /// Sets the [`text::Shaping`] strategy of the [`Toggler`].
191    pub fn text_shaping(mut self, shaping: text::Shaping) -> Self {
192        self.text_shaping = shaping;
193        self
194    }
195
196    /// Sets the [`text::Wrapping`] strategy of the [`Toggler`].
197    pub fn text_wrapping(mut self, wrapping: text::Wrapping) -> Self {
198        self.text_wrapping = wrapping;
199        self
200    }
201
202    /// Sets the spacing between the [`Toggler`] and the text.
203    pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self {
204        self.spacing = spacing.into().0;
205        self
206    }
207
208    /// Sets the [`Renderer::Font`] of the text of the [`Toggler`]
209    ///
210    /// [`Renderer::Font`]: crate::core::text::Renderer
211    pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
212        self.font = Some(font.into());
213        self
214    }
215
216    /// Sets the style of the [`Toggler`].
217    #[must_use]
218    pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
219    where
220        Theme::Class<'a>: From<StyleFn<'a, Theme>>,
221    {
222        self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
223        self
224    }
225
226    /// Sets the style class of the [`Toggler`].
227    #[cfg(feature = "advanced")]
228    #[must_use]
229    pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
230        self.class = class.into();
231        self
232    }
233}
234
235impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
236    for Toggler<'_, Message, Theme, Renderer>
237where
238    Theme: Catalog,
239    Renderer: text::Renderer,
240{
241    fn tag(&self) -> tree::Tag {
242        tree::Tag::of::<widget::text::State<Renderer::Paragraph>>()
243    }
244
245    fn state(&self) -> tree::State {
246        tree::State::new(widget::text::State::<Renderer::Paragraph>::default())
247    }
248
249    fn size(&self) -> Size<Length> {
250        Size {
251            width: self.width,
252            height: Length::Shrink,
253        }
254    }
255
256    fn layout(
257        &mut self,
258        tree: &mut Tree,
259        renderer: &Renderer,
260        limits: &layout::Limits,
261    ) -> layout::Node {
262        let limits = limits.width(self.width);
263
264        layout::next_to_each_other(
265            &limits,
266            if self.label.is_some() {
267                self.spacing
268            } else {
269                0.0
270            },
271            |_| layout::Node::new(Size::new(2.0 * self.size, self.size)),
272            |limits| {
273                if let Some(label) = self.label.as_deref() {
274                    let state = tree
275                        .state
276                        .downcast_mut::<widget::text::State<Renderer::Paragraph>>();
277
278                    widget::text::layout(
279                        state,
280                        renderer,
281                        limits,
282                        label,
283                        widget::text::Format {
284                            width: self.width,
285                            height: Length::Shrink,
286                            line_height: self.text_line_height,
287                            size: self.text_size,
288                            font: self.font,
289                            align_x: self.text_alignment,
290                            align_y: alignment::Vertical::Top,
291                            shaping: self.text_shaping,
292                            wrapping: self.text_wrapping,
293                        },
294                    )
295                } else {
296                    layout::Node::new(Size::ZERO)
297                }
298            },
299        )
300    }
301
302    fn update(
303        &mut self,
304        _tree: &mut Tree,
305        event: &Event,
306        layout: Layout<'_>,
307        cursor: mouse::Cursor,
308        _renderer: &Renderer,
309        _clipboard: &mut dyn Clipboard,
310        shell: &mut Shell<'_, Message>,
311        _viewport: &Rectangle,
312    ) {
313        let Some(on_toggle) = &self.on_toggle else {
314            return;
315        };
316
317        match event {
318            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
319            | Event::Touch(touch::Event::FingerPressed { .. }) => {
320                let mouse_over = cursor.is_over(layout.bounds());
321
322                if mouse_over {
323                    shell.publish(on_toggle(!self.is_toggled));
324                    shell.capture_event();
325                }
326            }
327            _ => {}
328        }
329
330        let current_status = if self.on_toggle.is_none() {
331            Status::Disabled {
332                is_toggled: self.is_toggled,
333            }
334        } else if cursor.is_over(layout.bounds()) {
335            Status::Hovered {
336                is_toggled: self.is_toggled,
337            }
338        } else {
339            Status::Active {
340                is_toggled: self.is_toggled,
341            }
342        };
343
344        if let Event::Window(window::Event::RedrawRequested(_now)) = event {
345            self.last_status = Some(current_status);
346        } else if self
347            .last_status
348            .is_some_and(|status| status != current_status)
349        {
350            shell.request_redraw();
351        }
352    }
353
354    fn mouse_interaction(
355        &self,
356        _tree: &Tree,
357        layout: Layout<'_>,
358        cursor: mouse::Cursor,
359        _viewport: &Rectangle,
360        _renderer: &Renderer,
361    ) -> mouse::Interaction {
362        if cursor.is_over(layout.bounds()) {
363            if self.on_toggle.is_some() {
364                mouse::Interaction::Pointer
365            } else {
366                mouse::Interaction::NotAllowed
367            }
368        } else {
369            mouse::Interaction::default()
370        }
371    }
372
373    fn draw(
374        &self,
375        tree: &Tree,
376        renderer: &mut Renderer,
377        theme: &Theme,
378        defaults: &renderer::Style,
379        layout: Layout<'_>,
380        _cursor: mouse::Cursor,
381        viewport: &Rectangle,
382    ) {
383        let mut children = layout.children();
384        let toggler_layout = children.next().unwrap();
385
386        let style = theme.style(
387            &self.class,
388            self.last_status.unwrap_or(Status::Disabled {
389                is_toggled: self.is_toggled,
390            }),
391        );
392
393        if self.label.is_some() {
394            let label_layout = children.next().unwrap();
395            let state: &widget::text::State<Renderer::Paragraph> = tree.state.downcast_ref();
396
397            crate::text::draw(
398                renderer,
399                defaults,
400                label_layout.bounds(),
401                state.raw(),
402                crate::text::Style {
403                    color: style.text_color,
404                },
405                viewport,
406            );
407        }
408
409        let bounds = toggler_layout.bounds();
410        let border_radius = style
411            .border_radius
412            .unwrap_or_else(|| border::Radius::new(bounds.height / 2.0));
413
414        renderer.fill_quad(
415            renderer::Quad {
416                bounds,
417                border: Border {
418                    radius: border_radius,
419                    width: style.background_border_width,
420                    color: style.background_border_color,
421                },
422                ..renderer::Quad::default()
423            },
424            style.background,
425        );
426
427        let padding = (style.padding_ratio * bounds.height).round();
428        let toggler_foreground_bounds = Rectangle {
429            x: bounds.x
430                + if self.is_toggled {
431                    bounds.width - bounds.height + padding
432                } else {
433                    padding
434                },
435            y: bounds.y + padding,
436            width: bounds.height - (2.0 * padding),
437            height: bounds.height - (2.0 * padding),
438        };
439
440        renderer.fill_quad(
441            renderer::Quad {
442                bounds: toggler_foreground_bounds,
443                border: Border {
444                    radius: border_radius,
445                    width: style.foreground_border_width,
446                    color: style.foreground_border_color,
447                },
448                ..renderer::Quad::default()
449            },
450            style.foreground,
451        );
452    }
453}
454
455impl<'a, Message, Theme, Renderer> From<Toggler<'a, Message, Theme, Renderer>>
456    for Element<'a, Message, Theme, Renderer>
457where
458    Message: 'a,
459    Theme: Catalog + 'a,
460    Renderer: text::Renderer + 'a,
461{
462    fn from(
463        toggler: Toggler<'a, Message, Theme, Renderer>,
464    ) -> Element<'a, Message, Theme, Renderer> {
465        Element::new(toggler)
466    }
467}
468
469/// The possible status of a [`Toggler`].
470#[derive(Debug, Clone, Copy, PartialEq, Eq)]
471pub enum Status {
472    /// The [`Toggler`] can be interacted with.
473    Active {
474        /// Indicates whether the [`Toggler`] is toggled.
475        is_toggled: bool,
476    },
477    /// The [`Toggler`] is being hovered.
478    Hovered {
479        /// Indicates whether the [`Toggler`] is toggled.
480        is_toggled: bool,
481    },
482    /// The [`Toggler`] is disabled.
483    Disabled {
484        /// Indicates whether the [`Toggler`] is toggled.
485        is_toggled: bool,
486    },
487}
488
489/// The appearance of a toggler.
490#[derive(Debug, Clone, Copy, PartialEq)]
491pub struct Style {
492    /// The background [`Color`] of the toggler.
493    pub background: Background,
494    /// The width of the background border of the toggler.
495    pub background_border_width: f32,
496    /// The [`Color`] of the background border of the toggler.
497    pub background_border_color: Color,
498    /// The foreground [`Color`] of the toggler.
499    pub foreground: Background,
500    /// The width of the foreground border of the toggler.
501    pub foreground_border_width: f32,
502    /// The [`Color`] of the foreground border of the toggler.
503    pub foreground_border_color: Color,
504    /// The text [`Color`] of the toggler.
505    pub text_color: Option<Color>,
506    /// The border radius of the toggler.
507    ///
508    /// If `None`, the toggler will be perfectly round.
509    pub border_radius: Option<border::Radius>,
510    /// The ratio of separation between the background and the toggle in relative height.
511    pub padding_ratio: f32,
512}
513
514/// The theme catalog of a [`Toggler`].
515pub trait Catalog: Sized {
516    /// The item class of the [`Catalog`].
517    type Class<'a>;
518
519    /// The default class produced by the [`Catalog`].
520    fn default<'a>() -> Self::Class<'a>;
521
522    /// The [`Style`] of a class with the given status.
523    fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
524}
525
526/// A styling function for a [`Toggler`].
527///
528/// This is just a boxed closure: `Fn(&Theme, Status) -> Style`.
529pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
530
531impl Catalog for Theme {
532    type Class<'a> = StyleFn<'a, Self>;
533
534    fn default<'a>() -> Self::Class<'a> {
535        Box::new(default)
536    }
537
538    fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
539        class(self, status)
540    }
541}
542
543/// The default style of a [`Toggler`].
544pub fn default(theme: &Theme, status: Status) -> Style {
545    let palette = theme.extended_palette();
546
547    let background = match status {
548        Status::Active { is_toggled } | Status::Hovered { is_toggled } => {
549            if is_toggled {
550                palette.primary.base.color
551            } else {
552                palette.background.strong.color
553            }
554        }
555        Status::Disabled { is_toggled } => {
556            if is_toggled {
557                palette.background.strong.color
558            } else {
559                palette.background.weak.color
560            }
561        }
562    };
563
564    let foreground = match status {
565        Status::Active { is_toggled } => {
566            if is_toggled {
567                palette.primary.base.text
568            } else {
569                palette.background.base.color
570            }
571        }
572        Status::Hovered { is_toggled } => {
573            if is_toggled {
574                Color {
575                    a: 0.5,
576                    ..palette.primary.base.text
577                }
578            } else {
579                palette.background.weak.color
580            }
581        }
582        Status::Disabled { .. } => palette.background.weakest.color,
583    };
584
585    Style {
586        background: background.into(),
587        foreground: foreground.into(),
588        foreground_border_width: 0.0,
589        foreground_border_color: Color::TRANSPARENT,
590        background_border_width: 0.0,
591        background_border_color: Color::TRANSPARENT,
592        text_color: None,
593        border_radius: None,
594        padding_ratio: 0.1,
595    }
596}