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