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