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    key: DiffKey,
103}
104
105impl KeyExt for Switch {
106    fn write_key(&mut self) -> &mut DiffKey {
107        &mut self.key
108    }
109}
110
111impl Default for Switch {
112    fn default() -> Self {
113        Self::new()
114    }
115}
116
117impl Switch {
118    pub fn new() -> Self {
119        Self {
120            toggled: false.into(),
121            on_toggle: None,
122            theme_colors: None,
123            theme_layout: None,
124            layout_variant: SwitchLayoutVariant::Normal,
125            enabled: true,
126            key: DiffKey::None,
127        }
128    }
129
130    pub fn toggled(mut self, toggled: impl Into<Readable<bool>>) -> Self {
131        self.toggled = toggled.into();
132        self
133    }
134
135    pub fn on_toggle(mut self, on_toggle: impl Into<EventHandler<()>>) -> Self {
136        self.on_toggle = Some(on_toggle.into());
137        self
138    }
139
140    pub fn enabled(mut self, enabled: impl Into<bool>) -> Self {
141        self.enabled = enabled.into();
142        self
143    }
144
145    pub fn layout_variant(mut self, layout_variant: impl Into<SwitchLayoutVariant>) -> Self {
146        self.layout_variant = layout_variant.into();
147        self
148    }
149
150    pub fn theme_colors(mut self, theme: SwitchColorsThemePartial) -> Self {
151        self.theme_colors = Some(theme);
152        self
153    }
154
155    pub fn theme_layout(mut self, theme: SwitchLayoutThemePartial) -> Self {
156        self.theme_layout = Some(theme);
157        self
158    }
159
160    /// Shortcut for [Self::layout_variant] and [SwitchLayoutVariant::Expanded].
161    pub fn expanded(self) -> Self {
162        self.layout_variant(SwitchLayoutVariant::Expanded)
163    }
164}
165
166impl Component for Switch {
167    fn render(self: &Switch) -> impl IntoElement {
168        let theme_colors = get_theme!(&self.theme_colors, SwitchColorsThemePreference, "switch");
169        let theme_layout = match self.layout_variant {
170            SwitchLayoutVariant::Normal => get_theme!(
171                &self.theme_layout,
172                SwitchLayoutThemePreference,
173                "switch_layout"
174            ),
175            SwitchLayoutVariant::Expanded => get_theme!(
176                &self.theme_layout,
177                SwitchLayoutThemePreference,
178                "expanded_switch_layout"
179            ),
180        };
181
182        let mut hovering = use_state(|| false);
183        let mut pressing = use_state(|| false);
184        let focus = use_focus();
185        let focus_status = use_focus_status(focus);
186
187        let toggled = *self.toggled.read();
188
189        let anim_toggle = use_animation_with_dependencies(
190            &(theme_colors.clone(), theme_layout.clone(), toggled),
191            |conf, (switch_colors, switch_layout, toggled)| {
192                conf.on_creation(OnCreation::Finish);
193                conf.on_change(OnChange::Rerun);
194
195                let value = (
196                    AnimNum::new(
197                        switch_layout.thumb_offset,
198                        switch_layout.toggled_thumb_offset,
199                    )
200                    .time(300)
201                    .function(Function::Expo)
202                    .ease(Ease::Out),
203                    AnimNum::new(switch_layout.thumb_size, switch_layout.toggled_thumb_size)
204                        .time(300)
205                        .function(Function::Expo)
206                        .ease(Ease::Out),
207                    AnimColor::new(switch_colors.background, switch_colors.toggled_background)
208                        .time(300)
209                        .function(Function::Expo)
210                        .ease(Ease::Out),
211                    AnimColor::new(
212                        switch_colors.thumb_background,
213                        switch_colors.toggled_thumb_background,
214                    )
215                    .time(300)
216                    .function(Function::Expo)
217                    .ease(Ease::Out),
218                );
219
220                if *toggled {
221                    value
222                } else {
223                    value.into_reversed()
224                }
225            },
226        );
227
228        let anim_press = use_animation_with_dependencies(&pressing(), move |conf, pressing| {
229            conf.on_creation(OnCreation::Finish);
230            conf.on_change(OnChange::Rerun);
231            let anim = AnimNum::new(0.0, theme_layout.pressed_thumb_size_offset)
232                .time(150)
233                .function(Function::Expo)
234                .ease(Ease::Out);
235            if *pressing {
236                anim
237            } else {
238                anim.into_reversed()
239            }
240        });
241        let press_size = anim_press.get().value();
242
243        let enabled = use_reactive(&self.enabled);
244        use_drop(move || {
245            if hovering() && enabled() {
246                Cursor::set(CursorIcon::default());
247            }
248        });
249
250        let border = if focus_status() == FocusStatus::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(focus.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 |_| {
277                        if let Some(on_toggle) = &on_toggle {
278                            on_toggle.call(())
279                        }
280                        focus.request_focus();
281                    }
282                })
283                .on_pointer_down(move |e: Event<PointerEventData>| {
284                    if matches!(e.data(), PointerEventData::Touch(_)) {
285                        pressing.set(true);
286                    }
287                })
288            })
289            .on_global_pointer_press(move |_| pressing.set_if_modified(false))
290            .on_pointer_enter(move |_| {
291                hovering.set(true);
292                if enabled() {
293                    Cursor::set(CursorIcon::Pointer);
294                } else {
295                    Cursor::set(CursorIcon::NotAllowed);
296                }
297            })
298            .on_pointer_leave(move |_| {
299                if hovering() {
300                    Cursor::set(CursorIcon::default());
301                    hovering.set(false);
302                }
303                pressing.set_if_modified(false);
304            })
305            .child(
306                rect()
307                    .width(Size::px(size + press_size))
308                    .height(Size::px(size + press_size))
309                    .background(thumb.mul_if(!self.enabled, 0.85))
310                    .corner_radius(CornerRadius::new_all(50.)),
311            )
312    }
313
314    fn render_key(&self) -> DiffKey {
315        self.key.clone().or(self.default_key())
316    }
317}