Skip to main content

material_ui_rs/widget/component/
toggler.rs

1//! Material 3 switch/toggler constructors with token-backed size and motion defaults.
2
3use super::*;
4
5type StyleFn<'a> = Box<dyn Fn(&Theme, iced_toggler::Status) -> iced_toggler::Style + 'a>;
6
7const SWITCH_ON_ICON_SVG: &[u8] = br##"
8<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
9  <path d="M9.55 18.2 3.65 12.3 5.275 10.675 9.55 14.95 18.725 5.775 20.35 7.4Z"/>
10</svg>
11"##;
12
13const SWITCH_OFF_ICON_SVG: &[u8] = br##"
14<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
15  <path d="M6.4 19.2 4.8 17.6 10.4 12 4.8 6.4 6.4 4.8 12 10.4 17.6 4.8 19.2 6.4 13.6 12 19.2 17.6 17.6 19.2 12 13.6Z"/>
16</svg>
17"##;
18
19/// A Material 3 switch with animated handle motion and color transitions.
20pub struct Toggler<'a, Message, Renderer = iced_widget::Renderer>
21where
22    Renderer: core_text::Renderer,
23{
24    is_toggled: bool,
25    on_toggle: Option<Box<dyn Fn(bool) -> Message + 'a>>,
26    on_toggle_with_origin: Option<Box<dyn Fn(bool, Point) -> Message + 'a>>,
27    label: Option<text::Fragment<'a>>,
28    width: Length,
29    track_height: f32,
30    spacing: f32,
31    text_size: Option<Pixels>,
32    text_line_height: LineHeight,
33    text_alignment: text::Alignment,
34    text_shaping: text::Shaping,
35    text_wrapping: text::Wrapping,
36    font: Option<Renderer::Font>,
37    icons: bool,
38    show_only_selected_icon: bool,
39    style: StyleFn<'a>,
40}
41
42impl<Message, Renderer> std::fmt::Debug for Toggler<'_, Message, Renderer>
43where
44    Renderer: core_text::Renderer,
45{
46    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47        f.debug_struct("Toggler")
48            .field("is_toggled", &self.is_toggled)
49            .field("has_on_toggle", &self.on_toggle.is_some())
50            .field(
51                "has_on_toggle_with_origin",
52                &self.on_toggle_with_origin.is_some(),
53            )
54            .field("has_label", &self.label.is_some())
55            .field("width", &self.width)
56            .field("track_height", &self.track_height)
57            .field("spacing", &self.spacing)
58            .field("text_size", &self.text_size)
59            .field("text_line_height", &self.text_line_height)
60            .field("text_alignment", &self.text_alignment)
61            .field("icons", &self.icons)
62            .field("show_only_selected_icon", &self.show_only_selected_icon)
63            .finish_non_exhaustive()
64    }
65}
66
67impl<'a, Message, Renderer> Toggler<'a, Message, Renderer>
68where
69    Renderer: core_text::Renderer,
70{
71    pub fn new(is_toggled: bool) -> Self {
72        Self {
73            is_toggled,
74            on_toggle: None,
75            on_toggle_with_origin: None,
76            label: None,
77            width: Length::Shrink,
78            track_height: tokens::component::switch::TRACK_HEIGHT,
79            spacing: f32::from(tokens::component::divider::LIST_ITEM_LEADING_SPACE),
80            text_size: Some(Pixels(tokens::component::switch::LABEL_TEXT_SIZE)),
81            text_line_height: absolute_line_height(
82                tokens::component::switch::LABEL_TEXT_LINE_HEIGHT,
83            ),
84            text_alignment: text::Alignment::Default,
85            text_shaping: text::Shaping::default(),
86            text_wrapping: text::Wrapping::default(),
87            font: None,
88            icons: false,
89            show_only_selected_icon: false,
90            style: Box::new(toggler_style::default),
91        }
92    }
93
94    pub fn label(mut self, label: impl text::IntoFragment<'a>) -> Self {
95        self.label = Some(label.into_fragment());
96        self
97    }
98
99    pub fn on_toggle(mut self, on_toggle: impl Fn(bool) -> Message + 'a) -> Self {
100        self.on_toggle = Some(Box::new(on_toggle));
101        self.on_toggle_with_origin = None;
102        self
103    }
104
105    pub fn on_toggle_with_origin(
106        mut self,
107        on_toggle: impl Fn(bool, Point) -> Message + 'a,
108    ) -> Self {
109        self.on_toggle = None;
110        self.on_toggle_with_origin = Some(Box::new(on_toggle));
111        self
112    }
113
114    pub fn on_toggle_maybe(mut self, on_toggle: Option<impl Fn(bool) -> Message + 'a>) -> Self {
115        self.on_toggle = on_toggle.map(|on_toggle| Box::new(on_toggle) as _);
116        self.on_toggle_with_origin = None;
117        self
118    }
119
120    pub fn size(mut self, size: impl Into<Pixels>) -> Self {
121        self.track_height = size.into().0;
122        self
123    }
124
125    pub fn width(mut self, width: impl Into<Length>) -> Self {
126        self.width = width.into();
127        self
128    }
129
130    pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self {
131        self.spacing = spacing.into().0;
132        self
133    }
134
135    pub fn text_size(mut self, text_size: impl Into<Pixels>) -> Self {
136        self.text_size = Some(text_size.into());
137        self
138    }
139
140    pub fn text_line_height(mut self, line_height: impl Into<LineHeight>) -> Self {
141        self.text_line_height = line_height.into();
142        self
143    }
144
145    pub fn text_alignment(mut self, alignment: impl Into<text::Alignment>) -> Self {
146        self.text_alignment = alignment.into();
147        self
148    }
149
150    pub fn text_shaping(mut self, shaping: text::Shaping) -> Self {
151        self.text_shaping = shaping;
152        self
153    }
154
155    pub fn text_wrapping(mut self, wrapping: text::Wrapping) -> Self {
156        self.text_wrapping = wrapping;
157        self
158    }
159
160    pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
161        self.font = Some(font.into());
162        self
163    }
164
165    pub fn icons(mut self, icons: bool) -> Self {
166        self.icons = icons;
167        self
168    }
169
170    pub fn show_only_selected_icon(mut self, show_only_selected_icon: bool) -> Self {
171        self.show_only_selected_icon = show_only_selected_icon;
172        self
173    }
174
175    pub fn style(
176        mut self,
177        style: impl Fn(&Theme, iced_toggler::Status) -> iced_toggler::Style + 'a,
178    ) -> Self {
179        self.style = Box::new(style);
180        self
181    }
182}
183
184impl<Message, Renderer> Toggler<'_, Message, Renderer>
185where
186    Renderer: core_text::Renderer,
187{
188    fn track_size(&self) -> Size {
189        let scale = self.track_height / tokens::component::switch::TRACK_HEIGHT;
190
191        Size::new(
192            tokens::component::switch::TRACK_WIDTH * scale,
193            self.track_height,
194        )
195    }
196
197    fn handle_size_for(&self, is_toggled: bool, is_pressed: bool) -> f32 {
198        if is_pressed {
199            tokens::component::switch::PRESSED_HANDLE_SIZE
200        } else if self.icons || (self.show_only_selected_icon && is_toggled) {
201            tokens::component::switch::WITH_ICON_HANDLE_SIZE
202        } else if is_toggled {
203            tokens::component::switch::SELECTED_HANDLE_SIZE
204        } else {
205            tokens::component::switch::UNSELECTED_HANDLE_SIZE
206        }
207    }
208
209    fn shows_icons(&self) -> bool {
210        self.icons || self.show_only_selected_icon
211    }
212
213    fn shows_off_icon(&self) -> bool {
214        self.icons && !self.show_only_selected_icon
215    }
216
217    fn current_status(&self, bounds: Rectangle, cursor: mouse::Cursor) -> iced_toggler::Status {
218        if !self.has_on_toggle() {
219            iced_toggler::Status::Disabled {
220                is_toggled: self.is_toggled,
221            }
222        } else if cursor.is_over(bounds) {
223            iced_toggler::Status::Hovered {
224                is_toggled: self.is_toggled,
225            }
226        } else {
227            iced_toggler::Status::Active {
228                is_toggled: self.is_toggled,
229            }
230        }
231    }
232
233    fn has_on_toggle(&self) -> bool {
234        self.on_toggle.is_some() || self.on_toggle_with_origin.is_some()
235    }
236
237    fn toggle_message(&self, is_toggled: bool, origin: Point) -> Option<Message> {
238        if let Some(on_toggle) = &self.on_toggle_with_origin {
239            Some((on_toggle)(is_toggled, origin))
240        } else {
241            self.on_toggle
242                .as_ref()
243                .map(|on_toggle| (on_toggle)(is_toggled))
244        }
245    }
246}
247
248fn toggler_event_origin(event: &Event, bounds: Rectangle, cursor: mouse::Cursor) -> Point {
249    cursor
250        .position()
251        .or(match event {
252            Event::Touch(
253                touch::Event::FingerPressed { position, .. }
254                | touch::Event::FingerLifted { position, .. },
255            ) => Some(*position),
256            _ => None,
257        })
258        .unwrap_or_else(|| bounds.center())
259}
260
261impl<Message, Renderer> Widget<Message, Theme, Renderer> for Toggler<'_, Message, Renderer>
262where
263    Renderer: core_text::Renderer + core_svg::Renderer,
264{
265    fn tag(&self) -> tree::Tag {
266        tree::Tag::of::<SelectionState<Renderer::Paragraph, iced_toggler::Status>>()
267    }
268
269    fn state(&self) -> tree::State {
270        let mut state =
271            SelectionState::<Renderer::Paragraph, iced_toggler::Status>::new(self.is_toggled);
272
273        state.size = AnimatedScalar::new(self.handle_size_for(self.is_toggled, false));
274
275        tree::State::new(state)
276    }
277
278    fn size(&self) -> Size<Length> {
279        Size {
280            width: self.width,
281            height: Length::Shrink,
282        }
283    }
284
285    fn layout(
286        &mut self,
287        tree: &mut Tree,
288        renderer: &Renderer,
289        limits: &layout::Limits,
290    ) -> layout::Node {
291        let track_size = self.track_size();
292
293        layout::next_to_each_other(
294            &limits.width(self.width),
295            if self.label.is_some() {
296                self.spacing
297            } else {
298                0.0
299            },
300            |_| layout::Node::new(track_size),
301            |limits| {
302                if let Some(label) = self.label.as_deref() {
303                    let state = tree
304                        .state
305                        .downcast_mut::<SelectionState<Renderer::Paragraph, iced_toggler::Status>>(
306                        );
307
308                    core_widget::text::layout(
309                        &mut state.text,
310                        renderer,
311                        limits,
312                        label,
313                        core_widget::text::Format {
314                            width: self.width,
315                            height: Length::Shrink,
316                            line_height: self.text_line_height,
317                            size: self.text_size,
318                            font: self.font,
319                            align_x: self.text_alignment,
320                            align_y: alignment::Vertical::Top,
321                            shaping: self.text_shaping,
322                            wrapping: self.text_wrapping,
323                        },
324                    )
325                } else {
326                    layout::Node::new(Size::ZERO)
327                }
328            },
329        )
330    }
331
332    fn update(
333        &mut self,
334        tree: &mut Tree,
335        event: &Event,
336        layout: Layout<'_>,
337        cursor: mouse::Cursor,
338        _renderer: &Renderer,
339        _clipboard: &mut dyn Clipboard,
340        shell: &mut Shell<'_, Message>,
341        _viewport: &Rectangle,
342    ) {
343        let state = tree
344            .state
345            .downcast_mut::<SelectionState<Renderer::Paragraph, iced_toggler::Status>>();
346        let hit_bounds =
347            selection_control_hit_bounds(layout, tokens::component::switch::STATE_LAYER_SIZE);
348
349        match event {
350            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
351            | Event::Touch(touch::Event::FingerPressed { .. })
352                if self.has_on_toggle() && press_is_over(event, hit_bounds, cursor) =>
353            {
354                state.is_pressed = true;
355                state.press_origin = Some(toggler_event_origin(event, layout.bounds(), cursor));
356                shell.capture_event();
357                shell.request_redraw();
358            }
359            Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
360            | Event::Touch(touch::Event::FingerLifted { .. })
361                if state.is_pressed =>
362            {
363                let is_released_over = release_is_over(event, hit_bounds, cursor);
364                let origin = state
365                    .press_origin
366                    .unwrap_or_else(|| toggler_event_origin(event, layout.bounds(), cursor));
367
368                state.is_pressed = false;
369                state.press_origin = None;
370
371                if is_released_over
372                    && let Some(message) = self.toggle_message(!self.is_toggled, origin)
373                {
374                    shell.publish(message);
375                }
376
377                shell.capture_event();
378                shell.request_redraw();
379            }
380            Event::Touch(touch::Event::FingerLost { .. }) if state.is_pressed => {
381                state.is_pressed = false;
382                state.press_origin = None;
383                shell.request_redraw();
384            }
385            _ => {}
386        }
387
388        let now = match event {
389            Event::Window(window::Event::RedrawRequested(now)) => Some(*now),
390            _ => None,
391        };
392
393        if state.target != self.is_toggled {
394            let now = now.unwrap_or_else(Instant::now);
395
396            state.target = self.is_toggled;
397            state.position.set_target(
398                bool_value(self.is_toggled),
399                now,
400                duration_ms(tokens::component::switch::HANDLE_POSITION_TRANSITION_DURATION_MS),
401                tokens::component::switch::HANDLE_POSITION_TRANSITION_EASING,
402            );
403            state.color.set_target(
404                bool_value(self.is_toggled),
405                now,
406                duration_ms(tokens::component::switch::TRACK_COLOR_TRANSITION_DURATION_MS),
407                tokens::motion::EASING_LINEAR,
408            );
409            state.size.set_target(
410                self.handle_size_for(self.is_toggled, state.is_pressed),
411                now,
412                if state.is_pressed {
413                    duration_ms(
414                        tokens::component::switch::PRESSED_HANDLE_SIZE_TRANSITION_DURATION_MS,
415                    )
416                } else {
417                    duration_ms(tokens::component::switch::HANDLE_SIZE_TRANSITION_DURATION_MS)
418                },
419                if state.is_pressed {
420                    tokens::motion::EASING_LINEAR
421                } else {
422                    tokens::motion::EASING_STANDARD
423                },
424            );
425            state.icon.set_target(
426                bool_value(self.is_toggled),
427                now,
428                duration_ms(tokens::component::switch::ICON_TRANSFORM_TRANSITION_DURATION_MS),
429                tokens::motion::EASING_STANDARD,
430            );
431            state.icon_opacity.set_target(
432                bool_value(self.is_toggled),
433                now,
434                duration_ms(tokens::component::switch::ICON_OPACITY_TRANSITION_DURATION_MS),
435                tokens::motion::EASING_LINEAR,
436            );
437            shell.request_redraw();
438        }
439
440        let target_handle_size = self.handle_size_for(self.is_toggled, state.is_pressed);
441
442        if (state.size.to - target_handle_size).abs() > f32::EPSILON {
443            let now = now.unwrap_or_else(Instant::now);
444
445            state.size.set_target(
446                target_handle_size,
447                now,
448                if state.is_pressed {
449                    duration_ms(
450                        tokens::component::switch::PRESSED_HANDLE_SIZE_TRANSITION_DURATION_MS,
451                    )
452                } else {
453                    duration_ms(tokens::component::switch::HANDLE_SIZE_TRANSITION_DURATION_MS)
454                },
455                if state.is_pressed {
456                    tokens::motion::EASING_LINEAR
457                } else {
458                    tokens::motion::EASING_STANDARD
459                },
460            );
461            shell.request_redraw();
462        }
463
464        let current_status = self.current_status(hit_bounds, cursor);
465
466        if let Some(now) = now {
467            if state.advance(now) {
468                shell.request_redraw();
469            }
470
471            state.last_status = Some(current_status);
472        } else if state
473            .last_status
474            .is_some_and(|status| status != current_status)
475            || state.is_animating()
476        {
477            shell.request_redraw();
478        }
479    }
480
481    fn mouse_interaction(
482        &self,
483        _tree: &Tree,
484        layout: Layout<'_>,
485        cursor: mouse::Cursor,
486        _viewport: &Rectangle,
487        _renderer: &Renderer,
488    ) -> mouse::Interaction {
489        let hit_bounds =
490            selection_control_hit_bounds(layout, tokens::component::switch::STATE_LAYER_SIZE);
491
492        if cursor.is_over(hit_bounds) {
493            if self.has_on_toggle() {
494                mouse::Interaction::Pointer
495            } else {
496                mouse::Interaction::NotAllowed
497            }
498        } else {
499            mouse::Interaction::default()
500        }
501    }
502
503    fn draw(
504        &self,
505        tree: &Tree,
506        renderer: &mut Renderer,
507        theme: &Theme,
508        defaults: &renderer::Style,
509        layout: Layout<'_>,
510        _cursor: mouse::Cursor,
511        viewport: &Rectangle,
512    ) {
513        let state = tree
514            .state
515            .downcast_ref::<SelectionState<Renderer::Paragraph, iced_toggler::Status>>();
516
517        let mut children = layout.children();
518        let toggler_layout = children.next().unwrap();
519        let bounds = toggler_layout.bounds();
520
521        let status = state.last_status.unwrap_or(iced_toggler::Status::Disabled {
522            is_toggled: self.is_toggled,
523        });
524        let unselected_status = match status {
525            iced_toggler::Status::Active { .. } => {
526                iced_toggler::Status::Active { is_toggled: false }
527            }
528            iced_toggler::Status::Hovered { .. } => {
529                iced_toggler::Status::Hovered { is_toggled: false }
530            }
531            iced_toggler::Status::Disabled { .. } => {
532                iced_toggler::Status::Disabled { is_toggled: false }
533            }
534        };
535        let selected_status = match status {
536            iced_toggler::Status::Active { .. } => {
537                iced_toggler::Status::Active { is_toggled: true }
538            }
539            iced_toggler::Status::Hovered { .. } => {
540                iced_toggler::Status::Hovered { is_toggled: true }
541            }
542            iced_toggler::Status::Disabled { .. } => {
543                iced_toggler::Status::Disabled { is_toggled: true }
544            }
545        };
546
547        let current_style = (self.style)(theme, status);
548        let unselected_style = (self.style)(theme, unselected_status);
549        let selected_style = (self.style)(theme, selected_status);
550
551        let color_progress = state.color.value.clamp(0.0, 1.0);
552        let scale = bounds.height / tokens::component::switch::TRACK_HEIGHT;
553        let colors = theme.colors();
554        let is_disabled = matches!(status, iced_toggler::Status::Disabled { .. });
555        let track_radius = current_style
556            .border_radius
557            .unwrap_or_else(|| border::Radius::new(bounds.height / 2.0));
558        let track_border_width = lerp(
559            unselected_style.background_border_width,
560            selected_style.background_border_width,
561            color_progress,
562        );
563        let track_border_color = mix(
564            unselected_style.background_border_color,
565            selected_style.background_border_color,
566            color_progress,
567        );
568        let track_background = mix(
569            solid_color(unselected_style.background),
570            solid_color(selected_style.background),
571            color_progress,
572        );
573
574        renderer.fill_quad(
575            renderer::Quad {
576                bounds,
577                border: Border {
578                    radius: track_radius,
579                    width: track_border_width,
580                    color: track_border_color,
581                },
582                ..renderer::Quad::default()
583            },
584            track_background,
585        );
586
587        let handle_size = state.size.value * scale;
588        let center_x = bounds.x
589            + scale
590                * (tokens::component::switch::TRACK_HEIGHT / 2.0
591                    + (tokens::component::switch::TRACK_WIDTH
592                        - tokens::component::switch::TRACK_HEIGHT)
593                        * state.position.value);
594        let center_y = bounds.center_y();
595        let handle_bounds = Rectangle {
596            x: center_x - handle_size / 2.0,
597            y: center_y - handle_size / 2.0,
598            width: handle_size,
599            height: handle_size,
600        };
601        let handle_background = mix(
602            solid_color(unselected_style.foreground),
603            solid_color(selected_style.foreground),
604            color_progress,
605        );
606        let handle_border_width = lerp(
607            unselected_style.foreground_border_width,
608            selected_style.foreground_border_width,
609            color_progress,
610        );
611        let handle_border_color = mix(
612            unselected_style.foreground_border_color,
613            selected_style.foreground_border_color,
614            color_progress,
615        );
616
617        let state_layer_opacity = if is_disabled {
618            0.0
619        } else if state.is_pressed {
620            tokens::state::PRESSED_STATE_LAYER_OPACITY
621        } else if matches!(status, iced_toggler::Status::Hovered { .. }) {
622            tokens::state::HOVER_STATE_LAYER_OPACITY
623        } else {
624            0.0
625        };
626
627        if state_layer_opacity > 0.0 {
628            let state_layer_size = tokens::component::switch::STATE_LAYER_SIZE * scale;
629            let state_layer_color = if self.is_toggled {
630                colors.primary.color
631            } else {
632                colors.surface.text
633            };
634
635            renderer.fill_quad(
636                renderer::Quad {
637                    bounds: Rectangle {
638                        x: center_x - state_layer_size / 2.0,
639                        y: center_y - state_layer_size / 2.0,
640                        width: state_layer_size,
641                        height: state_layer_size,
642                    },
643                    border: Border {
644                        radius: border::Radius::new(state_layer_size / 2.0),
645                        ..Border::default()
646                    },
647                    ..renderer::Quad::default()
648                },
649                alpha_color(state_layer_color, state_layer_opacity),
650            );
651        }
652
653        renderer.fill_quad(
654            renderer::Quad {
655                bounds: handle_bounds,
656                border: Border {
657                    radius: border::Radius::new(handle_size / 2.0),
658                    width: handle_border_width,
659                    color: handle_border_color,
660                },
661                ..renderer::Quad::default()
662            },
663            handle_background,
664        );
665
666        if self.shows_icons() {
667            let icon_progress = state.icon.value.clamp(0.0, 1.0);
668            let selected_icon_opacity = state.icon_opacity.value.clamp(0.0, 1.0);
669            let unselected_icon_opacity = if self.shows_off_icon() {
670                1.0 - selected_icon_opacity
671            } else {
672                0.0
673            };
674            let selected_icon_color = if is_disabled {
675                alpha_color(
676                    colors.surface.text,
677                    tokens::component::switch::DISABLED_SELECTED_ICON_OPACITY,
678                )
679            } else {
680                colors.primary.container_text
681            };
682            let unselected_icon_color = if is_disabled {
683                alpha_color(
684                    colors.surface.container.highest,
685                    tokens::component::switch::DISABLED_UNSELECTED_ICON_OPACITY,
686                )
687            } else {
688                colors.surface.container.highest
689            };
690
691            if selected_icon_opacity > 0.0 {
692                let icon_scale = 0.82 + 0.18 * icon_progress;
693                let icon_size = tokens::component::switch::SELECTED_ICON_SIZE * scale * icon_scale;
694
695                renderer.draw_svg(
696                    core_svg::Svg::new(core_svg::Handle::from_memory(SWITCH_ON_ICON_SVG))
697                        .color(selected_icon_color)
698                        .opacity(selected_icon_opacity),
699                    scaled_rect(handle_bounds, icon_size, icon_size),
700                    *viewport,
701                );
702            }
703
704            if unselected_icon_opacity > 0.0 {
705                let icon_size = tokens::component::switch::UNSELECTED_ICON_SIZE * scale;
706
707                renderer.draw_svg(
708                    core_svg::Svg::new(core_svg::Handle::from_memory(SWITCH_OFF_ICON_SVG))
709                        .color(unselected_icon_color)
710                        .opacity(unselected_icon_opacity),
711                    scaled_rect(handle_bounds, icon_size, icon_size),
712                    *viewport,
713                );
714            }
715        }
716
717        if self.label.is_none() {
718            return;
719        }
720
721        let label_layout = children.next().unwrap();
722
723        core_widget::text::draw(
724            renderer,
725            defaults,
726            label_layout.bounds(),
727            state.text.raw(),
728            core_widget::text::Style {
729                color: current_style.text_color,
730            },
731            viewport,
732        );
733    }
734
735    fn operate(
736        &mut self,
737        _tree: &mut Tree,
738        layout: Layout<'_>,
739        _renderer: &Renderer,
740        operation: &mut dyn core_widget::Operation,
741    ) {
742        if let Some(label) = self.label.as_deref() {
743            operation.text(None, layout.bounds(), label);
744        }
745    }
746}
747
748impl<'a, Message, Renderer> From<Toggler<'a, Message, Renderer>>
749    for Element<'a, Message, Theme, Renderer>
750where
751    Message: 'a,
752    Renderer: core_text::Renderer + core_svg::Renderer + 'a,
753{
754    fn from(toggler: Toggler<'a, Message, Renderer>) -> Self {
755        Element::new(toggler)
756    }
757}
758
759pub fn control<'a, Message, Renderer>(is_toggled: bool) -> Toggler<'a, Message, Renderer>
760where
761    Renderer: core_text::Renderer + core_svg::Renderer + 'a,
762{
763    Toggler::new(is_toggled)
764        .size(tokens::component::switch::TRACK_HEIGHT)
765        .spacing(f32::from(
766            tokens::component::divider::LIST_ITEM_LEADING_SPACE,
767        ))
768        .text_size(tokens::component::switch::LABEL_TEXT_SIZE)
769        .text_line_height(absolute_line_height(
770            tokens::component::switch::LABEL_TEXT_LINE_HEIGHT,
771        ))
772        .show_only_selected_icon(true)
773        .style(toggler_style::default)
774}
775
776pub fn standard<'a, Message, Renderer>(
777    is_toggled: bool,
778    label: impl text::IntoFragment<'a>,
779    on_toggle: impl Fn(bool) -> Message + 'a,
780) -> Element<'a, Message, Theme, Renderer>
781where
782    Message: 'a,
783    Renderer: iced_widget::core::Renderer + core_text::Renderer + core_svg::Renderer + 'a,
784{
785    Container::new(control(is_toggled).label(label).on_toggle(on_toggle))
786        .center_y(Length::Fixed(tokens::component::switch::STATE_LAYER_SIZE))
787        .into()
788}
789
790pub fn standard_with_origin<'a, Message, Renderer>(
791    is_toggled: bool,
792    label: impl text::IntoFragment<'a>,
793    on_toggle: impl Fn(bool, Point) -> Message + 'a,
794) -> Element<'a, Message, Theme, Renderer>
795where
796    Message: 'a,
797    Renderer: iced_widget::core::Renderer + core_text::Renderer + core_svg::Renderer + 'a,
798{
799    Container::new(
800        control(is_toggled)
801            .label(label)
802            .on_toggle_with_origin(on_toggle),
803    )
804    .center_y(Length::Fixed(tokens::component::switch::STATE_LAYER_SIZE))
805    .into()
806}