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