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        style: &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        if self.label.is_some() {
404            let label_layout = children.next().unwrap();
405            let state: &widget::text::State<Renderer::Paragraph> =
406                tree.state.downcast_ref();
407
408            crate::text::draw(
409                renderer,
410                style,
411                label_layout.bounds(),
412                state.raw(),
413                crate::text::Style::default(),
414                viewport,
415            );
416        }
417
418        let bounds = toggler_layout.bounds();
419        let style = theme
420            .style(&self.class, self.last_status.unwrap_or(Status::Disabled));
421
422        let border_radius = bounds.height / 2.0;
423        let space = (SPACE_RATIO * bounds.height).round();
424
425        let toggler_background_bounds = Rectangle {
426            x: bounds.x + space,
427            y: bounds.y + space,
428            width: bounds.width - (2.0 * space),
429            height: bounds.height - (2.0 * space),
430        };
431
432        renderer.fill_quad(
433            renderer::Quad {
434                bounds: toggler_background_bounds,
435                border: Border {
436                    radius: border_radius.into(),
437                    width: style.background_border_width,
438                    color: style.background_border_color,
439                },
440                ..renderer::Quad::default()
441            },
442            style.background,
443        );
444
445        let toggler_foreground_bounds = Rectangle {
446            x: bounds.x
447                + if self.is_toggled {
448                    bounds.width - 2.0 * space - (bounds.height - (4.0 * space))
449                } else {
450                    2.0 * space
451                },
452            y: bounds.y + (2.0 * space),
453            width: bounds.height - (4.0 * space),
454            height: bounds.height - (4.0 * space),
455        };
456
457        renderer.fill_quad(
458            renderer::Quad {
459                bounds: toggler_foreground_bounds,
460                border: Border {
461                    radius: border_radius.into(),
462                    width: style.foreground_border_width,
463                    color: style.foreground_border_color,
464                },
465                ..renderer::Quad::default()
466            },
467            style.foreground,
468        );
469    }
470}
471
472impl<'a, Message, Theme, Renderer> From<Toggler<'a, Message, Theme, Renderer>>
473    for Element<'a, Message, Theme, Renderer>
474where
475    Message: 'a,
476    Theme: Catalog + 'a,
477    Renderer: text::Renderer + 'a,
478{
479    fn from(
480        toggler: Toggler<'a, Message, Theme, Renderer>,
481    ) -> 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}
502
503/// The appearance of a toggler.
504#[derive(Debug, Clone, Copy, PartialEq)]
505pub struct Style {
506    /// The background [`Color`] of the toggler.
507    pub background: Color,
508    /// The width of the background border of the toggler.
509    pub background_border_width: f32,
510    /// The [`Color`] of the background border of the toggler.
511    pub background_border_color: Color,
512    /// The foreground [`Color`] of the toggler.
513    pub foreground: Color,
514    /// The width of the foreground border of the toggler.
515    pub foreground_border_width: f32,
516    /// The [`Color`] of the foreground border of the toggler.
517    pub foreground_border_color: Color,
518}
519
520/// The theme catalog of a [`Toggler`].
521pub trait Catalog: Sized {
522    /// The item class of the [`Catalog`].
523    type Class<'a>;
524
525    /// The default class produced by the [`Catalog`].
526    fn default<'a>() -> Self::Class<'a>;
527
528    /// The [`Style`] of a class with the given status.
529    fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
530}
531
532/// A styling function for a [`Toggler`].
533///
534/// This is just a boxed closure: `Fn(&Theme, Status) -> Style`.
535pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
536
537impl Catalog for Theme {
538    type Class<'a> = StyleFn<'a, Self>;
539
540    fn default<'a>() -> Self::Class<'a> {
541        Box::new(default)
542    }
543
544    fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
545        class(self, status)
546    }
547}
548
549/// The default style of a [`Toggler`].
550pub fn default(theme: &Theme, status: Status) -> Style {
551    let palette = theme.extended_palette();
552
553    let background = match status {
554        Status::Active { is_toggled } | Status::Hovered { is_toggled } => {
555            if is_toggled {
556                palette.primary.base.color
557            } else {
558                palette.background.strong.color
559            }
560        }
561        Status::Disabled => palette.background.weak.color,
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,
587        foreground,
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    }
593}