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 toggled = use_state(|| false);
65///
66///     Switch::new(toggled)
67/// }
68/// ```
69/// See the [interactive components demo](https://freyaui.dev/demo).
70#[derive(Clone, PartialEq)]
71pub struct Switch {
72    pub(crate) theme_colors: Option<SwitchColorsThemePartial>,
73    pub(crate) theme_layout: Option<SwitchLayoutThemePartial>,
74    layout_variant: SwitchLayoutVariant,
75    value: WritableSetter<bool>,
76    enabled: bool,
77    cursor_icon: Option<CursorIcon>,
78    key: DiffKey,
79}
80
81impl KeyExt for Switch {
82    fn key_mut(&mut self) -> &mut DiffKey {
83        &mut self.key
84    }
85}
86
87impl Switch {
88    pub fn new(value: impl Into<WritableSetter<bool>>) -> Self {
89        Self {
90            value: value.into(),
91            theme_colors: None,
92            theme_layout: None,
93            layout_variant: SwitchLayoutVariant::Normal,
94            enabled: true,
95            cursor_icon: None,
96            key: DiffKey::None,
97        }
98    }
99
100    pub fn enabled(mut self, enabled: impl Into<bool>) -> Self {
101        self.enabled = enabled.into();
102        self
103    }
104
105    pub fn layout_variant(mut self, layout_variant: impl Into<SwitchLayoutVariant>) -> Self {
106        self.layout_variant = layout_variant.into();
107        self
108    }
109
110    pub fn theme_colors(mut self, theme: SwitchColorsThemePartial) -> Self {
111        self.theme_colors = Some(theme);
112        self
113    }
114
115    pub fn theme_layout(mut self, theme: SwitchLayoutThemePartial) -> Self {
116        self.theme_layout = Some(theme);
117        self
118    }
119
120    /// Shortcut for [Self::layout_variant] and [SwitchLayoutVariant::Expanded].
121    pub fn expanded(self) -> Self {
122        self.layout_variant(SwitchLayoutVariant::Expanded)
123    }
124
125    /// Override the cursor icon shown when hovering over this component while enabled.
126    pub fn cursor(mut self, cursor_icon: impl Into<Option<CursorIcon>>) -> Self {
127        self.cursor_icon = cursor_icon.into();
128        self
129    }
130}
131
132impl Component for Switch {
133    fn render(self: &Switch) -> impl IntoElement {
134        let theme_colors = get_theme!(&self.theme_colors, SwitchColorsThemePreference, "switch");
135        let theme_layout = match self.layout_variant {
136            SwitchLayoutVariant::Normal => get_theme!(
137                &self.theme_layout,
138                SwitchLayoutThemePreference,
139                "switch_layout"
140            ),
141            SwitchLayoutVariant::Expanded => get_theme!(
142                &self.theme_layout,
143                SwitchLayoutThemePreference,
144                "expanded_switch_layout"
145            ),
146        };
147
148        let mut pressing = use_state(|| false);
149        let a11y_id = use_a11y();
150        let focus = use_focus(a11y_id);
151
152        let toggled = *self.value.read();
153
154        let anim_toggle = use_animation_with_dependencies(
155            &(theme_colors.clone(), theme_layout.clone(), toggled),
156            |conf, (switch_colors, switch_layout, toggled)| {
157                conf.on_creation(OnCreation::Finish);
158                conf.on_change(OnChange::Rerun);
159
160                let value = (
161                    AnimNum::new(
162                        switch_layout.thumb_offset,
163                        switch_layout.toggled_thumb_offset,
164                    )
165                    .time(300)
166                    .function(Function::Expo)
167                    .ease(Ease::Out),
168                    AnimNum::new(switch_layout.thumb_size, switch_layout.toggled_thumb_size)
169                        .time(300)
170                        .function(Function::Expo)
171                        .ease(Ease::Out),
172                    AnimColor::new(switch_colors.background, switch_colors.toggled_background)
173                        .time(300)
174                        .function(Function::Expo)
175                        .ease(Ease::Out),
176                    AnimColor::new(
177                        switch_colors.thumb_background,
178                        switch_colors.toggled_thumb_background,
179                    )
180                    .time(300)
181                    .function(Function::Expo)
182                    .ease(Ease::Out),
183                );
184
185                if *toggled {
186                    value
187                } else {
188                    value.into_reversed()
189                }
190            },
191        );
192
193        let anim_press = use_animation_with_dependencies(&pressing(), move |conf, pressing| {
194            conf.on_creation(OnCreation::Finish);
195            conf.on_change(OnChange::Rerun);
196            let anim = AnimNum::new(0.0, theme_layout.pressed_thumb_size_offset)
197                .time(150)
198                .function(Function::Expo)
199                .ease(Ease::Out);
200            if *pressing {
201                anim
202            } else {
203                anim.into_reversed()
204            }
205        });
206        let press_size = anim_press.get().value();
207
208        let border = if focus() == Focus::Keyboard {
209            Border::new()
210                .width(2.)
211                .alignment(BorderAlignment::Inner)
212                .fill(theme_colors.focus_border_fill.mul_if(!self.enabled, 0.9))
213        } else {
214            Border::new()
215        };
216        let (offset_x, size, background, thumb) = anim_toggle.get().value();
217
218        rect()
219            .a11y_id(a11y_id)
220            .a11y_enabled(self.enabled)
221            .a11y_focusable(self.enabled)
222            .a11y_role(AccessibilityRole::Switch)
223            .a11y_builder(|builder| builder.set_toggled(Toggled::from(toggled)))
224            .width(Size::px(theme_layout.width))
225            .height(Size::px(theme_layout.height))
226            .padding(Gaps::new_all(theme_layout.padding))
227            .main_align(Alignment::center())
228            .offset_x(offset_x - press_size / 2.0)
229            .corner_radius(CornerRadius::new_all(50.))
230            .background(background.mul_if(!self.enabled, 0.85))
231            .border(border)
232            .maybe(self.enabled, |rect| {
233                rect.on_press({
234                    let mut value = self.value.clone();
235                    move |event: Event<PressEventData>| {
236                        event.stop_propagation();
237                        let toggled = !*value.peek();
238                        value.set(toggled);
239                        a11y_id.request_focus();
240                    }
241                })
242                .on_pointer_down(move |e: Event<PointerEventData>| {
243                    if matches!(e.data(), PointerEventData::Touch(_)) {
244                        pressing.set(true);
245                    }
246                })
247            })
248            .on_global_pointer_up(move |_| pressing.set_if_modified(false))
249            .on_pointer_leave(move |_| pressing.set_if_modified(false))
250            .cursor(if self.enabled {
251                self.cursor_icon
252            } else {
253                CursorIcon::NotAllowed.into()
254            })
255            .child(
256                rect()
257                    .width(Size::px(size + press_size))
258                    .height(Size::px(size + press_size))
259                    .background(thumb.mul_if(!self.enabled, 0.85))
260                    .corner_radius(CornerRadius::new_all(50.)),
261            )
262    }
263
264    fn render_key(&self) -> DiffKey {
265        self.key.clone().or(self.default_key())
266    }
267}