1use dioxus::prelude::*;
2use freya_core::platform::CursorIcon;
3use freya_elements::{
4 self as dioxus_elements,
5 events::{
6 keyboard::Key,
7 KeyboardEvent,
8 MouseEvent,
9 WheelEvent,
10 },
11};
12use freya_hooks::{
13 use_applied_theme,
14 use_focus,
15 use_node,
16 use_platform,
17 SliderThemeWith,
18};
19
20#[derive(Props, Clone, PartialEq)]
22pub struct SliderProps {
23 pub theme: Option<SliderThemeWith>,
25 pub onmoved: EventHandler<f64>,
27 #[props(into, default = "100%".to_string())]
29 pub size: String,
30 pub value: f64,
32 #[props(default = "horizontal".to_string())]
33 pub direction: String,
34}
35
36#[inline]
37fn ensure_correct_slider_range(value: f64) -> f64 {
38 if value < 0.0 {
39 #[cfg(debug_assertions)]
40 tracing::info!("Slider value is less than 0.0, setting to 0.0");
41 0.0
42 } else if value > 100.0 {
43 #[cfg(debug_assertions)]
44 tracing::info!("Slider value is greater than 100.0, setting to 100.0");
45 100.0
46 } else {
47 value
48 }
49}
50
51#[derive(Debug, Default, PartialEq, Clone, Copy)]
53pub enum SliderStatus {
54 #[default]
56 Idle,
57 Hovering,
59}
60
61#[cfg_attr(feature = "docs",
105 doc = embed_doc_image::embed_image!("slider", "images/gallery_slider.png")
106)]
107#[allow(non_snake_case)]
108pub fn Slider(
109 SliderProps {
110 value,
111 onmoved,
112 theme,
113 size,
114 direction,
115 }: SliderProps,
116) -> Element {
117 let theme = use_applied_theme!(&theme, slider);
118 let mut focus = use_focus();
119 let mut status = use_signal(SliderStatus::default);
120 let mut clicking = use_signal(|| false);
121 let platform = use_platform();
122 let (node_reference, node_size) = use_node();
123
124 let direction_is_vertical = direction == "vertical";
125 let value = ensure_correct_slider_range(value);
126 let a11y_id = focus.attribute();
127
128 use_drop(move || {
129 if *status.peek() == SliderStatus::Hovering {
130 platform.set_cursor(CursorIcon::default());
131 }
132 });
133
134 let onkeydown = move |e: KeyboardEvent| match e.key {
135 Key::ArrowLeft if !direction_is_vertical => {
136 e.stop_propagation();
137 let percentage = (value - 4.).clamp(0.0, 100.0);
138 onmoved.call(percentage);
139 }
140 Key::ArrowRight if !direction_is_vertical => {
141 e.stop_propagation();
142 let percentage = (value + 4.).clamp(0.0, 100.0);
143 onmoved.call(percentage);
144 }
145 Key::ArrowUp if direction_is_vertical => {
146 e.stop_propagation();
147 let percentage = (value + 4.).clamp(0.0, 100.0);
148 onmoved.call(percentage);
149 }
150 Key::ArrowDown if direction_is_vertical => {
151 e.stop_propagation();
152 let percentage = (value - 4.).clamp(0.0, 100.0);
153 onmoved.call(percentage);
154 }
155 _ => {}
156 };
157
158 let onmouseleave = move |e: MouseEvent| {
159 e.stop_propagation();
160 *status.write() = SliderStatus::Idle;
161 platform.set_cursor(CursorIcon::default());
162 };
163
164 let onmouseenter = move |e: MouseEvent| {
165 e.stop_propagation();
166 *status.write() = SliderStatus::Hovering;
167 platform.set_cursor(CursorIcon::Pointer);
168 };
169
170 let onmousemove = {
171 to_owned![onmoved];
172 move |e: MouseEvent| {
173 e.stop_propagation();
174 if *clicking.peek() {
175 let coordinates = e.get_element_coordinates();
176 let percentage = if direction_is_vertical {
177 let y = coordinates.y - node_size.area.min_y() as f64 - 6.0;
178 100. - (y / (node_size.area.height() as f64 - 15.0) * 100.0)
179 } else {
180 let x = coordinates.x - node_size.area.min_x() as f64 - 6.0;
181 x / (node_size.area.width() as f64 - 15.0) * 100.0
182 };
183 let percentage = percentage.clamp(0.0, 100.0);
184
185 onmoved.call(percentage);
186 }
187 }
188 };
189
190 let onmousedown = {
191 to_owned![onmoved];
192 move |e: MouseEvent| {
193 e.stop_propagation();
194 focus.request_focus();
195 clicking.set(true);
196 let coordinates = e.get_element_coordinates();
197 let percentage = if direction_is_vertical {
198 let y = coordinates.y - 6.0;
199 100. - (y / (node_size.area.height() as f64 - 15.0) * 100.0)
200 } else {
201 let x = coordinates.x - 6.0;
202 x / (node_size.area.width() as f64 - 15.0) * 100.0
203 };
204 let percentage = percentage.clamp(0.0, 100.0);
205
206 onmoved.call(percentage);
207 }
208 };
209
210 let onclick = move |_: MouseEvent| {
211 clicking.set(false);
212 };
213
214 let onwheel = move |e: WheelEvent| {
215 e.stop_propagation();
216 let wheel_y = e.get_delta_y().clamp(-1.0, 1.0);
217 let percentage = value + (wheel_y * 2.0);
218 let percentage = percentage.clamp(0.0, 100.0);
219
220 onmoved.call(percentage);
221 };
222
223 let border = if focus.is_focused_with_keyboard() {
224 format!("2 inner {}", theme.border_fill)
225 } else {
226 "none".to_string()
227 };
228
229 let (
230 width,
231 height,
232 container_width,
233 container_height,
234 inner_width,
235 inner_height,
236 main_align,
237 offset_x,
238 offset_y,
239 ) = if direction_is_vertical {
240 let inner_height = (node_size.area.height() - 15.0) * (value / 100.0) as f32;
241 (
242 "20",
243 size.as_str(),
244 "6",
245 "100%",
246 "100%".to_string(),
247 inner_height.to_string(),
248 "end",
249 -6,
250 3,
251 )
252 } else {
253 let inner_width = (node_size.area.width() - 15.0) * (value / 100.0) as f32;
254 (
255 size.as_str(),
256 "20",
257 "100%",
258 "6",
259 inner_width.to_string(),
260 "100%".to_string(),
261 "start",
262 -3,
263 -6,
264 )
265 };
266
267 let inner_fill = rsx!(rect {
268 background: "{theme.thumb_inner_background}",
269 width: "{inner_width}",
270 height: "{inner_height}",
271 corner_radius: "50"
272 });
273
274 let thumb = rsx!(
275 rect {
276 width: "fill",
277 offset_x: "{offset_x}",
278 offset_y: "{offset_y}",
279 rect {
280 background: "{theme.thumb_background}",
281 width: "18",
282 height: "18",
283 corner_radius: "50",
284 padding: "4",
285 rect {
286 height: "100%",
287 width: "100%",
288 background: "{theme.thumb_inner_background}",
289 corner_radius: "50"
290 }
291 }
292 }
293 );
294
295 rsx!(
296 rect {
297 reference: node_reference,
298 width: "{width}",
299 height: "{height}",
300 onmousedown,
301 onglobalclick: onclick,
302 a11y_id,
303 onmouseenter,
304 onglobalmousemove: onmousemove,
305 onmouseleave,
306 onwheel: onwheel,
307 onkeydown,
308 main_align: "center",
309 cross_align: "center",
310 border: "{border}",
311 corner_radius: "8",
312 rect {
313 background: "{theme.background}",
314 width: "{container_width}",
315 height: "{container_height}",
316 main_align: "{main_align}",
317 direction: "{direction}",
318 corner_radius: "50",
319 if direction_is_vertical {
320 {thumb}
321 {inner_fill}
322 } else {
323 {inner_fill}
324 {thumb}
325 }
326 }
327 }
328 )
329}
330
331#[cfg(test)]
332mod test {
333 use dioxus::prelude::use_signal;
334 use freya::prelude::*;
335 use freya_testing::prelude::*;
336
337 #[tokio::test]
338 pub async fn slider() {
339 fn slider_app() -> Element {
340 let mut value = use_signal(|| 50.);
341
342 rsx!(
343 Slider {
344 value: *value.read(),
345 onmoved: move |p| {
346 value.set(p);
347 }
348 }
349 label {
350 "{value}"
351 }
352 )
353 }
354
355 let mut utils = launch_test(slider_app);
356 let root = utils.root();
357 let label = root.get(1);
358 utils.wait_for_update().await;
359
360 assert_eq!(label.get(0).text(), Some("50"));
361
362 utils.push_event(TestEvent::Mouse {
363 name: EventName::MouseMove,
364 cursor: (250.0, 7.0).into(),
365 button: Some(MouseButton::Left),
366 });
367 utils.push_event(TestEvent::Mouse {
368 name: EventName::MouseDown,
369 cursor: (250.0, 7.0).into(),
370 button: Some(MouseButton::Left),
371 });
372 utils.push_event(TestEvent::Mouse {
373 name: EventName::MouseMove,
374 cursor: (500.0, 7.0).into(),
375 button: Some(MouseButton::Left),
376 });
377 utils.wait_for_update().await;
378
379 assert_eq!(label.get(0).text(), Some("100"));
380 }
381}