1use crate::components::config_provider::use_config;
2use crate::components::form::use_form_item_control;
3#[cfg(target_arch = "wasm32")]
4use crate::components::interaction::start_pointer;
5use crate::components::interaction::{
6 PointerState, as_pointer_event, end_pointer, is_active_pointer,
7};
8#[cfg(target_arch = "wasm32")]
9use crate::components::slider_base::ratio_from_pointer_event;
10#[cfg(target_arch = "wasm32")]
11use crate::components::slider_base::ratio_to_value;
12use crate::components::slider_base::{
13 SliderMath, SliderOrientation, apply_keyboard_action, keyboard_action_for_key, snap_value,
14 value_to_ratio,
15};
16use dioxus::events::{KeyboardEvent, PointerData};
17use dioxus::prelude::*;
18use serde_json::{Number, Value};
19use std::rc::Rc;
20#[cfg(target_arch = "wasm32")]
21use wasm_bindgen::JsCast;
22
23#[derive(Clone, PartialEq)]
25pub struct SliderMark {
26 pub value: f64,
27 pub label: String,
28}
29
30#[derive(Clone, PartialEq, Debug)]
32pub enum SliderValue {
33 Single(f64),
34 Range(f64, f64),
35}
36
37impl SliderValue {
38 pub fn as_single(&self) -> f64 {
39 match *self {
40 SliderValue::Single(v) => v,
41 SliderValue::Range(_, end) => end,
42 }
43 }
44
45 pub fn as_range(&self) -> (f64, f64) {
46 match *self {
47 SliderValue::Single(v) => (v, v),
48 SliderValue::Range(start, end) => (start, end),
49 }
50 }
51
52 fn ensure_range(self) -> Self {
53 match self {
54 SliderValue::Single(v) => SliderValue::Range(v, v),
55 SliderValue::Range(start, end) => {
56 if start <= end {
57 SliderValue::Range(start, end)
58 } else {
59 SliderValue::Range(end, start)
60 }
61 }
62 }
63 }
64}
65
66#[derive(Props, Clone, PartialEq)]
68pub struct SliderProps {
69 #[props(optional)]
71 pub value: Option<SliderValue>,
72 #[props(optional)]
74 pub default_value: Option<SliderValue>,
75 #[props(default)]
77 pub range: bool,
78 #[props(default = 0.0)]
79 pub min: f64,
80 #[props(default = 100.0)]
81 pub max: f64,
82 #[props(optional)]
84 pub step: Option<f64>,
85 #[props(optional)]
87 pub precision: Option<u32>,
88 #[props(default)]
90 pub reverse: bool,
91 #[props(default)]
93 pub vertical: bool,
94 #[props(default)]
96 pub disabled: bool,
97 #[props(default)]
99 pub dots: bool,
100 #[props(optional)]
102 pub marks: Option<Vec<SliderMark>>,
103 #[props(optional)]
104 pub class: Option<String>,
105 #[props(optional)]
106 pub style: Option<String>,
107 #[props(optional)]
109 pub on_change: Option<EventHandler<SliderValue>>,
110 #[props(optional)]
112 pub on_change_complete: Option<EventHandler<SliderValue>>,
113}
114
115#[component]
116pub fn Slider(props: SliderProps) -> Element {
117 let SliderProps {
118 value,
119 default_value,
120 range,
121 min,
122 max,
123 step,
124 precision,
125 reverse,
126 vertical,
127 disabled,
128 dots,
129 marks,
130 class,
131 style,
132 on_change,
133 on_change_complete,
134 } = props;
135
136 let math = SliderMath {
137 min,
138 max,
139 step,
140 precision,
141 reverse,
142 orientation: if vertical {
143 SliderOrientation::Vertical
144 } else {
145 SliderOrientation::Horizontal
146 },
147 };
148
149 let config = use_config();
150 let form_control = use_form_item_control();
151 let controlled_by_prop = value.is_some();
152
153 let initial_value = normalize_value(
154 range,
155 value
156 .clone()
157 .or_else(|| {
158 form_control
159 .as_ref()
160 .and_then(|ctx| slider_value_from_form(ctx.value(), range))
161 })
162 .or(default_value.clone())
163 .unwrap_or_else(|| default_slider_value(range, &math)),
164 &math,
165 );
166
167 let current = use_signal(|| initial_value.clone());
168 let mut active_handle = use_signal(|| None::<usize>);
169 let active_pointer = use_signal::<PointerState>(PointerState::default);
170 {
172 let mut current_signal = current.clone();
173 let form_ctx = form_control.clone();
174 let value_prop = value.clone();
175 let default_val = default_value.clone();
176 use_effect(move || {
177 let next = normalize_value(
178 range,
179 value_prop
180 .clone()
181 .or_else(|| {
182 form_ctx
183 .as_ref()
184 .and_then(|ctx| slider_value_from_form(ctx.value(), range))
185 })
186 .or(default_val.clone())
187 .unwrap_or_else(|| default_slider_value(range, &math)),
188 &math,
189 );
190 current_signal.set(next);
191 });
192 }
193
194 let is_disabled =
195 disabled || config.disabled || form_control.as_ref().is_some_and(|ctx| ctx.is_disabled());
196
197 let mut class_list = vec!["adui-slider".to_string()];
198 if is_disabled {
199 class_list.push("adui-slider-disabled".into());
200 }
201 if vertical {
202 class_list.push("adui-slider-vertical".into());
203 }
204 if dots {
205 class_list.push("adui-slider-dots".into());
206 }
207 if let Some(extra) = class {
208 class_list.push(extra);
209 }
210 let class_attr = class_list.join(" ");
211 let style_attr = style.unwrap_or_default();
212
213 let on_change_cb = on_change;
214 let on_change_complete_cb = on_change_complete;
215 let form_ctx_for_apply = form_control.clone();
216 let current_for_apply = current.clone();
217
218 let apply_value = move |next: SliderValue, fire_change: bool| {
219 let normalized = normalize_value(range, next, &math);
220 if !controlled_by_prop {
221 let mut state = current_for_apply;
222 state.set(normalized.clone());
223 }
224
225 if let Some(ctx) = form_ctx_for_apply.as_ref() {
226 ctx.set_value(slider_value_to_form(&normalized));
227 }
228
229 if fire_change {
230 if let Some(cb) = on_change_cb.as_ref() {
231 cb.call(normalized.clone());
232 }
233 }
234 normalized
235 };
236
237 let handle_pointer_move = {
238 #[allow(unused_variables)]
239 let current_for_move = current.clone();
240 #[allow(unused_variables)]
241 let active_handle_for_move = active_handle.clone();
242 #[allow(unused_variables)]
243 let active_pointer_for_move = active_pointer.clone();
244 #[allow(unused_variables)]
245 let apply_for_move = apply_value.clone();
246 move |evt: Event<PointerData>| {
247 #[cfg(target_arch = "wasm32")]
248 {
249 if is_disabled {
250 return;
251 }
252 let Some(pevt) = as_pointer_event(&evt) else {
253 return;
254 };
255 if !is_active_pointer(&active_pointer_for_move, &pevt) {
256 return;
257 }
258 let rect = pevt
259 .current_target()
260 .and_then(|t| t.dyn_into::<web_sys::Element>().ok())
261 .map(|el| el.get_bounding_client_rect());
262 let Some(rect) = rect else {
263 return;
264 };
265 let Some(ratio) = pointer_ratio(&pevt, &rect, &math) else {
266 return;
267 };
268 let target_value = ratio_to_value(ratio, &math);
269
270 let handle_idx = active_handle_for_move.read().unwrap_or(0);
271 let next = update_handle_value(
272 ¤t_for_move.read(),
273 handle_idx,
274 target_value,
275 range,
276 &math,
277 );
278 apply_for_move(next, true);
279 }
280 #[cfg(not(target_arch = "wasm32"))]
281 {
282 let _ = evt;
283 }
284 }
285 };
286
287 let mut pointer_state_for_up = active_pointer.clone();
288 let mut active_handle_for_up = active_handle.clone();
289 let current_for_up = current.clone();
290 let on_change_complete_for_up = on_change_complete_cb.clone();
291
292 let handle_pointer_up = move |evt: Event<PointerData>| {
293 let Some(pevt) = as_pointer_event(&evt) else {
294 return;
295 };
296 if !is_active_pointer(&pointer_state_for_up, &pevt) {
297 return;
298 }
299 end_pointer(&mut pointer_state_for_up, &pevt);
300 active_handle_for_up.set(None);
301 if let Some(cb) = on_change_complete_for_up.as_ref() {
302 cb.call(current_for_up.read().clone());
303 }
304 };
305
306 let apply_for_key = apply_value.clone();
307
308 let handle_track_pointer_down = {
309 #[allow(unused_variables)]
310 let apply_for_track = apply_value.clone();
311 move |evt: Event<PointerData>| {
312 #[cfg(target_arch = "wasm32")]
313 {
314 if is_disabled {
315 return;
316 }
317 let Some(pevt) = as_pointer_event(&evt) else {
318 return;
319 };
320 let rect = pevt
321 .current_target()
322 .and_then(|t| t.dyn_into::<web_sys::Element>().ok())
323 .map(|el| el.get_bounding_client_rect());
324 let Some(rect) = rect else {
325 return;
326 };
327 let Some(ratio) = pointer_ratio(&pevt, &rect, &math) else {
328 return;
329 };
330 let target_value = ratio_to_value(ratio, &math);
331
332 let current_value = current.read();
333 let handle_idx = choose_handle(¤t_value, target_value);
334 active_handle.set(Some(handle_idx));
335 let mut pointer_for_down = active_pointer.clone();
336 start_pointer(&mut pointer_for_down, &pevt);
337
338 let next =
339 update_handle_value(¤t_value, handle_idx, target_value, range, &math);
340 let normalized = apply_for_track(next, true);
341 if let Some(cb) = on_change_complete_cb.as_ref() {
342 cb.call(normalized);
343 }
344 }
345 #[cfg(not(target_arch = "wasm32"))]
346 {
347 let _ = evt;
348 }
349 }
350 };
351
352 let handle_key: Rc<dyn Fn(usize, KeyboardEvent)> = Rc::new({
353 let current_signal = current.clone();
354 move |idx: usize, evt: KeyboardEvent| {
355 if is_disabled {
356 return;
357 }
358 if let Some(action) = keyboard_action_for_key(&evt.key(), math.reverse) {
359 let current_value = current_signal.read();
360 let handle_value = match *current_value {
361 SliderValue::Single(v) => v,
362 SliderValue::Range(start, end) => {
363 if idx == 0 {
364 start
365 } else {
366 end
367 }
368 }
369 };
370 let stepped = apply_keyboard_action(handle_value, action, &math);
371 let next = update_handle_value(¤t_value, idx, stepped, range, &math);
372 apply_for_key(next, true);
373 }
374 }
375 });
376
377 let value_now = current.read().clone();
378 let (ratios, handle_values): (Vec<f64>, Vec<f64>) = match value_now {
379 SliderValue::Single(v) => (vec![value_to_ratio(v, &math)], vec![v]),
380 SliderValue::Range(a, b) => (
381 vec![value_to_ratio(a, &math), value_to_ratio(b, &math)],
382 vec![a, b],
383 ),
384 };
385 let track_range = track_range_style(&ratios, math.orientation);
386
387 let marks_view = marks.map(|items| {
388 let dots_enabled = dots;
389 items
390 .into_iter()
391 .map(|mark| {
392 let pos = value_to_ratio(mark.value, &math) * 100.0;
393 rsx! {
394 div { class: "adui-slider-mark", style: match math.orientation {
395 SliderOrientation::Vertical => format!("bottom:{pos:.2}%;"),
396 SliderOrientation::Horizontal => format!("left:{pos:.2}%;"),
397 },
398 if dots_enabled {
399 span { class: "adui-slider-dot" }
400 }
401 span { class: "adui-slider-mark-label", {mark.label} }
402 }
403 }
404 })
405 .collect::<Vec<_>>()
406 });
407
408 rsx! {
409 div {
410 class: "{class_attr}",
411 style: "{style_attr}",
412 onpointerdown: handle_track_pointer_down,
413 onpointermove: handle_pointer_move,
414 onpointerup: handle_pointer_up,
415 onpointercancel: handle_pointer_up,
416 onpointerleave: handle_pointer_up,
417 div { class: "adui-slider-rail" }
418 div { class: "adui-slider-track", style: "{track_range}" }
419 {handles_view(&ratios, &handle_values, range, math, is_disabled, handle_key.clone())}
420 if let Some(marks) = marks_view {
421 div { class: "adui-slider-marks",
422 for mark in marks {
423 {mark}
424 }
425 }
426 }
427 }
428 }
429}
430
431fn handles_view(
432 ratios: &[f64],
433 values: &[f64],
434 range: bool,
435 math: SliderMath,
436 disabled: bool,
437 on_key: Rc<dyn Fn(usize, KeyboardEvent)>,
438) -> Element {
439 let count = if range {
440 ratios.len()
441 } else {
442 1.min(ratios.len())
443 };
444 let iter = ratios.iter().zip(values.iter()).take(count).enumerate();
445 rsx! {
446 Fragment {
447 for (idx, (ratio, value_now)) in iter {
448 button {
449 class: "adui-slider-handle",
450 role: "slider",
451 tabindex: 0,
452 aria_disabled: disabled,
453 aria_valuemin: math.min,
454 aria_valuemax: math.max,
455 aria_valuenow: *value_now,
456 style: "{handle_position_style(*ratio, math.orientation)}",
457 onkeydown: { let cb = on_key.clone(); move |evt| cb(idx, evt) },
458 }
459 }
460 }
461 }
462}
463
464fn track_range_style(ratios: &[f64], orientation: SliderOrientation) -> String {
465 let (start, end) = if ratios.len() >= 2 {
466 let a = ratios[0];
467 let b = ratios[1];
468 (a.min(b), a.max(b))
469 } else {
470 (0.0, *ratios.get(0).unwrap_or(&0.0))
471 };
472 let start_pct = start * 100.0;
473 let length_pct = (end - start).abs() * 100.0;
474 match orientation {
475 SliderOrientation::Horizontal => {
476 format!("left:{start_pct:.2}%;width:{length_pct:.2}%;")
477 }
478 SliderOrientation::Vertical => {
479 format!("bottom:{start_pct:.2}%;height:{length_pct:.2}%;")
480 }
481 }
482}
483
484fn handle_position_style(ratio: f64, orientation: SliderOrientation) -> String {
485 let pct = (ratio * 100.0).clamp(0.0, 100.0);
486 match orientation {
487 SliderOrientation::Horizontal => format!("left:{pct:.2}%;"),
488 SliderOrientation::Vertical => format!("bottom:{pct:.2}%;"),
489 }
490}
491
492fn normalize_value(range: bool, value: SliderValue, math: &SliderMath) -> SliderValue {
493 let mut normalized = match value {
494 SliderValue::Single(v) => SliderValue::Single(snap_value(v, math)),
495 SliderValue::Range(a, b) => {
496 let a = snap_value(a, math);
497 let b = snap_value(b, math);
498 SliderValue::Range(a.min(b), a.max(b))
499 }
500 };
501
502 if range {
503 normalized = normalized.ensure_range();
504 } else {
505 normalized = SliderValue::Single(normalized.as_single());
506 }
507 normalized
508}
509
510fn default_slider_value(range: bool, math: &SliderMath) -> SliderValue {
511 if range {
512 SliderValue::Range(math.min, math.max)
513 } else {
514 SliderValue::Single(math.min)
515 }
516}
517
518#[allow(dead_code)]
519fn choose_handle(current: &SliderValue, target: f64) -> usize {
520 match current {
521 SliderValue::Single(_) => 0,
522 SliderValue::Range(a, b) => {
523 let dist_a = (target - *a).abs();
524 let dist_b = (target - *b).abs();
525 if dist_a <= dist_b { 0 } else { 1 }
526 }
527 }
528}
529
530fn update_handle_value(
531 current: &SliderValue,
532 handle_idx: usize,
533 target: f64,
534 range: bool,
535 math: &SliderMath,
536) -> SliderValue {
537 let next = match (range, current) {
538 (false, _) => SliderValue::Single(target),
539 (true, SliderValue::Range(start, end)) => {
540 if handle_idx == 0 {
541 SliderValue::Range(target, *end)
542 } else {
543 SliderValue::Range(*start, target)
544 }
545 }
546 (true, SliderValue::Single(v)) => {
547 if handle_idx == 0 {
548 SliderValue::Range(target, *v)
549 } else {
550 SliderValue::Range(*v, target)
551 }
552 }
553 };
554 normalize_value(range, next, math)
555}
556
557fn slider_value_from_form(val: Option<Value>, range: bool) -> Option<SliderValue> {
558 if range {
559 match val {
560 Some(Value::Array(items)) if items.len() >= 2 => {
561 let first = items.get(0).and_then(|v| v.as_f64())?;
562 let second = items.get(1).and_then(|v| v.as_f64())?;
563 Some(SliderValue::Range(first, second))
564 }
565 Some(Value::Number(n)) => n.as_f64().map(|v| SliderValue::Range(v, v)),
566 _ => None,
567 }
568 } else {
569 match val {
570 Some(Value::Number(n)) => n.as_f64().map(SliderValue::Single),
571 Some(Value::Array(items)) if !items.is_empty() => items
572 .get(0)
573 .and_then(|v| v.as_f64())
574 .map(SliderValue::Single),
575 Some(Value::String(s)) => s.parse::<f64>().ok().map(SliderValue::Single),
576 _ => None,
577 }
578 }
579}
580
581fn slider_value_to_form(value: &SliderValue) -> Value {
582 match value {
583 SliderValue::Single(v) => Number::from_f64(*v)
584 .map(Value::Number)
585 .unwrap_or(Value::Null),
586 SliderValue::Range(a, b) => Value::Array(vec![
587 Number::from_f64(*a)
588 .map(Value::Number)
589 .unwrap_or(Value::Null),
590 Number::from_f64(*b)
591 .map(Value::Number)
592 .unwrap_or(Value::Null),
593 ]),
594 }
595}
596
597#[cfg(target_arch = "wasm32")]
598#[allow(dead_code)]
599fn pointer_ratio(
600 evt: &web_sys::PointerEvent,
601 rect: &web_sys::DomRect,
602 math: &SliderMath,
603) -> Option<f64> {
604 ratio_from_pointer_event(evt, rect, math)
605}
606
607#[cfg(not(target_arch = "wasm32"))]
608#[allow(dead_code)]
609fn pointer_ratio(
610 _evt: &web_sys::PointerEvent,
611 _rect: &web_sys::DomRect,
612 _math: &SliderMath,
613) -> Option<f64> {
614 None
615}
616
617#[cfg(test)]
618mod tests {
619 use super::*;
620
621 #[test]
622 fn normalize_single_respects_bounds() {
623 let math = SliderMath {
624 min: 0.0,
625 max: 10.0,
626 step: Some(1.0),
627 precision: None,
628 reverse: false,
629 orientation: SliderOrientation::Horizontal,
630 };
631 let val = normalize_value(false, SliderValue::Single(11.2), &math);
632 assert_eq!(val.as_single(), 10.0);
633 }
634
635 #[test]
636 fn normalize_single_below_min() {
637 let math = SliderMath {
638 min: 5.0,
639 max: 10.0,
640 step: None,
641 precision: None,
642 reverse: false,
643 orientation: SliderOrientation::Horizontal,
644 };
645 let val = normalize_value(false, SliderValue::Single(3.0), &math);
646 assert_eq!(val.as_single(), 5.0);
647 }
648
649 #[test]
650 fn normalize_range_orders_and_snaps() {
651 let math = SliderMath {
652 min: 0.0,
653 max: 5.0,
654 step: Some(0.5),
655 precision: Some(1),
656 reverse: false,
657 orientation: SliderOrientation::Horizontal,
658 };
659 let val = normalize_value(true, SliderValue::Range(3.3, 1.0), &math);
660 assert_eq!(val.as_range(), (1.0, 3.5));
661 }
662
663 #[test]
664 fn normalize_range_out_of_bounds() {
665 let math = SliderMath {
666 min: 0.0,
667 max: 10.0,
668 step: Some(1.0),
669 precision: None,
670 reverse: false,
671 orientation: SliderOrientation::Horizontal,
672 };
673 let val = normalize_value(true, SliderValue::Range(-5.0, 15.0), &math);
674 let (start, end) = val.as_range();
675 assert_eq!(start, 0.0);
676 assert_eq!(end, 10.0);
677 }
678
679 #[test]
680 fn normalize_single_with_step() {
681 let math = SliderMath {
682 min: 0.0,
683 max: 10.0,
684 step: Some(2.0),
685 precision: None,
686 reverse: false,
687 orientation: SliderOrientation::Horizontal,
688 };
689 let val = normalize_value(false, SliderValue::Single(7.3), &math);
690 assert_eq!(val.as_single(), 8.0);
691 }
692
693 #[test]
694 fn normalize_single_with_precision() {
695 let math = SliderMath {
696 min: 0.0,
697 max: 10.0,
698 step: None,
699 precision: Some(2),
700 reverse: false,
701 orientation: SliderOrientation::Horizontal,
702 };
703 let val = normalize_value(false, SliderValue::Single(3.456789), &math);
704 assert_eq!(val.as_single(), 3.46);
705 }
706
707 #[test]
708 fn choose_handle_picks_nearest() {
709 let val = SliderValue::Range(10.0, 20.0);
710 assert_eq!(choose_handle(&val, 12.0), 0);
711 assert_eq!(choose_handle(&val, 18.0), 1);
712 }
713
714 #[test]
715 fn choose_handle_equal_distance() {
716 let val = SliderValue::Range(10.0, 20.0);
717 assert_eq!(choose_handle(&val, 15.0), 0);
718 }
719
720 #[test]
721 fn choose_handle_single_value() {
722 let val = SliderValue::Single(15.0);
723 assert_eq!(choose_handle(&val, 20.0), 0);
724 }
725
726 #[test]
727 fn slider_value_as_single() {
728 let single = SliderValue::Single(5.0);
729 assert_eq!(single.as_single(), 5.0);
730
731 let range = SliderValue::Range(10.0, 20.0);
732 assert_eq!(range.as_single(), 20.0);
733 }
734
735 #[test]
736 fn slider_value_as_range() {
737 let single = SliderValue::Single(5.0);
738 assert_eq!(single.as_range(), (5.0, 5.0));
739
740 let range = SliderValue::Range(10.0, 20.0);
741 assert_eq!(range.as_range(), (10.0, 20.0));
742 }
743
744 #[test]
745 fn slider_value_ensure_range() {
746 let single = SliderValue::Single(5.0);
747 let range = single.ensure_range();
748 assert_eq!(range.as_range(), (5.0, 5.0));
749
750 let reversed = SliderValue::Range(20.0, 10.0);
751 let fixed = reversed.ensure_range();
752 assert_eq!(fixed.as_range(), (10.0, 20.0));
753 }
754
755 #[test]
756 fn default_slider_value_single() {
757 let math = SliderMath {
758 min: 0.0,
759 max: 100.0,
760 step: None,
761 precision: None,
762 reverse: false,
763 orientation: SliderOrientation::Horizontal,
764 };
765 let val = default_slider_value(false, &math);
766 assert_eq!(val.as_single(), 0.0);
767 }
768
769 #[test]
770 fn default_slider_value_range() {
771 let math = SliderMath {
772 min: 0.0,
773 max: 100.0,
774 step: None,
775 precision: None,
776 reverse: false,
777 orientation: SliderOrientation::Horizontal,
778 };
779 let val = default_slider_value(true, &math);
780 assert_eq!(val.as_range(), (0.0, 100.0));
781 }
782}