1#![allow(non_snake_case)]
2
3use std::rc::Rc;
4use std::sync::atomic::{AtomicU64, Ordering};
5
6use repose_core::*;
7use repose_ui::Box;
8
9use super::*;
10
11#[derive(Clone)]
13pub struct SliderConfig {
14 pub modifier: Modifier,
16 pub enabled: bool,
18 pub active_track_color: Color,
19 pub inactive_track_color: Color,
20 pub thumb_color: Color,
21 pub active_tick_color: Color,
22 pub inactive_tick_color: Color,
23 pub disabled_thumb_color: Color,
24 pub disabled_active_track_color: Color,
25 pub disabled_inactive_track_color: Color,
26 pub disabled_active_tick_color: Color,
27 pub disabled_inactive_tick_color: Color,
28 pub state_colors: StateColors,
29 pub on_value_change_finished: Option<Rc<dyn Fn()>>,
30 pub interaction_source: Option<MutableInteractionSource>,
31}
32
33impl std::fmt::Debug for SliderConfig {
34 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35 f.debug_struct("SliderConfig")
36 .field("modifier", &self.modifier)
37 .field("enabled", &self.enabled)
38 .field("active_track_color", &self.active_track_color)
39 .field("inactive_track_color", &self.inactive_track_color)
40 .field("thumb_color", &self.thumb_color)
41 .field("active_tick_color", &self.active_tick_color)
42 .field("inactive_tick_color", &self.inactive_tick_color)
43 .field("disabled_thumb_color", &self.disabled_thumb_color)
44 .field(
45 "disabled_active_track_color",
46 &self.disabled_active_track_color,
47 )
48 .field(
49 "disabled_inactive_track_color",
50 &self.disabled_inactive_track_color,
51 )
52 .field(
53 "disabled_active_tick_color",
54 &self.disabled_active_tick_color,
55 )
56 .field(
57 "disabled_inactive_tick_color",
58 &self.disabled_inactive_tick_color,
59 )
60 .field("state_colors", &self.state_colors)
61 .field(
62 "on_value_change_finished",
63 &self.on_value_change_finished.as_ref().map(|_| ".."),
64 )
65 .field(
66 "interaction_source",
67 &self.interaction_source.as_ref().map(|_| ".."),
68 )
69 .finish()
70 }
71}
72
73impl Default for SliderConfig {
74 fn default() -> Self {
75 Self {
76 modifier: Modifier::new(),
77 enabled: true,
78 active_track_color: SliderDefaults::active_track_color(),
79 inactive_track_color: SliderDefaults::inactive_track_color(),
80 thumb_color: SliderDefaults::thumb_color(),
81 active_tick_color: SliderDefaults::active_tick_color(),
82 inactive_tick_color: SliderDefaults::inactive_tick_color(),
83 disabled_thumb_color: SliderDefaults::disabled_thumb_color(),
84 disabled_active_track_color: SliderDefaults::disabled_active_track_color(),
85 disabled_inactive_track_color: SliderDefaults::disabled_inactive_track_color(),
86 disabled_active_tick_color: SliderDefaults::disabled_active_tick_color(),
87 disabled_inactive_tick_color: SliderDefaults::disabled_inactive_tick_color(),
88 state_colors: SliderDefaults::state_colors_default(),
89 on_value_change_finished: None,
90 interaction_source: None,
91 }
92 }
93}
94
95static SLIDER_COUNTER: AtomicU64 = AtomicU64::new(0);
96
97fn snap_step(v: f32, min: f32, max: f32, step: Option<f32>) -> f32 {
98 let v = v.clamp(min, max);
99 if let Some(s) = step.filter(|s| *s > 0.0) {
100 let t = ((v - min) / s).round();
101 (min + t * s).clamp(min, max)
102 } else {
103 v
104 }
105}
106
107fn value_from_x(x: f32, rect: Rect, min: f32, max: f32, step: Option<f32>) -> f32 {
108 let w = rect.w.max(1.0);
109 let t = ((x - rect.x) / w).clamp(0.0, 1.0);
110 let v = min + t * (max - min);
111 snap_step(v, min, max, step)
112}
113
114pub fn Slider(
115 value: f32,
116 range: (f32, f32),
117 step: Option<f32>,
118 on_change: impl Fn(f32) + 'static,
119 config: SliderConfig,
120) -> View {
121 assert!(range.0 <= range.1, "Slider range start must be <= end");
122 if let Some(s) = step {
123 assert!(s > 0.0, "Slider step must be positive");
124 }
125 let id = *remember(|| SLIDER_COUNTER.fetch_add(1, Ordering::Relaxed));
126 let track_rect = remember_state_with_key(format!("ms_rect_{}", id), Rect::default);
127 let drag_active = remember_mutable_with_key(format!("ms_da_{}", id), || false);
128 let hovered = remember(|| Signal::new(false));
129
130 let track_rect_p = track_rect.clone();
131 let drag_active_p = drag_active.clone();
132 let hovered_sig = hovered.clone();
133 let sc = config.state_colors;
134
135 let min = range.0;
136 let max = range.1;
137 let oc = Rc::new(on_change);
138 let range_size = (max - min).max(1e-6);
139 let t = ((value - min) / range_size).clamp(0.0, 1.0);
140
141 let tick_frac: Vec<f32> = if let Some(s) = step {
142 let n = ((max - min) / s.max(1e-6)).round() as usize;
143 (0..=n).map(|i| i as f32 / n as f32).collect()
144 } else {
145 Vec::new()
146 };
147
148 let sl_source: Rc<MutableInteractionSource> = config
149 .interaction_source
150 .clone()
151 .map(Rc::new)
152 .unwrap_or_else(|| remember(MutableInteractionSource::new));
153 Box(Modifier::new()
154 .min_width(200.0)
155 .height(44.0)
156 .interaction_source(&sl_source)
157 .painter(move |scene: &mut Scene, rect: Rect, alpha: f32| {
158 let mul_c = |c: Color| {
159 Color(
160 c.0,
161 c.1,
162 c.2,
163 ((c.3 as f32) * alpha).clamp(0.0, 255.0) as u8,
164 )
165 };
166 let track_h = dp_to_px(16.0);
167 let thumb_w = dp_to_px(4.0);
168 let thumb_h = dp_to_px(44.0);
169 let dot_r = dp_to_px(2.0);
170 let corner = track_h * 0.5;
171 let gap = thumb_w * 0.5 + dp_to_px(ProgressIndicatorDefaults::SLIDER_THUMB_TRACK_GAP);
172 let pad = thumb_w * 0.5;
173 let track_x = rect.x + pad;
174 let track_w = (rect.w - thumb_w).max(0.0);
175 let cy = rect.y + rect.h * 0.5;
176
177 let kx = if step.is_some() && !tick_frac.is_empty() {
178 let is_first = (t - tick_frac[0]).abs() < 1e-6;
179 let is_last = (t - tick_frac[tick_frac.len() - 1]).abs() < 1e-6;
180 if is_first || is_last {
181 track_x + t * track_w
182 } else {
183 track_x + (track_w - track_h) * t + corner
184 }
185 } else {
186 track_x + t * track_w
187 };
188
189 *track_rect_p.borrow_mut() = Rect {
190 x: track_x,
191 y: rect.y,
192 w: track_w,
193 h: rect.h,
194 };
195
196 let inactive_x = track_x.max(kx + gap);
197 let inactive_w = (track_x + track_w - inactive_x).max(0.0);
198 if inactive_w > 0.0 {
199 scene.nodes.push(SceneNode::Rect {
200 rect: Rect {
201 x: inactive_x,
202 y: cy - track_h * 0.5,
203 w: inactive_w,
204 h: track_h,
205 },
206 brush: Brush::Solid(mul_c(config.inactive_track_color)),
207 radius: [corner; 4],
208 });
209 let sx = track_x + track_w - corner;
210 scene.nodes.push(SceneNode::Ellipse {
211 rect: Rect {
212 x: sx - dot_r,
213 y: cy - dot_r,
214 w: dot_r * 2.0,
215 h: dot_r * 2.0,
216 },
217 brush: Brush::Solid(mul_c(config.inactive_tick_color)),
218 });
219 }
220 let fill_w = (kx - gap - track_x).max(0.0);
221 if fill_w > 0.0 {
222 scene.nodes.push(SceneNode::Rect {
223 rect: Rect {
224 x: track_x,
225 y: cy - track_h * 0.5,
226 w: fill_w,
227 h: track_h,
228 },
229 brush: Brush::Solid(mul_c(config.active_track_color)),
230 radius: [corner; 4],
231 });
232 }
233 let tick_start = track_x + corner;
234 let tick_end = track_x + track_w - corner;
235 for (i, &tf) in tick_frac.iter().enumerate() {
236 let tx = tick_start + tf * (tick_end - tick_start);
237 if i == tick_frac.len() - 1 {
239 continue;
240 }
241 if tx >= kx - gap && tx <= kx + gap {
242 continue;
243 }
244 let on_active = tx <= kx - gap;
245 scene.nodes.push(SceneNode::Ellipse {
246 rect: Rect {
247 x: tx - dot_r,
248 y: cy - dot_r,
249 w: dot_r * 2.0,
250 h: dot_r * 2.0,
251 },
252 brush: Brush::Solid(mul_c(if on_active {
253 config.active_tick_color
254 } else {
255 config.inactive_tick_color
256 })),
257 });
258 }
259 let da = *drag_active_p.get();
260 let hv = hovered_sig.get();
261 let tw = if da { thumb_w * 0.5 } else { thumb_w };
262 scene.nodes.push(SceneNode::Rect {
263 rect: Rect {
264 x: kx - tw * 0.5,
265 y: cy - thumb_h * 0.5,
266 w: tw,
267 h: thumb_h,
268 },
269 brush: Brush::Solid(mul_c(config.thumb_color)),
270 radius: [tw * 0.5; 4],
271 });
272 let sc_target = if da {
273 sc.pressed
274 } else if hv {
275 sc.hovered
276 } else {
277 sc.default
278 };
279 if sc_target.3 > 0 {
280 scene.nodes.push(SceneNode::Rect {
281 rect: Rect {
282 x: kx - tw * 0.5,
283 y: cy - thumb_h * 0.5,
284 w: tw,
285 h: thumb_h,
286 },
287 brush: Brush::Solid(mul_c(sc_target)),
288 radius: [tw * 0.5; 4],
289 });
290 }
291 })
292 .on_pointer_enter({
293 let h = hovered.clone();
294 move |_pe: PointerEvent| h.set(true)
295 })
296 .on_pointer_leave({
297 let h = hovered.clone();
298 move |_pe: PointerEvent| h.set(false)
299 })
300 .on_pointer_down({
301 let oc = oc.clone();
302 let track_rect = track_rect.clone();
303 let drag_active = drag_active.clone();
304 move |pe: PointerEvent| {
305 drag_active.set(true);
306 let r = *track_rect.borrow();
307 (oc)(value_from_x(pe.position.x, r, min, max, step));
308 }
309 })
310 .on_pointer_move({
311 let oc = oc.clone();
312 let track_rect = track_rect.clone();
313 let drag_active = drag_active.clone();
314 move |pe: PointerEvent| {
315 if !*drag_active.get() {
316 return;
317 }
318 let r = *track_rect.borrow();
319 (oc)(value_from_x(pe.position.x, r, min, max, step));
320 }
321 })
322 .on_pointer_up({
323 let on_finished = config.on_value_change_finished.clone();
324 move |_pe: PointerEvent| {
325 drag_active.set(false);
326 if let Some(ref cb) = on_finished {
327 (cb)();
328 }
329 }
330 })
331 .on_scroll({
332 let oc = oc.clone();
333 move |d: Vec2| -> Vec2 {
334 let dir = if d.y < -0.5 {
335 1
336 } else if d.y > 0.5 {
337 -1
338 } else {
339 0
340 };
341 if dir == 0 {
342 return d;
343 }
344 let step_val = step.unwrap_or(1.0).max(1e-6);
345 let new_val = snap_step(value + (dir as f32) * step_val, min, max, step);
346 if (new_val - value).abs() > 1e-6 {
347 (oc)(new_val);
348 Vec2 { x: d.x, y: 0.0 }
349 } else {
350 d
351 }
352 }
353 })
354 .then(config.modifier))
355 .semantics(Semantics {
356 role: Role::Slider,
357 label: None,
358 focused: false,
359 enabled: true,
360 selectable_group: false,
361 })
362}
363
364pub fn RangeSlider(
365 start: f32,
366 end: f32,
367 range: (f32, f32),
368 step: Option<f32>,
369 on_change: impl Fn(f32, f32) + 'static,
370 config: SliderConfig,
371) -> View {
372 assert!(range.0 <= range.1, "Slider range start must be <= end");
373 if let Some(s) = step {
374 assert!(s > 0.0, "Slider step must be positive");
375 }
376 let id = *remember(|| SLIDER_COUNTER.fetch_add(1, Ordering::Relaxed));
377 let track_rect = remember_state_with_key(format!("mrs_rect_{}", id), Rect::default);
378 let drag_active = remember_mutable_with_key(format!("mrs_da_{}", id), || false);
379 let active_thumb = remember_mutable_with_key(format!("mrs_at_{}", id), || false);
380 let hovered = remember(|| Signal::new(false));
381
382 let min = range.0;
383 let max = range.1;
384 let oc = Rc::new(on_change);
385 let range_size = (max - min).max(1e-6);
386 let t0 = ((start - min) / range_size).clamp(0.0, 1.0);
387 let t1 = ((end - min) / range_size).clamp(0.0, 1.0);
388 let sc = config.state_colors;
389 let is_enabled = config.enabled;
390
391 let act_trk = if !is_enabled {
392 config.disabled_active_track_color
393 } else {
394 config.active_track_color
395 };
396 let inact_trk = if !is_enabled {
397 config.disabled_inactive_track_color
398 } else {
399 config.inactive_track_color
400 };
401 let act_tick = if !is_enabled {
402 config.disabled_active_tick_color
403 } else {
404 config.active_tick_color
405 };
406 let inact_tick = if !is_enabled {
407 config.disabled_inactive_tick_color
408 } else {
409 config.inactive_tick_color
410 };
411 let thumb_col = if !is_enabled {
412 config.disabled_thumb_color
413 } else {
414 config.thumb_color
415 };
416
417 let tick_frac: Vec<f32> = if let Some(s) = step {
418 let n = ((max - min) / s.max(1e-6)).round() as usize;
419 (0..=n).map(|i| i as f32 / n as f32).collect()
420 } else {
421 Vec::new()
422 };
423
424 let track_rect_p = track_rect.clone();
425 let drag_active_p = drag_active.clone();
426 let active_thumb_p = active_thumb.clone();
427 let hovered_sig = hovered.clone();
428
429 Box(Modifier::new()
430 .min_width(200.0)
431 .height(44.0)
432 .painter(move |scene: &mut Scene, rect: Rect, alpha: f32| {
433 let mul_c = |c: Color| {
434 Color(
435 c.0,
436 c.1,
437 c.2,
438 ((c.3 as f32) * alpha).clamp(0.0, 255.0) as u8,
439 )
440 };
441 let track_h = dp_to_px(16.0);
442 let thumb_w = dp_to_px(4.0);
443 let thumb_h = dp_to_px(44.0);
444 let dot_r = dp_to_px(2.0);
445 let corner = track_h * 0.5;
446 let gap = thumb_w * 0.5 + dp_to_px(ProgressIndicatorDefaults::SLIDER_THUMB_TRACK_GAP);
447 let pad = thumb_w * 0.5;
448 let track_x = rect.x + pad;
449 let track_w = (rect.w - thumb_w).max(0.0);
450 let cy = rect.y + rect.h * 0.5;
451
452 let thumb_pos = |tf: f32, fracs: &[f32]| {
453 if step.is_some() && !fracs.is_empty() {
454 let is_first = (tf - fracs[0]).abs() < 1e-6;
455 let is_last = (tf - fracs[fracs.len() - 1]).abs() < 1e-6;
456 if is_first || is_last {
457 track_x + tf * track_w
458 } else {
459 track_x + (track_w - track_h) * tf + corner
460 }
461 } else {
462 track_x + tf * track_w
463 }
464 };
465 let k0 = thumb_pos(t0, &tick_frac);
466 let k1 = thumb_pos(t1, &tick_frac);
467 let active_l = k0.min(k1);
468 let active_r = k0.max(k1);
469
470 *track_rect_p.borrow_mut() = Rect {
471 x: track_x,
472 y: rect.y,
473 w: track_w,
474 h: rect.h,
475 };
476
477 let linactive_w = (active_l - gap - track_x).max(0.0);
478 if linactive_w > 0.0 {
479 scene.nodes.push(SceneNode::Rect {
480 rect: Rect {
481 x: track_x,
482 y: cy - track_h * 0.5,
483 w: linactive_w,
484 h: track_h,
485 },
486 brush: Brush::Solid(mul_c(inact_trk)),
487 radius: [corner; 4],
488 });
489 let sx0 = track_x + corner;
490 scene.nodes.push(SceneNode::Ellipse {
491 rect: Rect {
492 x: sx0 - dot_r,
493 y: cy - dot_r,
494 w: dot_r * 2.0,
495 h: dot_r * 2.0,
496 },
497 brush: Brush::Solid(mul_c(inact_tick)),
498 });
499 }
500 let rinactive_x = (active_r + gap).min(track_x + track_w);
501 let rinactive_w = (track_x + track_w - rinactive_x).max(0.0);
502 if rinactive_w > 0.0 {
503 scene.nodes.push(SceneNode::Rect {
504 rect: Rect {
505 x: rinactive_x,
506 y: cy - track_h * 0.5,
507 w: rinactive_w,
508 h: track_h,
509 },
510 brush: Brush::Solid(mul_c(inact_trk)),
511 radius: [corner; 4],
512 });
513 let sx = track_x + track_w - corner;
514 scene.nodes.push(SceneNode::Ellipse {
515 rect: Rect {
516 x: sx - dot_r,
517 y: cy - dot_r,
518 w: dot_r * 2.0,
519 h: dot_r * 2.0,
520 },
521 brush: Brush::Solid(mul_c(inact_tick)),
522 });
523 }
524 let active_w = (active_r - gap - (active_l + gap)).max(0.0);
525 if active_w > 0.0 {
526 scene.nodes.push(SceneNode::Rect {
527 rect: Rect {
528 x: active_l + gap,
529 y: cy - track_h * 0.5,
530 w: active_w,
531 h: track_h,
532 },
533 brush: Brush::Solid(mul_c(act_trk)),
534 radius: [corner; 4],
535 });
536 }
537 let tick_start = track_x + corner;
538 let tick_end = track_x + track_w - corner;
539 for (i, &tf) in tick_frac.iter().enumerate() {
540 let tx = tick_start + tf * (tick_end - tick_start);
541 if i == 0 || i == tick_frac.len() - 1 {
543 continue;
544 }
545 let in_lgap = tx >= active_l - gap && tx <= active_l + gap;
546 let in_rgap = tx >= active_r - gap && tx <= active_r + gap;
547 if in_lgap || in_rgap {
548 continue;
549 }
550 let on_active = tx >= active_l + gap && tx <= active_r - gap;
551 scene.nodes.push(SceneNode::Ellipse {
552 rect: Rect {
553 x: tx - dot_r,
554 y: cy - dot_r,
555 w: dot_r * 2.0,
556 h: dot_r * 2.0,
557 },
558 brush: Brush::Solid(mul_c(if on_active { act_tick } else { inact_tick })),
559 });
560 }
561 let da = *drag_active_p.get();
562 let at = *active_thumb_p.get();
563 let hv = hovered_sig.get();
564 let thumbs = [k0, k1];
565 for (idx, &kx) in thumbs.iter().enumerate() {
566 let is_active = da && (if idx == 0 { !at } else { at });
567 let tw = if is_active { thumb_w * 0.5 } else { thumb_w };
568 scene.nodes.push(SceneNode::Rect {
569 rect: Rect {
570 x: kx - tw * 0.5,
571 y: cy - thumb_h * 0.5,
572 w: tw,
573 h: thumb_h,
574 },
575 brush: Brush::Solid(mul_c(thumb_col)),
576 radius: [tw * 0.5; 4],
577 });
578 let sc_target = if !is_enabled {
579 Color::TRANSPARENT
580 } else if is_active {
581 sc.pressed
582 } else if hv {
583 sc.hovered
584 } else {
585 sc.default
586 };
587 if sc_target.3 > 0 {
588 scene.nodes.push(SceneNode::Rect {
589 rect: Rect {
590 x: kx - tw * 0.5,
591 y: cy - thumb_h * 0.5,
592 w: tw,
593 h: thumb_h,
594 },
595 brush: Brush::Solid(mul_c(sc_target)),
596 radius: [tw * 0.5; 4],
597 });
598 }
599 }
600 })
601 .on_pointer_enter({
602 let h = hovered.clone();
603 let en = is_enabled;
604 move |_pe: PointerEvent| {
605 if en {
606 h.set(true);
607 }
608 }
609 })
610 .on_pointer_leave({
611 let h = hovered.clone();
612 move |_pe: PointerEvent| h.set(false)
613 })
614 .on_pointer_down({
615 let oc = oc.clone();
616 let track_rect = track_rect.clone();
617 let drag_active = drag_active.clone();
618 let active_thumb = active_thumb.clone();
619 let en = is_enabled;
620 move |pe: PointerEvent| {
621 if !en {
622 return;
623 }
624 drag_active.set(true);
625 let r = *track_rect.borrow();
626 let v = value_from_x(pe.position.x, r, min, max, step);
627 let use_end = (v - end).abs() < (v - start).abs();
628 active_thumb.set(use_end);
629 let (a, b) = if use_end {
630 (start, v.max(start))
631 } else {
632 (v.min(end), end)
633 };
634 (oc)(a, b);
635 }
636 })
637 .on_pointer_move({
638 let oc = oc.clone();
639 let track_rect = track_rect.clone();
640 let drag_active = drag_active.clone();
641 let active_thumb = active_thumb.clone();
642 move |pe: PointerEvent| {
643 if !*drag_active.get() {
644 return;
645 }
646 let r = *track_rect.borrow();
647 let v = value_from_x(pe.position.x, r, min, max, step);
648 let use_end = *active_thumb.get();
649 let (a, b) = if use_end {
650 (start, v.max(start))
651 } else {
652 (v.min(end), end)
653 };
654 (oc)(a, b);
655 }
656 })
657 .on_pointer_up({
658 let drag_active = drag_active.clone();
659 let active_thumb = active_thumb.clone();
660 move |_pe: PointerEvent| {
661 drag_active.set(false);
662 active_thumb.set(false);
663 }
664 })
665 .on_scroll({
666 let oc = oc.clone();
667 let active_thumb = active_thumb.clone();
668 let en = is_enabled;
669 move |d: Vec2| -> Vec2 {
670 if !en {
671 return d;
672 }
673 let dir = if d.y < -0.5 {
674 1
675 } else if d.y > 0.5 {
676 -1
677 } else {
678 0
679 };
680 if dir == 0 {
681 return d;
682 }
683 let step_val = step.unwrap_or(1.0).max(1e-6);
684 let use_end = *active_thumb.get();
685 let (mut a, mut b) = (start, end);
686 if use_end {
687 b = snap_step(end + (dir as f32) * step_val, min, max, step).max(a);
688 } else {
689 a = snap_step(start + (dir as f32) * step_val, min, max, step).min(b);
690 }
691 if (a - start).abs() > 1e-6 || (b - end).abs() > 1e-6 {
692 (oc)(a, b);
693 Vec2 { x: d.x, y: 0.0 }
694 } else {
695 d
696 }
697 }
698 })
699 .then(config.modifier))
700 .semantics(Semantics {
701 role: Role::Slider,
702 label: None,
703 focused: false,
704 enabled: is_enabled,
705 selectable_group: false,
706 })
707}