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
129    let track_rect_p = track_rect.clone();
130    let drag_active_p = drag_active.clone();
131
132    let min = range.0;
133    let max = range.1;
134    let oc = Rc::new(on_change);
135    let range_size = (max - min).max(1e-6);
136    let t = ((value - min) / range_size).clamp(0.0, 1.0);
137
138    let is_enabled = config.enabled;
139
140    let act_trk = if !is_enabled {
141        config.disabled_active_track_color
142    } else {
143        config.active_track_color
144    };
145    let inact_trk = if !is_enabled {
146        config.disabled_inactive_track_color
147    } else {
148        config.inactive_track_color
149    };
150    let act_tick = if !is_enabled {
151        config.disabled_active_tick_color
152    } else {
153        config.active_tick_color
154    };
155    let inact_tick = if !is_enabled {
156        config.disabled_inactive_tick_color
157    } else {
158        config.inactive_tick_color
159    };
160    let thumb_col = if !is_enabled {
161        config.disabled_thumb_color
162    } else {
163        config.thumb_color
164    };
165
166    let tick_frac: Vec<f32> = if let Some(s) = step {
167        let n = ((max - min) / s.max(1e-6)).round() as usize;
168        (0..=n).map(|i| i as f32 / n as f32).collect()
169    } else {
170        Vec::new()
171    };
172
173    let sl_source: Rc<MutableInteractionSource> = config
174        .interaction_source
175        .clone()
176        .map(Rc::new)
177        .unwrap_or_else(|| remember(MutableInteractionSource::new));
178    let mut host = Modifier::new()
179        .min_width(200.0)
180        .height(44.0)
181        .interaction_source(&sl_source);
182    if !is_enabled {
183        host = host.enabled(false);
184    }
185    host = host.focusable(true);
186    Box(host
187        .painter(move |scene: &mut Scene, rect: Rect, alpha: f32| {
188            let mul_c = |c: Color| {
189                Color(
190                    c.0,
191                    c.1,
192                    c.2,
193                    ((c.3 as f32) * alpha).clamp(0.0, 255.0) as u8,
194                )
195            };
196            let track_h = dp_to_px(SliderDefaults::TRACK_HEIGHT);
197            let thumb_w = dp_to_px(SliderDefaults::THUMB_WIDTH);
198            let thumb_h = dp_to_px(SliderDefaults::THUMB_HEIGHT);
199            let dot_r = dp_to_px(2.0);
200            let corner = track_h * 0.5;
201            let gap = thumb_w * 0.5 + dp_to_px(ProgressIndicatorDefaults::SLIDER_THUMB_TRACK_GAP);
202            let pad = thumb_w * 0.5;
203            let track_x = rect.x + pad;
204            let track_w = (rect.w - thumb_w).max(0.0);
205            let cy = rect.y + rect.h * 0.5;
206
207            let kx = if step.is_some() && !tick_frac.is_empty() {
208                let is_first = (t - tick_frac[0]).abs() < 1e-6;
209                let is_last = (t - tick_frac[tick_frac.len() - 1]).abs() < 1e-6;
210                if is_first || is_last {
211                    track_x + t * track_w
212                } else {
213                    track_x + (track_w - track_h) * t + corner
214                }
215            } else {
216                track_x + t * track_w
217            };
218
219            *track_rect_p.borrow_mut() = Rect {
220                x: track_x,
221                y: rect.y,
222                w: track_w,
223                h: rect.h,
224            };
225
226            let inactive_x = track_x.max(kx + gap);
227            let inactive_w = (track_x + track_w - inactive_x).max(0.0);
228            if inactive_w > 0.0 {
229                scene.nodes.push(SceneNode::Rect {
230                    rect: Rect {
231                        x: inactive_x,
232                        y: cy - track_h * 0.5,
233                        w: inactive_w,
234                        h: track_h,
235                    },
236                    brush: Brush::Solid(mul_c(inact_trk)),
237                    radius: [corner; 4],
238                });
239                let sx = track_x + track_w - corner;
240                scene.nodes.push(SceneNode::Ellipse {
241                    rect: Rect {
242                        x: sx - dot_r,
243                        y: cy - dot_r,
244                        w: dot_r * 2.0,
245                        h: dot_r * 2.0,
246                    },
247                    brush: Brush::Solid(mul_c(inact_tick)),
248                });
249            }
250            let fill_w = (kx - gap - track_x).max(0.0);
251            if fill_w > 0.0 {
252                scene.nodes.push(SceneNode::Rect {
253                    rect: Rect {
254                        x: track_x,
255                        y: cy - track_h * 0.5,
256                        w: fill_w,
257                        h: track_h,
258                    },
259                    brush: Brush::Solid(mul_c(act_trk)),
260                    radius: [corner; 4],
261                });
262            }
263            let tick_start = track_x + corner;
264            let tick_end = track_x + track_w - corner;
265            for (i, &tf) in tick_frac.iter().enumerate() {
266                let tx = tick_start + tf * (tick_end - tick_start);
267                // skip ticks that fall on the stop indicator (last)
268                if i == tick_frac.len() - 1 {
269                    continue;
270                }
271                if tx >= kx - gap && tx <= kx + gap {
272                    continue;
273                }
274                let on_active = tx <= kx - gap;
275                scene.nodes.push(SceneNode::Ellipse {
276                    rect: Rect {
277                        x: tx - dot_r,
278                        y: cy - dot_r,
279                        w: dot_r * 2.0,
280                        h: dot_r * 2.0,
281                    },
282                    brush: Brush::Solid(mul_c(if on_active { act_tick } else { inact_tick })),
283                });
284            }
285            let da = *drag_active_p.get();
286            let tw = if da { thumb_w * 0.5 } else { thumb_w };
287            scene.nodes.push(SceneNode::Rect {
288                rect: Rect {
289                    x: kx - tw * 0.5,
290                    y: cy - thumb_h * 0.5,
291                    w: tw,
292                    h: thumb_h,
293                },
294                brush: Brush::Solid(mul_c(thumb_col)),
295                radius: [tw * 0.5; 4],
296            });
297        })
298        .on_pointer_down({
299            let oc = oc.clone();
300            let track_rect = track_rect.clone();
301            let drag_active = drag_active.clone();
302            let en = is_enabled;
303            move |pe: PointerEvent| {
304                if !en {
305                    return;
306                }
307                drag_active.set(true);
308                let r = *track_rect.borrow();
309                (oc)(value_from_x(pe.position_in_window().x, r, min, max, step));
310                pe.consume();
311            }
312        })
313        .on_pointer_move({
314            let oc = oc.clone();
315            let track_rect = track_rect.clone();
316            let drag_active = drag_active.clone();
317            let en = is_enabled;
318            move |pe: PointerEvent| {
319                if !en {
320                    return;
321                }
322                if !*drag_active.get() {
323                    return;
324                }
325                let r = *track_rect.borrow();
326                (oc)(value_from_x(pe.position_in_window().x, r, min, max, step));
327                pe.consume();
328            }
329        })
330        .on_pointer_up({
331            let drag_active = drag_active.clone();
332            let on_finished = config.on_value_change_finished.clone();
333            move |_pe: PointerEvent| {
334                let was = *drag_active.get();
335                drag_active.set(false);
336                if was {
337                    if let Some(ref cb) = on_finished {
338                        (cb)();
339                    }
340                }
341            }
342        })
343        .on_pointer_cancel({
344            let drag_active = drag_active.clone();
345            let on_finished = config.on_value_change_finished.clone();
346            move |_pe: PointerEvent| {
347                if *drag_active.get() {
348                    drag_active.set(false);
349                    if let Some(ref cb) = on_finished {
350                        (cb)();
351                    }
352                }
353            }
354        })
355        .on_scroll({
356            let oc = oc.clone();
357            let en = is_enabled;
358            move |d: Vec2| -> Vec2 {
359                if !en {
360                    return d;
361                }
362                let dir = if d.y < -0.5 {
363                    1
364                } else if d.y > 0.5 {
365                    -1
366                } else {
367                    0
368                };
369                if dir == 0 {
370                    return d;
371                }
372                let step_val = step.unwrap_or(1.0).max(1e-6);
373                let new_val = snap_step(value + (dir as f32) * step_val, min, max, step);
374                if (new_val - value).abs() > 1e-6 {
375                    (oc)(new_val);
376                    Vec2 { x: d.x, y: 0.0 }
377                } else {
378                    d
379                }
380            }
381        })
382        .on_key_event({
383            let oc = oc.clone();
384            let en = is_enabled;
385            move |ke: KeyEvent| -> bool {
386                if !en || ke.event_type != KeyEventType::Down {
387                    return false;
388                }
389                let step_val = step.unwrap_or(((max - min) / 100.0).max(1e-6)).max(1e-6);
390                let page = step_val * 10.0;
391                let new_val = match ke.key {
392                    Key::ArrowLeft | Key::ArrowDown => snap_step(value - step_val, min, max, step),
393                    Key::ArrowRight | Key::ArrowUp => snap_step(value + step_val, min, max, step),
394                    Key::PageDown => snap_step(value - page, min, max, step),
395                    Key::PageUp => snap_step(value + page, min, max, step),
396                    Key::Home => min,
397                    Key::End => max,
398                    _ => return false,
399                };
400                if (new_val - value).abs() > 1e-6 {
401                    (oc)(new_val);
402                }
403                true
404            }
405        })
406        .then(config.modifier))
407    .semantics(Semantics {
408        role: Role::Slider,
409        enabled: is_enabled,
410        value: Some(format!("{value}")),
411        ..Default::default()
412    })
413}
414
415pub fn RangeSlider(
416    start: f32,
417    end: f32,
418    range: (f32, f32),
419    step: Option<f32>,
420    on_change: impl Fn(f32, f32) + 'static,
421    config: SliderConfig,
422) -> View {
423    assert!(range.0 <= range.1, "Slider range start must be <= end");
424    if let Some(s) = step {
425        assert!(s > 0.0, "Slider step must be positive");
426    }
427    let id = *remember(|| SLIDER_COUNTER.fetch_add(1, Ordering::Relaxed));
428    let track_rect = remember_state_with_key(format!("mrs_rect_{}", id), Rect::default);
429    let drag_active = remember_mutable_with_key(format!("mrs_da_{}", id), || false);
430    let active_thumb = remember_mutable_with_key(format!("mrs_at_{}", id), || false);
431
432    let min = range.0;
433    let max = range.1;
434    let oc = Rc::new(on_change);
435    let range_size = (max - min).max(1e-6);
436    let t0 = ((start - min) / range_size).clamp(0.0, 1.0);
437    let t1 = ((end - min) / range_size).clamp(0.0, 1.0);
438    let is_enabled = config.enabled;
439
440    let act_trk = if !is_enabled {
441        config.disabled_active_track_color
442    } else {
443        config.active_track_color
444    };
445    let inact_trk = if !is_enabled {
446        config.disabled_inactive_track_color
447    } else {
448        config.inactive_track_color
449    };
450    let act_tick = if !is_enabled {
451        config.disabled_active_tick_color
452    } else {
453        config.active_tick_color
454    };
455    let inact_tick = if !is_enabled {
456        config.disabled_inactive_tick_color
457    } else {
458        config.inactive_tick_color
459    };
460    let thumb_col = if !is_enabled {
461        config.disabled_thumb_color
462    } else {
463        config.thumb_color
464    };
465
466    let tick_frac: Vec<f32> = if let Some(s) = step {
467        let n = ((max - min) / s.max(1e-6)).round() as usize;
468        (0..=n).map(|i| i as f32 / n as f32).collect()
469    } else {
470        Vec::new()
471    };
472
473    let track_rect_p = track_rect.clone();
474    let drag_active_p = drag_active.clone();
475    let active_thumb_p = active_thumb.clone();
476
477    let sl_source: Rc<MutableInteractionSource> = config
478        .interaction_source
479        .clone()
480        .map(Rc::new)
481        .unwrap_or_else(|| remember(MutableInteractionSource::new));
482    let mut host = Modifier::new()
483        .min_width(200.0)
484        .height(44.0)
485        .interaction_source(&sl_source);
486    if !is_enabled {
487        host = host.enabled(false);
488    }
489    host = host.focusable(true);
490    Box(host
491        .painter(move |scene: &mut Scene, rect: Rect, alpha: f32| {
492            let mul_c = |c: Color| {
493                Color(
494                    c.0,
495                    c.1,
496                    c.2,
497                    ((c.3 as f32) * alpha).clamp(0.0, 255.0) as u8,
498                )
499            };
500            let track_h = dp_to_px(SliderDefaults::TRACK_HEIGHT);
501            let thumb_w = dp_to_px(SliderDefaults::THUMB_WIDTH);
502            let thumb_h = dp_to_px(SliderDefaults::THUMB_HEIGHT);
503            let dot_r = dp_to_px(2.0);
504            let corner = track_h * 0.5;
505            let gap = thumb_w * 0.5 + dp_to_px(ProgressIndicatorDefaults::SLIDER_THUMB_TRACK_GAP);
506            let pad = thumb_w * 0.5;
507            let track_x = rect.x + pad;
508            let track_w = (rect.w - thumb_w).max(0.0);
509            let cy = rect.y + rect.h * 0.5;
510
511            let thumb_pos = |tf: f32, fracs: &[f32]| {
512                if step.is_some() && !fracs.is_empty() {
513                    let is_first = (tf - fracs[0]).abs() < 1e-6;
514                    let is_last = (tf - fracs[fracs.len() - 1]).abs() < 1e-6;
515                    if is_first || is_last {
516                        track_x + tf * track_w
517                    } else {
518                        track_x + (track_w - track_h) * tf + corner
519                    }
520                } else {
521                    track_x + tf * track_w
522                }
523            };
524            let k0 = thumb_pos(t0, &tick_frac);
525            let k1 = thumb_pos(t1, &tick_frac);
526            let active_l = k0.min(k1);
527            let active_r = k0.max(k1);
528
529            *track_rect_p.borrow_mut() = Rect {
530                x: track_x,
531                y: rect.y,
532                w: track_w,
533                h: rect.h,
534            };
535
536            let linactive_w = (active_l - gap - track_x).max(0.0);
537            if linactive_w > 0.0 {
538                scene.nodes.push(SceneNode::Rect {
539                    rect: Rect {
540                        x: track_x,
541                        y: cy - track_h * 0.5,
542                        w: linactive_w,
543                        h: track_h,
544                    },
545                    brush: Brush::Solid(mul_c(inact_trk)),
546                    radius: [corner; 4],
547                });
548                let sx0 = track_x + corner;
549                scene.nodes.push(SceneNode::Ellipse {
550                    rect: Rect {
551                        x: sx0 - dot_r,
552                        y: cy - dot_r,
553                        w: dot_r * 2.0,
554                        h: dot_r * 2.0,
555                    },
556                    brush: Brush::Solid(mul_c(inact_tick)),
557                });
558            }
559            let rinactive_x = (active_r + gap).min(track_x + track_w);
560            let rinactive_w = (track_x + track_w - rinactive_x).max(0.0);
561            if rinactive_w > 0.0 {
562                scene.nodes.push(SceneNode::Rect {
563                    rect: Rect {
564                        x: rinactive_x,
565                        y: cy - track_h * 0.5,
566                        w: rinactive_w,
567                        h: track_h,
568                    },
569                    brush: Brush::Solid(mul_c(inact_trk)),
570                    radius: [corner; 4],
571                });
572                let sx = track_x + track_w - corner;
573                scene.nodes.push(SceneNode::Ellipse {
574                    rect: Rect {
575                        x: sx - dot_r,
576                        y: cy - dot_r,
577                        w: dot_r * 2.0,
578                        h: dot_r * 2.0,
579                    },
580                    brush: Brush::Solid(mul_c(inact_tick)),
581                });
582            }
583            let active_w = (active_r - gap - (active_l + gap)).max(0.0);
584            if active_w > 0.0 {
585                scene.nodes.push(SceneNode::Rect {
586                    rect: Rect {
587                        x: active_l + gap,
588                        y: cy - track_h * 0.5,
589                        w: active_w,
590                        h: track_h,
591                    },
592                    brush: Brush::Solid(mul_c(act_trk)),
593                    radius: [corner; 4],
594                });
595            }
596            let tick_start = track_x + corner;
597            let tick_end = track_x + track_w - corner;
598            for (i, &tf) in tick_frac.iter().enumerate() {
599                let tx = tick_start + tf * (tick_end - tick_start);
600                // skip ticks that fall on the stop indicators (first and last)
601                if i == 0 || i == tick_frac.len() - 1 {
602                    continue;
603                }
604                let in_lgap = tx >= active_l - gap && tx <= active_l + gap;
605                let in_rgap = tx >= active_r - gap && tx <= active_r + gap;
606                if in_lgap || in_rgap {
607                    continue;
608                }
609                let on_active = tx >= active_l + gap && tx <= active_r - gap;
610                scene.nodes.push(SceneNode::Ellipse {
611                    rect: Rect {
612                        x: tx - dot_r,
613                        y: cy - dot_r,
614                        w: dot_r * 2.0,
615                        h: dot_r * 2.0,
616                    },
617                    brush: Brush::Solid(mul_c(if on_active { act_tick } else { inact_tick })),
618                });
619            }
620            let da = *drag_active_p.get();
621            let at = *active_thumb_p.get();
622            let thumbs = [k0, k1];
623            for (idx, &kx) in thumbs.iter().enumerate() {
624                let is_active = da && (if idx == 0 { !at } else { at });
625                let tw = if is_active { thumb_w * 0.5 } else { thumb_w };
626                scene.nodes.push(SceneNode::Rect {
627                    rect: Rect {
628                        x: kx - tw * 0.5,
629                        y: cy - thumb_h * 0.5,
630                        w: tw,
631                        h: thumb_h,
632                    },
633                    brush: Brush::Solid(mul_c(thumb_col)),
634                    radius: [tw * 0.5; 4],
635                });
636            }
637        })
638        .on_pointer_down({
639            let oc = oc.clone();
640            let track_rect = track_rect.clone();
641            let drag_active = drag_active.clone();
642            let active_thumb = active_thumb.clone();
643            let en = is_enabled;
644            move |pe: PointerEvent| {
645                if !en {
646                    return;
647                }
648                drag_active.set(true);
649                let r = *track_rect.borrow();
650                let v = value_from_x(pe.position_in_window().x, r, min, max, step);
651                let use_end = (v - end).abs() < (v - start).abs();
652                active_thumb.set(use_end);
653                let (a, b) = if use_end {
654                    (start, v.max(start))
655                } else {
656                    (v.min(end), end)
657                };
658                (oc)(a, b);
659                pe.consume();
660            }
661        })
662        .on_pointer_move({
663            let oc = oc.clone();
664            let track_rect = track_rect.clone();
665            let drag_active = drag_active.clone();
666            let active_thumb = active_thumb.clone();
667            let en = is_enabled;
668            move |pe: PointerEvent| {
669                if !en {
670                    return;
671                }
672                if !*drag_active.get() {
673                    return;
674                }
675                let r = *track_rect.borrow();
676                let v = value_from_x(pe.position_in_window().x, r, min, max, step);
677                let use_end = *active_thumb.get();
678                let (a, b) = if use_end {
679                    (start, v.max(start))
680                } else {
681                    (v.min(end), end)
682                };
683                (oc)(a, b);
684                pe.consume();
685            }
686        })
687        .on_pointer_up({
688            let drag_active = drag_active.clone();
689            let active_thumb = active_thumb.clone();
690            let on_finished = config.on_value_change_finished.clone();
691            move |_pe: PointerEvent| {
692                let was = *drag_active.get();
693                drag_active.set(false);
694                active_thumb.set(false);
695                if was {
696                    if let Some(ref cb) = on_finished {
697                        (cb)();
698                    }
699                }
700            }
701        })
702        .on_pointer_cancel({
703            let drag_active = drag_active.clone();
704            let active_thumb = active_thumb.clone();
705            let on_finished = config.on_value_change_finished.clone();
706            move |_pe: PointerEvent| {
707                if *drag_active.get() {
708                    drag_active.set(false);
709                    active_thumb.set(false);
710                    if let Some(ref cb) = on_finished {
711                        (cb)();
712                    }
713                }
714            }
715        })
716        .on_scroll({
717            let oc = oc.clone();
718            let active_thumb = active_thumb.clone();
719            let en = is_enabled;
720            move |d: Vec2| -> Vec2 {
721                if !en {
722                    return d;
723                }
724                let dir = if d.y < -0.5 {
725                    1
726                } else if d.y > 0.5 {
727                    -1
728                } else {
729                    0
730                };
731                if dir == 0 {
732                    return d;
733                }
734                let step_val = step.unwrap_or(1.0).max(1e-6);
735                let use_end = *active_thumb.get();
736                let (mut a, mut b) = (start, end);
737                if use_end {
738                    b = snap_step(end + (dir as f32) * step_val, min, max, step).max(a);
739                } else {
740                    a = snap_step(start + (dir as f32) * step_val, min, max, step).min(b);
741                }
742                if (a - start).abs() > 1e-6 || (b - end).abs() > 1e-6 {
743                    (oc)(a, b);
744                    Vec2 { x: d.x, y: 0.0 }
745                } else {
746                    d
747                }
748            }
749        })
750        .on_key_event({
751            let oc = oc.clone();
752            let active_thumb = active_thumb.clone();
753            let en = is_enabled;
754            move |ke: KeyEvent| -> bool {
755                if !en || ke.event_type != KeyEventType::Down {
756                    return false;
757                }
758                let step_val = step.unwrap_or(((max - min) / 100.0).max(1e-6)).max(1e-6);
759                let page = step_val * 10.0;
760                let use_end = *active_thumb.get();
761                let (a, b) = if use_end {
762                    let new_b = match ke.key {
763                        Key::ArrowLeft | Key::ArrowDown => {
764                            snap_step(end - step_val, min, max, step)
765                        }
766                        Key::ArrowRight | Key::ArrowUp => {
767                            snap_step(end + step_val, min, max, step)
768                        }
769                        Key::PageDown => snap_step(end - page, min, max, step),
770                        Key::PageUp => snap_step(end + page, min, max, step),
771                        Key::Home => start,
772                        Key::End => max,
773                        _ => return false,
774                    };
775                    (start, new_b.max(start))
776                } else {
777                    let new_a = match ke.key {
778                        Key::ArrowLeft | Key::ArrowDown => {
779                            snap_step(start - step_val, min, max, step)
780                        }
781                        Key::ArrowRight | Key::ArrowUp => {
782                            snap_step(start + step_val, min, max, step)
783                        }
784                        Key::PageDown => snap_step(start - page, min, max, step),
785                        Key::PageUp => snap_step(start + page, min, max, step),
786                        Key::Home => min,
787                        Key::End => end,
788                        _ => return false,
789                    };
790                    (new_a.min(end), end)
791                };
792                if (a - start).abs() > 1e-6 || (b - end).abs() > 1e-6 {
793                    (oc)(a, b);
794                }
795                true
796            }
797        })
798        .then(config.modifier))
799    .semantics(Semantics {
800        role: Role::Slider,
801        enabled: is_enabled,
802        value: Some(format!("{start}..{end}")),
803        ..Default::default()
804    })
805}