Skip to main content

repose_material/material3/
slider.rs

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/// Configuration for [`Slider`] and [`RangeSlider`].
12#[derive(Clone)]
13pub struct SliderConfig {
14    // Debug impl is manual because on_value_change_finished contains a closure
15    pub modifier: Modifier,
16    /// When false, renders disabled colors and does not respond to input.
17    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                // skip ticks that fall on the stop indicator (last)
238                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                // skip ticks that fall on the stop indicators (first and last)
542                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}