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            |_| {
272                let size = if renderer::CRISP {
273                    let scale_factor = renderer.scale_factor().unwrap_or(1.0);
274
275                    (self.size * scale_factor).round() / scale_factor
276                } else {
277                    self.size
278                };
279
280                layout::Node::new(Size::new(2.0 * size, size))
281            },
282            |limits| {
283                if let Some(label) = self.label.as_deref() {
284                    let state = tree
285                        .state
286                        .downcast_mut::<widget::text::State<Renderer::Paragraph>>();
287
288                    widget::text::layout(
289                        state,
290                        renderer,
291                        limits,
292                        label,
293                        widget::text::Format {
294                            width: self.width,
295                            height: Length::Shrink,
296                            line_height: self.text_line_height,
297                            size: self.text_size,
298                            font: self.font,
299                            align_x: self.text_alignment,
300                            align_y: alignment::Vertical::Top,
301                            shaping: self.text_shaping,
302                            wrapping: self.text_wrapping,
303                        },
304                    )
305                } else {
306                    layout::Node::new(Size::ZERO)
307                }
308            },
309        )
310    }
311
312    fn update(
313        &mut self,
314        _tree: &mut Tree,
315        event: &Event,
316        layout: Layout<'_>,
317        cursor: mouse::Cursor,
318        _renderer: &Renderer,
319        _clipboard: &mut dyn Clipboard,
320        shell: &mut Shell<'_, Message>,
321        _viewport: &Rectangle,
322    ) {
323        let Some(on_toggle) = &self.on_toggle else {
324            return;
325        };
326
327        match event {
328            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
329            | Event::Touch(touch::Event::FingerPressed { .. }) => {
330                let mouse_over = cursor.is_over(layout.bounds());
331
332                if mouse_over {
333                    shell.publish(on_toggle(!self.is_toggled));
334                    shell.capture_event();
335                }
336            }
337            _ => {}
338        }
339
340        let current_status = if self.on_toggle.is_none() {
341            Status::Disabled {
342                is_toggled: self.is_toggled,
343            }
344        } else if cursor.is_over(layout.bounds()) {
345            Status::Hovered {
346                is_toggled: self.is_toggled,
347            }
348        } else {
349            Status::Active {
350                is_toggled: self.is_toggled,
351            }
352        };
353
354        if let Event::Window(window::Event::RedrawRequested(_now)) = event {
355            self.last_status = Some(current_status);
356        } else if self
357            .last_status
358            .is_some_and(|status| status != current_status)
359        {
360            shell.request_redraw();
361        }
362    }
363
364    fn mouse_interaction(
365        &self,
366        _tree: &Tree,
367        layout: Layout<'_>,
368        cursor: mouse::Cursor,
369        _viewport: &Rectangle,
370        _renderer: &Renderer,
371    ) -> mouse::Interaction {
372        if cursor.is_over(layout.bounds()) {
373            if self.on_toggle.is_some() {
374                mouse::Interaction::Pointer
375            } else {
376                mouse::Interaction::NotAllowed
377            }
378        } else {
379            mouse::Interaction::default()
380        }
381    }
382
383    fn draw(
384        &self,
385        tree: &Tree,
386        renderer: &mut Renderer,
387        theme: &Theme,
388        defaults: &renderer::Style,
389        layout: Layout<'_>,
390        _cursor: mouse::Cursor,
391        viewport: &Rectangle,
392    ) {
393        let mut children = layout.children();
394        let toggler_layout = children.next().unwrap();
395
396        let style = theme.style(
397            &self.class,
398            self.last_status.unwrap_or(Status::Disabled {
399                is_toggled: self.is_toggled,
400            }),
401        );
402
403        if self.label.is_some() {
404            let label_layout = children.next().unwrap();
405            let state: &widget::text::State<Renderer::Paragraph> = tree.state.downcast_ref();
406
407            crate::text::draw(
408                renderer,
409                defaults,
410                label_layout.bounds(),
411                state.raw(),
412                crate::text::Style {
413                    color: style.text_color,
414                },
415                viewport,
416            );
417        }
418
419        let scale_factor = renderer.scale_factor().unwrap_or(1.0);
420        let bounds = toggler_layout.bounds();
421
422        let border_radius = style
423            .border_radius
424            .unwrap_or_else(|| border::Radius::new(bounds.height / 2.0));
425
426        renderer.fill_quad(
427            renderer::Quad {
428                bounds,
429                border: Border {
430                    radius: border_radius,
431                    width: style.background_border_width,
432                    color: style.background_border_color,
433                },
434                ..renderer::Quad::default()
435            },
436            style.background,
437        );
438
439        let toggle_bounds = {
440            // Try to align toggle to the pixel grid
441            let bounds = if renderer::CRISP {
442                (bounds * scale_factor).round()
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            } * (1.0 / scale_factor)
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
477impl<'a, Message, Theme, Renderer> From<Toggler<'a, Message, Theme, Renderer>>
478    for Element<'a, Message, Theme, Renderer>
479where
480    Message: 'a,
481    Theme: Catalog + 'a,
482    Renderer: text::Renderer + 'a,
483{
484    fn from(
485        toggler: Toggler<'a, Message, Theme, Renderer>,
486    ) -> Element<'a, Message, Theme, Renderer> {
487        Element::new(toggler)
488    }
489}
490
491/// The possible status of a [`Toggler`].
492#[derive(Debug, Clone, Copy, PartialEq, Eq)]
493pub enum Status {
494    /// The [`Toggler`] can be interacted with.
495    Active {
496        /// Indicates whether the [`Toggler`] is toggled.
497        is_toggled: bool,
498    },
499    /// The [`Toggler`] is being hovered.
500    Hovered {
501        /// Indicates whether the [`Toggler`] is toggled.
502        is_toggled: bool,
503    },
504    /// The [`Toggler`] is disabled.
505    Disabled {
506        /// Indicates whether the [`Toggler`] is toggled.
507        is_toggled: bool,
508    },
509}
510
511/// The appearance of a toggler.
512#[derive(Debug, Clone, Copy, PartialEq)]
513pub struct Style {
514    /// The background [`Color`] of the toggler.
515    pub background: Background,
516    /// The width of the background border of the toggler.
517    pub background_border_width: f32,
518    /// The [`Color`] of the background border of the toggler.
519    pub background_border_color: Color,
520    /// The foreground [`Color`] of the toggler.
521    pub foreground: Background,
522    /// The width of the foreground border of the toggler.
523    pub foreground_border_width: f32,
524    /// The [`Color`] of the foreground border of the toggler.
525    pub foreground_border_color: Color,
526    /// The text [`Color`] of the toggler.
527    pub text_color: Option<Color>,
528    /// The border radius of the toggler.
529    ///
530    /// If `None`, the toggler will be perfectly round.
531    pub border_radius: Option<border::Radius>,
532    /// The ratio of separation between the background and the toggle in relative height.
533    pub padding_ratio: f32,
534}
535
536/// The theme catalog of a [`Toggler`].
537pub trait Catalog: Sized {
538    /// The item class of the [`Catalog`].
539    type Class<'a>;
540
541    /// The default class produced by the [`Catalog`].
542    fn default<'a>() -> Self::Class<'a>;
543
544    /// The [`Style`] of a class with the given status.
545    fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
546}
547
548/// A styling function for a [`Toggler`].
549///
550/// This is just a boxed closure: `Fn(&Theme, Status) -> Style`.
551pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
552
553impl Catalog for Theme {
554    type Class<'a> = StyleFn<'a, Self>;
555
556    fn default<'a>() -> Self::Class<'a> {
557        Box::new(default)
558    }
559
560    fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
561        class(self, status)
562    }
563}
564
565/// The default style of a [`Toggler`].
566pub fn default(theme: &Theme, status: Status) -> Style {
567    let palette = theme.extended_palette();
568
569    let background = match status {
570        Status::Active { is_toggled } | Status::Hovered { is_toggled } => {
571            if is_toggled {
572                palette.primary.base.color
573            } else {
574                palette.background.strong.color
575            }
576        }
577        Status::Disabled { is_toggled } => {
578            if is_toggled {
579                palette.background.strong.color
580            } else {
581                palette.background.weak.color
582            }
583        }
584    };
585
586    let foreground = match status {
587        Status::Active { is_toggled } => {
588            if is_toggled {
589                palette.primary.base.text
590            } else {
591                palette.background.base.color
592            }
593        }
594        Status::Hovered { is_toggled } => {
595            if is_toggled {
596                Color {
597                    a: 0.5,
598                    ..palette.primary.base.text
599                }
600            } else {
601                palette.background.weak.color
602            }
603        }
604        Status::Disabled { .. } => palette.background.weakest.color,
605    };
606
607    Style {
608        background: background.into(),
609        foreground: foreground.into(),
610        foreground_border_width: 0.0,
611        foreground_border_color: Color::TRANSPARENT,
612        background_border_width: 0.0,
613        background_border_color: Color::TRANSPARENT,
614        text_color: None,
615        border_radius: None,
616        padding_ratio: 0.1,
617    }
618}