Skip to main content

freya_components/
switch.rs

1use accesskit::Toggled;
2use freya_animation::prelude::*;
3use freya_core::prelude::*;
4use torin::{
5    alignment::Alignment,
6    gaps::Gaps,
7    size::Size,
8};
9
10use crate::{
11    define_theme,
12    get_theme,
13};
14
15define_theme! {
16    for = Switch;
17    theme_field = theme_colors;
18
19    %[component]
20    pub SwitchColors {
21        %[fields]
22        background: Color,
23        thumb_background: Color,
24        toggled_background: Color,
25        toggled_thumb_background: Color,
26        focus_border_fill: Color,
27    }
28}
29
30define_theme! {
31    for = Switch;
32    theme_field = theme_layout;
33
34    %[component]
35    pub SwitchLayout {
36        %[fields]
37        margin: Gaps,
38        width: f32,
39        height: f32,
40        padding: f32,
41        thumb_size: f32,
42        toggled_thumb_size: f32,
43        pressed_thumb_size_offset: f32,
44        thumb_offset: f32,
45        toggled_thumb_offset: f32,
46    }
47}
48
49#[derive(Clone, PartialEq)]
50pub enum SwitchLayoutVariant {
51    Normal,
52    Expanded,
53}
54
55/// Toggle between `true` and `false`.
56///
57/// Commonly used for enabled/disabled scenarios.
58///
59/// Example: light/dark theme.
60///
61/// ```rust
62/// # use freya::prelude::*;
63/// fn app() -> impl IntoElement {
64///     let mut toggled = use_state(|| false);
65///
66///     Switch::new()
67///         .toggled(toggled())
68///         .on_toggle(move |_| toggled.toggle())
69/// }
70/// # // TOGGLED
71/// # use freya_testing::prelude::*;
72/// # launch_doc(|| {
73/// #   rect().center().expanded().child(Switch::new().toggled(true))
74/// # }, "./images/gallery_toggled_switch.png").render();
75/// #
76/// # // NOT TOGGLED
77/// # use freya_testing::prelude::*;
78/// # launch_doc(|| {
79/// #   rect().center().expanded().child(Switch::new().toggled(false))
80/// # }, "./images/gallery_not_toggled_switch.png").render();
81/// ```
82/// # Preview
83///
84/// | Toggled       | Not Toggled   |
85/// | ------------- | ------------- |
86/// | ![Switch Toggled Demo][gallery_toggled_switch] | ![Switch Not Toggled Demo][gallery_not_toggled_switch] |
87#[cfg_attr(feature = "docs",
88    doc = embed_doc_image::embed_image!(
89        "gallery_toggled_switch",
90        "images/gallery_toggled_switch.png"
91    ),
92    doc = embed_doc_image::embed_image!("gallery_not_toggled_switch", "images/gallery_not_toggled_switch.png")
93)]
94#[derive(Clone, PartialEq)]
95pub struct Switch {
96    pub(crate) theme_colors: Option<SwitchColorsThemePartial>,
97    pub(crate) theme_layout: Option<SwitchLayoutThemePartial>,
98    layout_variant: SwitchLayoutVariant,
99    toggled: Readable<bool>,
100    on_toggle: Option<EventHandler<()>>,
101    enabled: bool,
102    cursor_icon: CursorIcon,
103    key: DiffKey,
104}
105
106impl KeyExt for Switch {
107    fn write_key(&mut self) -> &mut DiffKey {
108        &mut self.key
109    }
110}
111
112impl Default for Switch {
113    fn default() -> Self {
114        Self::new()
115    }
116}
117
118impl Switch {
119    pub fn new() -> Self {
120        Self {
121            toggled: false.into(),
122            on_toggle: None,
123            theme_colors: None,
124            theme_layout: None,
125            layout_variant: SwitchLayoutVariant::Normal,
126            enabled: true,
127            cursor_icon: CursorIcon::default(),
128            key: DiffKey::None,
129        }
130    }
131
132    pub fn toggled(mut self, toggled: impl Into<Readable<bool>>) -> Self {
133        self.toggled = toggled.into();
134        self
135    }
136
137    pub fn on_toggle(mut self, on_toggle: impl Into<EventHandler<()>>) -> Self {
138        self.on_toggle = Some(on_toggle.into());
139        self
140    }
141
142    pub fn enabled(mut self, enabled: impl Into<bool>) -> Self {
143        self.enabled = enabled.into();
144        self
145    }
146
147    pub fn layout_variant(mut self, layout_variant: impl Into<SwitchLayoutVariant>) -> Self {
148        self.layout_variant = layout_variant.into();
149        self
150    }
151
152    pub fn theme_colors(mut self, theme: SwitchColorsThemePartial) -> Self {
153        self.theme_colors = Some(theme);
154        self
155    }
156
157    pub fn theme_layout(mut self, theme: SwitchLayoutThemePartial) -> Self {
158        self.theme_layout = Some(theme);
159        self
160    }
161
162    /// Shortcut for [Self::layout_variant] and [SwitchLayoutVariant::Expanded].
163    pub fn expanded(self) -> Self {
164        self.layout_variant(SwitchLayoutVariant::Expanded)
165    }
166
167    /// Override the cursor icon shown when hovering over this component while enabled.
168    pub fn cursor_icon(mut self, cursor_icon: impl Into<CursorIcon>) -> Self {
169        self.cursor_icon = cursor_icon.into();
170        self
171    }
172}
173
174impl Component for Switch {
175    fn render(self: &Switch) -> impl IntoElement {
176        let theme_colors = get_theme!(&self.theme_colors, SwitchColorsThemePreference, "switch");
177        let theme_layout = match self.layout_variant {
178            SwitchLayoutVariant::Normal => get_theme!(
179                &self.theme_layout,
180                SwitchLayoutThemePreference,
181                "switch_layout"
182            ),
183            SwitchLayoutVariant::Expanded => get_theme!(
184                &self.theme_layout,
185                SwitchLayoutThemePreference,
186                "expanded_switch_layout"
187            ),
188        };
189
190        let mut pressing = use_state(|| false);
191        let a11y_id = use_a11y();
192        let focus = use_focus(a11y_id);
193
194        let toggled = *self.toggled.read();
195
196        let anim_toggle = use_animation_with_dependencies(
197            &(theme_colors.clone(), theme_layout.clone(), toggled),
198            |conf, (switch_colors, switch_layout, toggled)| {
199                conf.on_creation(OnCreation::Finish);
200                conf.on_change(OnChange::Rerun);
201
202                let value = (
203                    AnimNum::new(
204                        switch_layout.thumb_offset,
205                        switch_layout.toggled_thumb_offset,
206                    )
207                    .time(300)
208                    .function(Function::Expo)
209                    .ease(Ease::Out),
210                    AnimNum::new(switch_layout.thumb_size, switch_layout.toggled_thumb_size)
211                        .time(300)
212                        .function(Function::Expo)
213                        .ease(Ease::Out),
214                    AnimColor::new(switch_colors.background, switch_colors.toggled_background)
215                        .time(300)
216                        .function(Function::Expo)
217                        .ease(Ease::Out),
218                    AnimColor::new(
219                        switch_colors.thumb_background,
220                        switch_colors.toggled_thumb_background,
221                    )
222                    .time(300)
223                    .function(Function::Expo)
224                    .ease(Ease::Out),
225                );
226
227                if *toggled {
228                    value
229                } else {
230                    value.into_reversed()
231                }
232            },
233        );
234
235        let anim_press = use_animation_with_dependencies(&pressing(), move |conf, pressing| {
236            conf.on_creation(OnCreation::Finish);
237            conf.on_change(OnChange::Rerun);
238            let anim = AnimNum::new(0.0, theme_layout.pressed_thumb_size_offset)
239                .time(150)
240                .function(Function::Expo)
241                .ease(Ease::Out);
242            if *pressing {
243                anim
244            } else {
245                anim.into_reversed()
246            }
247        });
248        let press_size = anim_press.get().value();
249
250        let border = if focus() == Focus::Keyboard {
251            Border::new()
252                .width(2.)
253                .alignment(BorderAlignment::Inner)
254                .fill(theme_colors.focus_border_fill.mul_if(!self.enabled, 0.9))
255        } else {
256            Border::new()
257        };
258        let (offset_x, size, background, thumb) = anim_toggle.get().value();
259
260        rect()
261            .a11y_id(a11y_id)
262            .a11y_focusable(self.enabled)
263            .a11y_role(AccessibilityRole::Switch)
264            .a11y_builder(|builder| builder.set_toggled(Toggled::from(toggled)))
265            .width(Size::px(theme_layout.width))
266            .height(Size::px(theme_layout.height))
267            .padding(Gaps::new_all(theme_layout.padding))
268            .main_align(Alignment::center())
269            .offset_x(offset_x - press_size / 2.0)
270            .corner_radius(CornerRadius::new_all(50.))
271            .background(background.mul_if(!self.enabled, 0.85))
272            .border(border)
273            .maybe(self.enabled, |rect| {
274                rect.on_press({
275                    let on_toggle = self.on_toggle.clone();
276                    move |event: Event<PressEventData>| {
277                        if let Some(on_toggle) = &on_toggle {
278                            event.stop_propagation();
279                            on_toggle.call(())
280                        }
281                        a11y_id.request_focus();
282                    }
283                })
284                .on_pointer_down(move |e: Event<PointerEventData>| {
285                    if matches!(e.data(), PointerEventData::Touch(_)) {
286                        pressing.set(true);
287                    }
288                })
289            })
290            .on_global_pointer_press(move |_| pressing.set_if_modified(false))
291            .on_pointer_leave(move |_| pressing.set_if_modified(false))
292            .cursor(if self.enabled {
293                self.cursor_icon
294            } else {
295                CursorIcon::NotAllowed
296            })
297            .child(
298                rect()
299                    .width(Size::px(size + press_size))
300                    .height(Size::px(size + press_size))
301                    .background(thumb.mul_if(!self.enabled, 0.85))
302                    .corner_radius(CornerRadius::new_all(50.)),
303            )
304    }
305
306    fn render_key(&self) -> DiffKey {
307        self.key.clone().or(self.default_key())
308    }
309}