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    get_theme,
12    theming::component_themes::SwitchThemePartial,
13};
14
15/// Toggle between `true` and `false`.
16///
17/// Commonly used for enabled/disabled scenarios.
18///
19/// Example: light/dark theme.
20///
21/// ```rust
22/// # use freya::prelude::*;
23/// fn app() -> impl IntoElement {
24///     let mut toggled = use_state(|| false);
25///
26///     Switch::new()
27///         .toggled(toggled())
28///         .on_toggle(move |_| toggled.toggle())
29/// }
30/// # // TOGGLED
31/// # use freya_testing::prelude::*;
32/// # launch_doc(|| {
33/// #   rect().center().expanded().child(Switch::new().toggled(true))
34/// # }, "./images/gallery_toggled_switch.png").render();
35/// #
36/// # // NOT TOGGLED
37/// # use freya_testing::prelude::*;
38/// # launch_doc(|| {
39/// #   rect().center().expanded().child(Switch::new().toggled(false))
40/// # }, "./images/gallery_not_toggled_switch.png").render();
41/// ```
42/// # Preview
43///
44/// | Toggled       | Not Toggled   |
45/// | ------------- | ------------- |
46/// | ![Switch Toggled Demo][gallery_toggled_switch] | ![Switch Not Toggled Demo][gallery_not_toggled_switch] |
47#[cfg_attr(feature = "docs",
48    doc = embed_doc_image::embed_image!(
49        "gallery_toggled_switch",
50        "images/gallery_toggled_switch.png"
51    ),
52    doc = embed_doc_image::embed_image!("gallery_not_toggled_switch", "images/gallery_not_toggled_switch.png")
53)]
54#[derive(Clone, PartialEq)]
55pub struct Switch {
56    pub(crate) theme: Option<SwitchThemePartial>,
57    toggled: ReadState<bool>,
58    on_toggle: Option<EventHandler<()>>,
59    enabled: bool,
60    key: DiffKey,
61}
62
63impl KeyExt for Switch {
64    fn write_key(&mut self) -> &mut DiffKey {
65        &mut self.key
66    }
67}
68
69impl Default for Switch {
70    fn default() -> Self {
71        Self::new()
72    }
73}
74
75impl Switch {
76    pub fn new() -> Self {
77        Self {
78            toggled: false.into(),
79            on_toggle: None,
80            theme: None,
81            enabled: true,
82            key: DiffKey::None,
83        }
84    }
85
86    pub fn toggled(mut self, toggled: impl Into<ReadState<bool>>) -> Self {
87        self.toggled = toggled.into();
88        self
89    }
90
91    pub fn on_toggle(mut self, on_toggle: impl Into<EventHandler<()>>) -> Self {
92        self.on_toggle = Some(on_toggle.into());
93        self
94    }
95
96    pub fn enabled(mut self, enabled: impl Into<bool>) -> Self {
97        self.enabled = enabled.into();
98        self
99    }
100}
101
102impl Component for Switch {
103    fn render(self: &Switch) -> impl IntoElement {
104        let theme = get_theme!(&self.theme, switch);
105        let mut hovering = use_state(|| false);
106        let focus = use_focus();
107        let focus_status = use_focus_status(focus);
108
109        let toggled = *self.toggled.read();
110
111        let animation = use_animation_with_dependencies(
112            &(theme.clone(), toggled),
113            |conf, (switch_theme, toggled)| {
114                conf.on_creation(OnCreation::Finish);
115                conf.on_change(OnChange::Rerun);
116
117                let value = (
118                    AnimNum::new(2., 22.)
119                        .time(300)
120                        .function(Function::Expo)
121                        .ease(Ease::Out),
122                    AnimNum::new(14., 18.)
123                        .time(300)
124                        .function(Function::Expo)
125                        .ease(Ease::Out),
126                    AnimColor::new(switch_theme.background, switch_theme.toggled_background)
127                        .time(300)
128                        .function(Function::Expo)
129                        .ease(Ease::Out),
130                    AnimColor::new(
131                        switch_theme.thumb_background,
132                        switch_theme.toggled_thumb_background,
133                    )
134                    .time(300)
135                    .function(Function::Expo)
136                    .ease(Ease::Out),
137                );
138
139                if *toggled {
140                    value
141                } else {
142                    value.into_reversed()
143                }
144            },
145        );
146
147        let enabled = use_reactive(&self.enabled);
148        use_drop(move || {
149            if hovering() && enabled() {
150                Cursor::set(CursorIcon::default());
151            }
152        });
153
154        let border = if focus_status() == FocusStatus::Keyboard {
155            Border::new()
156                .width(2.)
157                .alignment(BorderAlignment::Inner)
158                .fill(theme.focus_border_fill.mul_if(!self.enabled, 0.9))
159        } else {
160            Border::new()
161        };
162        let (offset_x, size, background, thumb) = animation.get().value();
163
164        rect()
165            .a11y_id(focus.a11y_id())
166            .a11y_focusable(self.enabled)
167            .a11y_role(AccessibilityRole::Switch)
168            .a11y_builder(|builder| builder.set_toggled(Toggled::from(toggled)))
169            .width(Size::px(48.))
170            .height(Size::px(25.))
171            .padding(Gaps::new_all(4.0))
172            .main_align(Alignment::center())
173            .offset_x(offset_x)
174            .corner_radius(CornerRadius::new_all(50.))
175            .background(background.mul_if(!self.enabled, 0.85))
176            .border(border)
177            .maybe(self.enabled, |rect| {
178                rect.on_press({
179                    let on_toggle = self.on_toggle.clone();
180                    move |_| {
181                        if let Some(on_toggle) = &on_toggle {
182                            on_toggle.call(())
183                        }
184                        focus.request_focus();
185                    }
186                })
187            })
188            .on_pointer_enter(move |_| {
189                hovering.set(true);
190                if enabled() {
191                    Cursor::set(CursorIcon::Pointer);
192                } else {
193                    Cursor::set(CursorIcon::NotAllowed);
194                }
195            })
196            .on_pointer_leave(move |_| {
197                if hovering() {
198                    Cursor::set(CursorIcon::default());
199                    hovering.set(false);
200                }
201            })
202            .child(
203                rect()
204                    .width(Size::px(size))
205                    .height(Size::px(size))
206                    .background(thumb.mul_if(!self.enabled, 0.85))
207                    .corner_radius(CornerRadius::new_all(50.)),
208            )
209    }
210
211    fn render_key(&self) -> DiffKey {
212        self.key.clone().or(self.default_key())
213    }
214}