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 hovering = use_state(|| false);
191        let mut pressing = use_state(|| false);
192        let a11y_id = use_a11y();
193        let focus = use_focus(a11y_id);
194
195        let toggled = *self.toggled.read();
196
197        let anim_toggle = use_animation_with_dependencies(
198            &(theme_colors.clone(), theme_layout.clone(), toggled),
199            |conf, (switch_colors, switch_layout, toggled)| {
200                conf.on_creation(OnCreation::Finish);
201                conf.on_change(OnChange::Rerun);
202
203                let value = (
204                    AnimNum::new(
205                        switch_layout.thumb_offset,
206                        switch_layout.toggled_thumb_offset,
207                    )
208                    .time(300)
209                    .function(Function::Expo)
210                    .ease(Ease::Out),
211                    AnimNum::new(switch_layout.thumb_size, switch_layout.toggled_thumb_size)
212                        .time(300)
213                        .function(Function::Expo)
214                        .ease(Ease::Out),
215                    AnimColor::new(switch_colors.background, switch_colors.toggled_background)
216                        .time(300)
217                        .function(Function::Expo)
218                        .ease(Ease::Out),
219                    AnimColor::new(
220                        switch_colors.thumb_background,
221                        switch_colors.toggled_thumb_background,
222                    )
223                    .time(300)
224                    .function(Function::Expo)
225                    .ease(Ease::Out),
226                );
227
228                if *toggled {
229                    value
230                } else {
231                    value.into_reversed()
232                }
233            },
234        );
235
236        let anim_press = use_animation_with_dependencies(&pressing(), move |conf, pressing| {
237            conf.on_creation(OnCreation::Finish);
238            conf.on_change(OnChange::Rerun);
239            let anim = AnimNum::new(0.0, theme_layout.pressed_thumb_size_offset)
240                .time(150)
241                .function(Function::Expo)
242                .ease(Ease::Out);
243            if *pressing {
244                anim
245            } else {
246                anim.into_reversed()
247            }
248        });
249        let press_size = anim_press.get().value();
250
251        let enabled = use_reactive(&self.enabled);
252        let cursor_icon = self.cursor_icon;
253        use_drop(move || {
254            if hovering() && enabled() {
255                Cursor::set(CursorIcon::default());
256            }
257        });
258
259        let border = if focus() == Focus::Keyboard {
260            Border::new()
261                .width(2.)
262                .alignment(BorderAlignment::Inner)
263                .fill(theme_colors.focus_border_fill.mul_if(!self.enabled, 0.9))
264        } else {
265            Border::new()
266        };
267        let (offset_x, size, background, thumb) = anim_toggle.get().value();
268
269        rect()
270            .a11y_id(a11y_id)
271            .a11y_focusable(self.enabled)
272            .a11y_role(AccessibilityRole::Switch)
273            .a11y_builder(|builder| builder.set_toggled(Toggled::from(toggled)))
274            .width(Size::px(theme_layout.width))
275            .height(Size::px(theme_layout.height))
276            .padding(Gaps::new_all(theme_layout.padding))
277            .main_align(Alignment::center())
278            .offset_x(offset_x - press_size / 2.0)
279            .corner_radius(CornerRadius::new_all(50.))
280            .background(background.mul_if(!self.enabled, 0.85))
281            .border(border)
282            .maybe(self.enabled, |rect| {
283                rect.on_press({
284                    let on_toggle = self.on_toggle.clone();
285                    move |_| {
286                        if let Some(on_toggle) = &on_toggle {
287                            on_toggle.call(())
288                        }
289                        a11y_id.request_focus();
290                    }
291                })
292                .on_pointer_down(move |e: Event<PointerEventData>| {
293                    if matches!(e.data(), PointerEventData::Touch(_)) {
294                        pressing.set(true);
295                    }
296                })
297            })
298            .on_global_pointer_press(move |_| pressing.set_if_modified(false))
299            .on_pointer_enter(move |_| {
300                hovering.set(true);
301                if enabled() {
302                    Cursor::set(cursor_icon);
303                } else {
304                    Cursor::set(CursorIcon::NotAllowed);
305                }
306            })
307            .on_pointer_leave(move |_| {
308                if hovering() {
309                    Cursor::set(CursorIcon::default());
310                    hovering.set(false);
311                }
312                pressing.set_if_modified(false);
313            })
314            .child(
315                rect()
316                    .width(Size::px(size + press_size))
317                    .height(Size::px(size + press_size))
318                    .background(thumb.mul_if(!self.enabled, 0.85))
319                    .corner_radius(CornerRadius::new_all(50.)),
320            )
321    }
322
323    fn render_key(&self) -> DiffKey {
324        self.key.clone().or(self.default_key())
325    }
326}