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