freya_components/
switch.rs

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