Seregon/StratoSDK

StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.

Rust/27.3 KB/No license
crates/strato-ui-core/src/ui_components/slider.rs
1use std::{ops::Range, sync::Arc};
2 
3use crate::platform::Cursor;
4use crate::{
5 elements::{
6 AnchorPair, ConstrainedBox, Container, CornerRadius, DragAxis, Draggable, DraggableState,
7 DropShadow, Fill, Hoverable, MouseStateHandle, OffsetPositioning, OffsetType,
8 ParentElement, ParentOffsetBounds, PositionedElementOffsetBounds, PositioningAxis, Radius,
9 Rect, SavePosition, Stack, XAxisAnchor, YAxisAnchor,
10 },
11 ui_components::components::UiComponentStyles,
12 AppContext, Element, EventContext,
13};
14use lazy_static::lazy_static;
15use parking_lot::{Mutex, RwLock};
16use pathfinder_color::ColorU;
17use pathfinder_geometry::{rect::RectF, vector::vec2f};
18 
19use super::components::UiComponent;
20 
21const DEFAULT_THUMB_SIZE: f32 = 18.;
22const DEFAULT_TRACK_HEIGHT: f32 = 4.;
23const HOVER_OPACITY: u8 = 100;
24const HOVER_BORDER_SIZE: f32 = 10.;
25 
26lazy_static! {
27 pub static ref DEFAULT_TRACK_COLOR: ColorU = ColorU::new(170, 170, 170, 255);
28 pub static ref DEFAULT_TRACK_FILL: Fill = Fill::Solid(ColorU::new(170, 170, 170, 255));
29 pub static ref DEFAULT_THUMB_FILL: Fill = Fill::Solid(ColorU::white());
30 static ref THUMB_DROP_SHADOW: DropShadow = DropShadow {
31 color: ColorU::black(),
32 offset: vec2f(-0.5, 2.),
33 blur_radius: 20.,
34 spread_radius: 0.,
35 };
36 
37 /// A static counter of the number of instantiated sliders, which is used to create a unique
38 /// SavePosition ID to reference the position of the slider track, which is used to position
39 /// the slider thumb.
40 static ref TRACK_POSITION_ID_COUNT: RwLock<usize> = RwLock::new(0);
41}
42 
43#[derive(Clone, Copy, Default)]
44struct SliderState {
45 // The thumb's current offset from the "beginning" (minimum) x-axis coordinate of the track.
46 thumb_offset_x: Option<f32>,
47}
48 
49#[derive(Clone, Default)]
50pub struct SliderStateHandle {
51 thumb_hoverable_state: MouseStateHandle,
52 thumb_draggable_state: DraggableState,
53 track_hoverable_state: MouseStateHandle,
54 inner: Arc<Mutex<SliderState>>,
55}
56 
57impl SliderStateHandle {
58 // Returns the thumb's current offset from the "beginning" (minimum) x-axis coordinate of the
59 // track.
60 fn thumb_offset_x(&self) -> Option<f32> {
61 self.inner.lock().thumb_offset_x
62 }
63 
64 /// Sets the inner [`SliderState`] to `new_state`.
65 fn store(&self, new_state: SliderState) {
66 let mut guard = self.inner.lock();
67 *guard = new_state;
68 }
69 
70 /// Resets the thumb's offset to `None`, which causes the default value to be
71 /// used when the slider is next rendered.
72 pub fn reset_offset(&self) {
73 self.store(SliderState {
74 thumb_offset_x: None,
75 });
76 }
77}
78 
79/// Type alias for `on_drag` and `on_change` callbacks, either of which is executed when the slider's
80/// value has changed.
81type OnValueChangedFn = dyn Fn(&mut EventContext, &AppContext, f32) + 'static;
82 
83/// Shared track geometry and snapping configuration passed to every
84/// slider callback registration function.
85#[derive(Clone)]
86struct SliderTrackConfig {
87 track_position_id: String,
88 thumb_size: f32,
89 value_range: Range<f32>,
90 step: Option<f32>,
91 snap_values: Option<Arc<Vec<f32>>>,
92 state_handle: SliderStateHandle,
93}
94 
95/// Slider UiComponent for modulating a value between given bounds.
96///
97/// Builder methods allow the caller to configure the styling of the slider, as well as set a
98/// callback to be executed when the slider 'thumb' (handle) is dragged, as well as when the thumb
99/// is dropped (marking the end of a 'drag').
100pub struct Slider {
101 state_handle: SliderStateHandle,
102 track_position_id: String,
103 on_drag_callback: Option<Box<OnValueChangedFn>>,
104 on_change_callback: Option<Arc<OnValueChangedFn>>,
105 thumb_size: f32,
106 track_height: f32,
107 track_fill: Fill,
108 thumb_fill: Fill,
109 styles: UiComponentStyles,
110 value_range: Range<f32>,
111 default_value: Option<f32>,
112 step: Option<f32>,
113 snap_values: Option<Arc<Vec<f32>>>,
114}
115 
116impl Slider {
117 pub fn new(slider_state_handle: SliderStateHandle) -> Self {
118 Self {
119 track_position_id: new_track_position_id(),
120 state_handle: slider_state_handle,
121 on_drag_callback: None,
122 on_change_callback: None,
123 thumb_size: DEFAULT_THUMB_SIZE,
124 track_height: DEFAULT_TRACK_HEIGHT,
125 track_fill: *DEFAULT_TRACK_FILL,
126 thumb_fill: *DEFAULT_THUMB_FILL,
127 value_range: 0.0..1.,
128 default_value: None,
129 step: None,
130 snap_values: None,
131 styles: UiComponentStyles {
132 ..Default::default()
133 },
134 }
135 }
136 
137 /// Sets a step size so that both the thumb and emitted value snap to
138 /// discrete increments of `step` from `value_range.start`. `value_range.end`
139 /// is always reachable even if it isn't step-aligned.
140 pub fn with_step(mut self, step: f32) -> Self {
141 self.step = Some(step);
142 self
143 }
144 
145 /// Sets an explicit list of discrete values that the slider snaps to.
146 /// Drag/drop/click events snap to the nearest value in the list by
147 /// absolute distance, and the thumb is positioned **linearly** based on
148 /// the value (`(value - start) / (end - start)`) — this keeps
149 /// non-step-aligned inputs from looking logarithmic. Takes precedence
150 /// over [`Self::with_step`] when set.
151 pub fn with_snap_values(mut self, values: Vec<f32>) -> Self {
152 self.snap_values = Some(Arc::new(values));
153 self
154 }
155 
156 pub fn with_thumb_size(mut self, thumb_size: f32) -> Self {
157 self.thumb_size = thumb_size;
158 self
159 }
160 
161 pub fn with_thumb_fill(mut self, fill: Fill) -> Self {
162 self.thumb_fill = fill;
163 self
164 }
165 
166 pub fn with_track_fill(mut self, fill: Fill) -> Self {
167 self.track_fill = fill;
168 self
169 }
170 
171 pub fn with_track_height(mut self, height: f32) -> Self {
172 self.track_height = height;
173 self
174 }
175 
176 /// Sets the slider's value range. If set, values passed to the `on_change` callback are
177 /// normalized to the given range.
178 pub fn with_range(mut self, range: Range<f32>) -> Self {
179 self.value_range = range;
180 self
181 }
182 
183 pub fn with_default_value(mut self, value: f32) -> Self {
184 self.default_value = Some(value);
185 self
186 }
187 
188 /// Called when the value represented by the slider changes when the user drags the slider
189 /// thumb. The emitted value is normalized to the slider's value range, the default for
190 /// which is [0, 1].
191 pub fn on_drag<F>(mut self, callback: F) -> Self
192 where
193 F: Fn(&mut EventContext, &AppContext, f32) + 'static,
194 {
195 self.on_drag_callback = Some(Box::new(callback));
196 self
197 }
198 
199 /// Called when the slider thumb is 'dropped' at the end of a drag. The emitted value is
200 /// normalized to the slider's value range, the default for which is [0, 1].
201 pub fn on_change<F>(mut self, callback: F) -> Self
202 where
203 F: Fn(&mut EventContext, &AppContext, f32) + 'static,
204 {
205 self.on_change_callback = Some(Arc::new(callback));
206 self
207 }
208 
209 /// Registers the 'on_drag_start` callback on the `Draggable` element representing the slider
210 /// thumb.
211 ///
212 /// This callback stores the thumb's x-axis offset from the start of the track in the
213 /// given `SliderStateHandle`, snapping if configured.
214 fn register_on_drag_start_callback(thumb_draggable: &mut Draggable, config: SliderTrackConfig) {
215 thumb_draggable.set_on_drag_start(move |event_ctx, _app, thumb_position| {
216 let track_position = event_ctx
217 .element_position_by_id(config.track_position_id.as_str())
218 .expect("Track should be laid out by the time the slider is dragged.");
219 
220 let raw_offset_x = thumb_position.origin_x() - track_position.origin_x();
221 let draggable_width = draggable_width(track_position, config.thumb_size);
222 let (snapped_offset_x, _) = snap_offset_and_value(
223 raw_offset_x,
224 draggable_width,
225 &config.value_range,
226 config.step,
227 config.snap_values.as_deref().map(Vec::as_slice),
228 );
229 
230 config.state_handle.store(SliderState {
231 thumb_offset_x: Some(snapped_offset_x),
232 });
233 let delta = snapped_offset_x - raw_offset_x;
234 if delta.abs() > f32::EPSILON {
235 config
236 .state_handle
237 .thumb_draggable_state
238 .adjust_mouse_position(vec2f(delta, 0.));
239 }
240 });
241 }
242 
243 /// Registers the 'on_drag` callback on the `Draggable` element representing the slider thumb.
244 ///
245 /// The registered callback calls the user's supplied `on_drag` callback if the slider's x-axis
246 /// position has changed since the last time it was called. The user's callback is called with
247 /// the slider's current value, which is basically the slider thumb's offset x normalized to
248 /// the slider's `value_range`. In addition, it updates the `thumb_offset_x` in the slider's
249 /// state.
250 fn register_on_drag_callback(
251 thumb_draggable: &mut Draggable,
252 config: SliderTrackConfig,
253 on_drag_callback: Option<Box<OnValueChangedFn>>,
254 ) {
255 thumb_draggable.set_on_drag(move |event_ctx, app, thumb_position, _| {
256 let track_position = event_ctx
257 .element_position_by_id(config.track_position_id.as_str())
258 .expect("Track should be laid out by the time the slider is dragged.");
259 
260 let raw_offset_x = thumb_position.origin_x() - track_position.origin_x();
261 let draggable_width = draggable_width(track_position, config.thumb_size);
262 let (snapped_offset_x, snapped_value) = snap_offset_and_value(
263 raw_offset_x,
264 draggable_width,
265 &config.value_range,
266 config.step,
267 config.snap_values.as_deref().map(Vec::as_slice),
268 );
269 
270 let delta = snapped_offset_x - raw_offset_x;
271 if delta.abs() > f32::EPSILON {
272 config
273 .state_handle
274 .thumb_draggable_state
275 .adjust_mouse_position(vec2f(delta, 0.));
276 }
277 
278 if Some(snapped_offset_x) != config.state_handle.thumb_offset_x() {
279 config.state_handle.store(SliderState {
280 thumb_offset_x: Some(snapped_offset_x),
281 });
282 
283 if let Some(callback) = &on_drag_callback {
284 callback(event_ctx, app, snapped_value);
285 }
286 }
287 });
288 }
289 
290 /// Registers the 'on_change` callback on the `Draggable` element representing the slider thumb.
291 ///
292 /// The registered callback unconditinoally calls the user's supplied `on_change` callback. The
293 /// user's callback is called with the slider's current value, which is basically the slider
294 /// thumb's offset x normalized to the slider's `value_range`. In addition, it updates the
295 /// `thumb_offset_x` in the slider's state.
296 fn register_on_drop_callback(
297 thumb_draggable: &mut Draggable,
298 config: SliderTrackConfig,
299 on_change_callback: Option<Arc<OnValueChangedFn>>,
300 ) {
301 thumb_draggable.set_on_drop(move |event_ctx, app, thumb_position, _| {
302 let track_position = event_ctx
303 .element_position_by_id(config.track_position_id.as_str())
304 .expect("Track should be laid out by the time the slider is dropped.");
305 let raw_offset_x = thumb_position.origin_x() - track_position.origin_x();
306 let draggable_width = draggable_width(track_position, config.thumb_size);
307 let (snapped_offset_x, snapped_value) = snap_offset_and_value(
308 raw_offset_x,
309 draggable_width,
310 &config.value_range,
311 config.step,
312 config.snap_values.as_deref().map(Vec::as_slice),
313 );
314 config.state_handle.store(SliderState {
315 thumb_offset_x: Some(snapped_offset_x),
316 });
317 
318 if let Some(callback) = &on_change_callback {
319 callback(event_ctx, app, snapped_value);
320 }
321 });
322 }
323 
324 /// Registers the 'on_change_callback` callback on the `Hoverable` element representing the slider track.
325 ///
326 /// Whenever the underlying track is clicked, we set the thumb offset to the location of the click,
327 /// and then call the on_change_callback with the updated value. Basically works as if a user immediately
328 /// dragged the thumb to that location, without all the intermediate on_drag calls.
329 fn register_on_click_callback(
330 track_hoverable: Hoverable,
331 config: SliderTrackConfig,
332 on_change_callback: Option<Arc<OnValueChangedFn>>,
333 ) -> Hoverable {
334 track_hoverable.on_click(move |event_ctx, app, click_position| {
335 let Some(track_position) =
336 event_ctx.element_position_by_id(config.track_position_id.as_str())
337 else {
338 return;
339 };
340 
341 let click_position_x = click_position.x();
342 let padding = config.thumb_size / 2.;
343 let min_x = track_position.min_x() + padding;
344 let max_x = track_position.max_x() - padding;
345 
346 if min_x > click_position_x || max_x < click_position_x {
347 return;
348 }
349 
350 let raw_offset_x = click_position_x - min_x;
351 let draggable_width = draggable_width(track_position, config.thumb_size);
352 let (snapped_offset_x, snapped_value) = snap_offset_and_value(
353 raw_offset_x,
354 draggable_width,
355 &config.value_range,
356 config.step,
357 config.snap_values.as_deref().map(Vec::as_slice),
358 );
359 
360 config.state_handle.store(SliderState {
361 thumb_offset_x: Some(snapped_offset_x),
362 });
363 
364 if let Some(callback) = &on_change_callback {
365 callback(event_ctx, app, snapped_value);
366 }
367 })
368 }
369}
370 
371/// Snaps `raw_offset_x` (a pixel offset along the slider track) to the
372/// nearest discrete position, returning both the snapped pixel offset and
373/// the corresponding value.
374///
375/// If `snap_values` is provided it takes precedence: the raw value (linearly
376/// derived from the pixel position and `value_range`) is snapped to the
377/// nearest entry in the list by absolute distance, and the returned pixel
378/// offset is positioned **linearly** by the snapped value — so positions
379/// along the slider always match the value scale. Otherwise `step` (if any)
380/// is used for linear stepping from `value_range.start`.
381fn snap_offset_and_value(
382 raw_offset_x: f32,
383 draggable_width: f32,
384 value_range: &Range<f32>,
385 step: Option<f32>,
386 snap_values: Option<&[f32]>,
387) -> (f32, f32) {
388 if draggable_width <= 0. {
389 return (raw_offset_x, value_range.start);
390 }
391 let canonical = (raw_offset_x / draggable_width).clamp(0., 1.);
392 let raw_value = canonical * (value_range.end - value_range.start) + value_range.start;
393 
394 if let Some(values) = snap_values {
395 if !values.is_empty() {
396 // Snap to nearest value by absolute distance.
397 let snapped_value = values.iter().copied().fold(values[0], |best, v| {
398 if (v - raw_value).abs() < (best - raw_value).abs() {
399 v
400 } else {
401 best
402 }
403 });
404 let snapped_canonical = value_to_canonical_linear(snapped_value, value_range);
405 return (snapped_canonical * draggable_width, snapped_value);
406 }
407 }
408 
409 let Some(step) = step.filter(|s| *s > 0.) else {
410 return (raw_offset_x, raw_value);
411 };
412 
413 // Snap to nearest step from `range.start`, with `range.end` always reachable.
414 let snapped_value = if value_range.end - raw_value < step / 2. {
415 value_range.end
416 } else {
417 let offset_from_start = raw_value - value_range.start;
418 let steps = (offset_from_start / step).round();
419 (value_range.start + steps * step).clamp(value_range.start, value_range.end)
420 };
421 
422 let snapped_canonical = value_to_canonical_linear(snapped_value, value_range);
423 (snapped_canonical * draggable_width, snapped_value)
424}
425 
426/// Linearly maps `value` to a canonical 0..1 position along the slider
427/// track. Used both for snap positioning and for rendering `default_value`,
428/// so non-snap values (e.g. typed into a freeform input box) render at a
429/// position proportional to their actual magnitude.
430fn value_to_canonical_linear(value: f32, value_range: &Range<f32>) -> f32 {
431 let span = value_range.end - value_range.start;
432 if span <= 0. {
433 return 0.;
434 }
435 ((value - value_range.start) / span).clamp(0., 1.)
436}
437 
438impl UiComponent for Slider {
439 type ElementType = Container;
440 
441 fn build(self) -> Self::ElementType {
442 let Slider {
443 state_handle,
444 track_position_id: slider_track_position_id,
445 on_drag_callback,
446 on_change_callback,
447 thumb_size,
448 track_height,
449 track_fill,
450 thumb_fill,
451 styles,
452 value_range,
453 default_value,
454 step,
455 snap_values,
456 } = self;
457 
458 let track_position_id = slider_track_position_id.clone();
459 let mut slider_thumb = Draggable::new(
460 state_handle.thumb_draggable_state.clone(),
461 render_thumb(
462 thumb_fill,
463 thumb_size,
464 state_handle.thumb_hoverable_state.clone(),
465 ),
466 )
467 .with_drag_axis(DragAxis::HorizontalOnly)
468 .with_drag_bounds_callback(move |position_cache, _| {
469 position_cache
470 .get_position(track_position_id.as_str())
471 .map(|track_position| {
472 // Set drag bounds so the thumb may only be dragged along the track.
473 RectF::new(
474 vec2f(track_position.origin_x(), track_position.origin_y()),
475 vec2f(track_position.width(), 0.),
476 )
477 })
478 });
479 
480 let config = SliderTrackConfig {
481 track_position_id: slider_track_position_id.clone(),
482 thumb_size,
483 value_range: value_range.clone(),
484 step,
485 snap_values,
486 state_handle: state_handle.clone(),
487 };
488 
489 Self::register_on_drag_start_callback(&mut slider_thumb, config.clone());
490 Self::register_on_drag_callback(&mut slider_thumb, config.clone(), on_drag_callback);
491 Self::register_on_drop_callback(
492 &mut slider_thumb,
493 config.clone(),
494 on_change_callback.clone(),
495 );
496 
497 let track = Hoverable::new(state_handle.track_hoverable_state.clone(), |_| {
498 render_track(thumb_size, styles.width, track_height, track_fill)
499 });
500 
501 let track = Self::register_on_click_callback(track, config, on_change_callback.clone());
502 
503 let mut slider = Stack::new();
504 
505 slider.add_child(
506 SavePosition::new(track.finish(), slider_track_position_id.as_str()).finish(),
507 );
508 
509 let offset = match state_handle.thumb_offset_x() {
510 Some(offset_x) => OffsetType::Pixel(offset_x),
511 None => OffsetType::Percentage(
512 default_value
513 .map(|value| value_to_canonical_linear(value, &value_range))
514 .unwrap_or(0.),
515 ),
516 };
517 
518 slider.add_positioned_child(
519 slider_thumb.finish(),
520 OffsetPositioning::from_axes(
521 PositioningAxis::relative_to_stack_child(
522 &slider_track_position_id,
523 PositionedElementOffsetBounds::AnchoredElement,
524 // Set the position of the thumb based on the slider's current value.
525 offset,
526 AnchorPair::new(XAxisAnchor::Left, XAxisAnchor::Left),
527 ),
528 PositioningAxis::relative_to_stack_child(
529 &slider_track_position_id,
530 PositionedElementOffsetBounds::Unbounded,
531 OffsetType::Pixel(0.),
532 AnchorPair::new(YAxisAnchor::Middle, YAxisAnchor::Middle),
533 ),
534 ),
535 );
536 Container::new(slider.finish())
537 .with_margin_top(styles.margin.map(|margin| margin.top).unwrap_or(0.))
538 .with_margin_bottom(styles.margin.map(|margin| margin.bottom).unwrap_or(0.))
539 .with_margin_left(styles.margin.map(|margin| margin.left).unwrap_or(0.))
540 .with_margin_right(styles.margin.map(|margin| margin.right).unwrap_or(0.))
541 .with_padding_top(styles.padding.map(|padding| padding.top).unwrap_or(0.))
542 .with_padding_bottom(styles.padding.map(|padding| padding.bottom).unwrap_or(0.))
543 .with_padding_left(styles.padding.map(|padding| padding.left).unwrap_or(0.))
544 .with_padding_right(styles.padding.map(|padding| padding.right).unwrap_or(0.))
545 }
546 
547 fn with_style(self, styles: UiComponentStyles) -> Self {
548 Self {
549 state_handle: self.state_handle,
550 track_position_id: self.track_position_id,
551 on_drag_callback: self.on_drag_callback,
552 on_change_callback: self.on_change_callback,
553 thumb_size: self.thumb_size,
554 track_height: self.track_height,
555 track_fill: self.track_fill,
556 thumb_fill: self.thumb_fill,
557 value_range: self.value_range,
558 default_value: self.default_value,
559 step: self.step,
560 snap_values: self.snap_values,
561 styles: self.styles.merge(styles),
562 }
563 }
564}
565 
566/// Renders the slider 'track', along which the thumb can be dragged.
567fn render_track(thumb_size: f32, width: Option<f32>, height: f32, fill: Fill) -> Box<dyn Element> {
568 let mut track = ConstrainedBox::new(
569 Container::new(
570 Rect::new()
571 .with_background(fill)
572 .with_corner_radius(CornerRadius::with_all(Radius::Percentage(50.)))
573 .finish(),
574 )
575 .with_padding_left(thumb_size / 2.)
576 .with_padding_right(thumb_size / 2.)
577 .finish(),
578 )
579 .with_height(height);
580 if let Some(width) = width {
581 track = track.with_width(width);
582 }
583 
584 // We add a container with extra padding to make the track
585 // as tall (invisibly) as the thumb. This way, we can detect
586 // clicks that are slightly above or below the track bar itself.
587 let vertical_padding = ((thumb_size - height) / 2.).max(0.);
588 Container::new(track.finish())
589 .with_padding_top(vertical_padding)
590 .with_padding_bottom(vertical_padding)
591 .finish()
592}
593 
594/// Renders the 'thumb' (handle) for the slider.
595///
596/// The thumb is a circle with diameter set to `size`.
597fn render_thumb(fill: Fill, size: f32, state_handle: MouseStateHandle) -> Box<dyn Element> {
598 Hoverable::new(state_handle, move |hover_state| {
599 let thumb = Container::new(
600 ConstrainedBox::new(
601 Rect::new()
602 .with_background(fill)
603 .with_corner_radius(CornerRadius::with_all(Radius::Percentage(50.)))
604 .with_drop_shadow(*THUMB_DROP_SHADOW)
605 .finish(),
606 )
607 .with_width(size)
608 .with_height(size)
609 .finish(),
610 )
611 .finish();
612 let mut stack = Stack::new();
613 
614 if hover_state.is_hovered() {
615 let hover_size = size + HOVER_BORDER_SIZE;
616 let mut hover_background = *DEFAULT_TRACK_COLOR;
617 hover_background.a = HOVER_OPACITY;
618 
619 let thumb_hover = Container::new(
620 ConstrainedBox::new(
621 Rect::new()
622 .with_background_color(hover_background)
623 .with_corner_radius(CornerRadius::with_all(Radius::Percentage(50.)))
624 .finish(),
625 )
626 .with_width(hover_size)
627 .with_height(hover_size)
628 .finish(),
629 )
630 .finish();
631 
632 // Position the hover so that it's centered around the thumb. Since the hover
633 // is guaranteed to be larger than the thumb, we position the hover at the top
634 // left corner of the thumb and then translate it to the left and up so that it
635 // is centered.
636 stack.add_positioned_child(
637 thumb_hover,
638 OffsetPositioning::from_axes(
639 PositioningAxis::relative_to_parent(
640 ParentOffsetBounds::Unbounded,
641 OffsetType::Pixel(-((hover_size - size) / 2.)),
642 AnchorPair::new(XAxisAnchor::Left, XAxisAnchor::Left),
643 ),
644 PositioningAxis::relative_to_parent(
645 ParentOffsetBounds::Unbounded,
646 OffsetType::Pixel(-((hover_size - size) / 2.)),
647 AnchorPair::new(YAxisAnchor::Top, YAxisAnchor::Top),
648 ),
649 ),
650 );
651 }
652 
653 stack.add_child(thumb);
654 stack.finish()
655 })
656 .with_cursor(Cursor::PointingHand)
657 .finish()
658}
659 
660/// Returns a unique position ID for the slider track.
661fn new_track_position_id() -> String {
662 let current_count = *TRACK_POSITION_ID_COUNT.read();
663 let position_id = format!("SliderTrack{current_count}");
664 *TRACK_POSITION_ID_COUNT.write() = current_count + 1;
665 position_id
666}
667 
668/// Returns total width of the draggable area on the 'track'.
669fn draggable_width(track_position: RectF, thumb_size: f32) -> f32 {
670 track_position.max_x() - track_position.min_x() - thumb_size
671}
672