freya_components/
switch.rs1use dioxus::prelude::*;
2use freya_core::platform::CursorIcon;
3use freya_elements::{
4 self as dioxus_elements,
5 events::{
6 KeyboardEvent,
7 MouseEvent,
8 },
9};
10use freya_hooks::{
11 use_animation_with_dependencies,
12 use_applied_theme,
13 use_focus,
14 use_platform,
15 AnimColor,
16 AnimNum,
17 Ease,
18 Function,
19 OnDepsChange,
20 SwitchThemeWith,
21};
22
23#[derive(Props, Clone, PartialEq)]
25pub struct SwitchProps {
26 pub theme: Option<SwitchThemeWith>,
28 pub enabled: bool,
30 pub ontoggled: EventHandler<()>,
32}
33
34#[derive(Debug, Default, PartialEq, Clone, Copy)]
36pub enum SwitchStatus {
37 #[default]
39 Idle,
40 Hovering,
42}
43
44#[cfg_attr(feature = "docs",
104 doc = embed_doc_image::embed_image!(
105 "gallery_not_enabled_switch",
106 "images/gallery_not_enabled_switch.png"
107 )
108)]
109#[cfg_attr(feature = "docs",
110 doc = embed_doc_image::embed_image!("gallery_enabled_switch", "images/gallery_enabled_switch.png")
111)]
112#[allow(non_snake_case)]
113pub fn Switch(props: SwitchProps) -> Element {
114 let theme = use_applied_theme!(&props.theme, switch);
115 let animation = use_animation_with_dependencies(&theme, |conf, theme| {
116 conf.on_deps_change(OnDepsChange::Finish);
117 (
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(&theme.background, &theme.enabled_background)
127 .time(300)
128 .function(Function::Expo)
129 .ease(Ease::Out),
130 AnimColor::new(&theme.thumb_background, &theme.enabled_thumb_background)
131 .time(300)
132 .function(Function::Expo)
133 .ease(Ease::Out),
134 )
135 });
136 let platform = use_platform();
137 let mut status = use_signal(SwitchStatus::default);
138 let mut focus = use_focus();
139
140 let a11y_id = focus.attribute();
141
142 use_drop(move || {
143 if *status.read() == SwitchStatus::Hovering {
144 platform.set_cursor(CursorIcon::default());
145 }
146 });
147
148 let onmousedown = |e: MouseEvent| {
149 e.stop_propagation();
150 };
151
152 let onmouseleave = move |e: MouseEvent| {
153 e.stop_propagation();
154 *status.write() = SwitchStatus::Idle;
155 platform.set_cursor(CursorIcon::default());
156 };
157
158 let onmouseenter = move |e: MouseEvent| {
159 e.stop_propagation();
160 *status.write() = SwitchStatus::Hovering;
161 platform.set_cursor(CursorIcon::Pointer);
162 };
163
164 let onclick = move |e: MouseEvent| {
165 e.stop_propagation();
166 focus.request_focus();
167 props.ontoggled.call(());
168 };
169
170 let onkeydown = move |e: KeyboardEvent| {
171 if focus.validate_keydown(&e) {
172 props.ontoggled.call(());
173 }
174 };
175
176 let (offset_x, size, background, circle) = &*animation.get().read_unchecked();
177 let offset_x = offset_x.read();
178 let size = size.read();
179 let background = background.read();
180 let circle = circle.read();
181
182 let border = if focus.is_focused_with_keyboard() {
183 if props.enabled {
184 format!("2 inner {}", theme.enabled_focus_border_fill)
185 } else {
186 format!("2 inner {}", theme.focus_border_fill)
187 }
188 } else {
189 "none".to_string()
190 };
191
192 use_memo(use_reactive(&props.enabled, move |enabled| {
193 if enabled {
194 animation.start();
195 } else if animation.peek_has_run_yet() {
196 animation.reverse();
197 }
198 }));
199
200 let a11y_toggled = if props.enabled { "true" } else { "false" };
201
202 rsx!(
203 rect {
204 margin: "{theme.margin}",
205 width: "48",
206 height: "25",
207 padding: "4",
208 corner_radius: "50",
209 background: "{background}",
210 border: "{border}",
211 onmousedown,
212 onmouseenter,
213 onmouseleave,
214 onkeydown,
215 onclick,
216 a11y_role: "switch",
217 a11y_id,
218 a11y_toggled,
219 offset_x: "{offset_x}",
220 main_align: "center",
221 rect {
222 background: "{circle}",
223 width: "{size}",
224 height: "{size}",
225 corner_radius: "50",
226 }
227 }
228 )
229}
230
231#[cfg(test)]
232mod test {
233 use dioxus::prelude::use_signal;
234 use freya::prelude::*;
235 use freya_testing::prelude::*;
236
237 #[tokio::test]
238 pub async fn switch() {
239 fn switch_app() -> Element {
240 let mut enabled = use_signal(|| false);
241
242 rsx!(
243 Switch {
244 enabled: *enabled.read(),
245 ontoggled: move |_| {
246 enabled.toggle();
247 }
248 }
249 label {
250 "{enabled}"
251 }
252 )
253 }
254
255 let mut utils = launch_test(switch_app);
256 let root = utils.root();
257 let label = root.get(1);
258 utils.wait_for_update().await;
259
260 assert_eq!(label.get(0).text(), Some("false"));
262
263 utils.click_cursor((15., 15.)).await;
264
265 assert_eq!(label.get(0).text(), Some("true"));
267
268 utils.click_cursor((15., 15.)).await;
269
270 assert_eq!(label.get(0).text(), Some("false"));
272 }
273}