Skip to main content

gpui_base/
slider.rs

1use crate::TestSupportExt as _;
2use std::ops::Range;
3
4use gpui::{
5    AccessibleAction, Along, AnyElement, App, AppContext as _, Axis, Bounds, Context, Div,
6    DragMoveEvent, Empty, Entity, EntityId, EventEmitter, HitboxBehavior, InteractiveElement,
7    IntoElement, MouseButton, MouseDownEvent, Orientation, ParentElement, Pixels, Point, Render,
8    RenderOnce, Role, StatefulInteractiveElement, StyleRefinement, Styled, TouchDragEvent,
9    TouchPhase, Window, div, prelude::FluentBuilder as _, px,
10};
11
12use crate::{element_ext::ElementExt, geometry::AxisExt};
13
14/// Events emitted by the [`SliderState`].
15pub enum SliderEvent {
16    /// Emitted continuously while the slider value is being changed by the user.
17    Change(SliderValue),
18    /// Emitted once when the user releases the slider after a drag or click.
19    Release(SliderValue),
20}
21
22/// The value of the slider, can be a single value or a range of values.
23///
24/// - Can from a f32 value, which will be treated as a single value.
25/// - Or from a (f32, f32) tuple, which will be treated as a range of values.
26///
27/// The default value is `SliderValue::Single(0.0)`.
28#[derive(Clone, Copy, Debug, PartialEq)]
29pub enum SliderValue {
30    Single(f32),
31    Range(f32, f32),
32}
33
34impl std::fmt::Display for SliderValue {
35    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36        match self {
37            SliderValue::Single(value) => write!(f, "{}", value),
38            SliderValue::Range(start, end) => write!(f, "{}..{}", start, end),
39        }
40    }
41}
42
43impl From<f32> for SliderValue {
44    fn from(value: f32) -> Self {
45        SliderValue::Single(value)
46    }
47}
48
49impl From<(f32, f32)> for SliderValue {
50    fn from(value: (f32, f32)) -> Self {
51        SliderValue::Range(value.0, value.1)
52    }
53}
54
55impl From<Range<f32>> for SliderValue {
56    fn from(value: Range<f32>) -> Self {
57        SliderValue::Range(value.start, value.end)
58    }
59}
60
61impl Default for SliderValue {
62    fn default() -> Self {
63        SliderValue::Single(0.)
64    }
65}
66
67impl SliderValue {
68    /// Clamp the value to the given range.
69    pub fn clamp(self, min: f32, max: f32) -> Self {
70        match self {
71            SliderValue::Single(value) => SliderValue::Single(value.clamp(min, max)),
72            SliderValue::Range(start, end) => {
73                SliderValue::Range(start.clamp(min, max), end.clamp(min, max))
74            }
75        }
76    }
77
78    /// Check if the value is a single value.
79    #[inline]
80    pub fn is_single(&self) -> bool {
81        matches!(self, SliderValue::Single(_))
82    }
83
84    /// Check if the value is a range of values.
85    #[inline]
86    pub fn is_range(&self) -> bool {
87        matches!(self, SliderValue::Range(_, _))
88    }
89
90    /// Get the start value.
91    pub fn start(&self) -> f32 {
92        match self {
93            SliderValue::Single(value) => *value,
94            SliderValue::Range(start, _) => *start,
95        }
96    }
97
98    /// Get the end value.
99    pub fn end(&self) -> f32 {
100        match self {
101            SliderValue::Single(value) => *value,
102            SliderValue::Range(_, end) => *end,
103        }
104    }
105
106    fn set_start(&mut self, value: f32) {
107        if let SliderValue::Range(_, end) = self {
108            *self = SliderValue::Range(value.min(*end), *end);
109        } else {
110            *self = SliderValue::Single(value);
111        }
112    }
113
114    fn set_end(&mut self, value: f32) {
115        if let SliderValue::Range(start, _) = self {
116            *self = SliderValue::Range(*start, value.max(*start));
117        } else {
118            *self = SliderValue::Single(value);
119        }
120    }
121}
122
123/// The scale mode of the slider.
124#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
125pub enum SliderScale {
126    /// Linear scale where values change uniformly across the slider range.
127    /// This is the default mode.
128    #[default]
129    Linear,
130    /// Logarithmic scale where the distance between values increases exponentially.
131    ///
132    /// This is useful for parameters that have a large range of values where smaller
133    /// changes are more significant at lower values. Common examples include:
134    ///
135    /// - Volume controls (human hearing perception is logarithmic)
136    /// - Frequency controls (musical notes follow a logarithmic scale)
137    /// - Zoom levels
138    /// - Any parameter where you want finer control at lower values
139    ///
140    /// # For example
141    ///
142    /// ```
143    /// use gpui_base::slider::{SliderScale, SliderState};
144    ///
145    /// let slider = SliderState::new()
146    ///     .min(1.0)    // Must be > 0 for logarithmic scale
147    ///     .max(1000.0)
148    ///     .scale(SliderScale::Logarithmic);
149    /// ```
150    ///
151    /// - Moving the slider 1/3 of the way will yield ~10
152    /// - Moving it 2/3 of the way will yield ~100
153    /// - The full range covers 3 orders of magnitude evenly
154    Logarithmic,
155}
156
157impl SliderScale {
158    #[inline]
159    pub fn is_linear(&self) -> bool {
160        matches!(self, SliderScale::Linear)
161    }
162
163    #[inline]
164    pub fn is_logarithmic(&self) -> bool {
165        matches!(self, SliderScale::Logarithmic)
166    }
167}
168
169/// State of the [`Slider`].
170pub struct SliderState {
171    min: f32,
172    max: f32,
173    step: f32,
174    value: SliderValue,
175    /// When is single value mode, only `end` is used, the start is always 0.0.
176    percentage: Range<f32>,
177    /// The bounds of the slider after rendered.
178    bounds: Bounds<Pixels>,
179    scale: SliderScale,
180    /// Tracks whether the user is currently interacting with the slider so we
181    /// only emit [`SliderEvent::Release`] after a real press/drag.
182    dragging: bool,
183    /// The touch drag this slider claimed, while one is live.
184    touch_drag: Option<TouchDrag>,
185}
186
187/// A touch drag claimed by a slider.
188#[derive(Clone, Copy)]
189struct TouchDrag {
190    /// Identifies the gesture: every event of one drag carries the same start.
191    start_position: Point<Pixels>,
192    /// Whether it moves the range start thumb.
193    is_start: bool,
194}
195
196impl SliderState {
197    /// Create a new [`SliderState`].
198    pub fn new() -> Self {
199        Self {
200            min: 0.0,
201            max: 100.0,
202            step: 1.0,
203            value: SliderValue::default(),
204            percentage: (0.0..0.0),
205            bounds: Bounds::default(),
206            scale: SliderScale::default(),
207            dragging: false,
208            touch_drag: None,
209        }
210    }
211
212    /// Set the minimum value of the slider, default: 0.0
213    pub fn min(mut self, min: f32) -> Self {
214        if self.scale.is_logarithmic() {
215            assert!(
216                min > 0.0,
217                "`min` must be greater than 0 for SliderScale::Logarithmic"
218            );
219            assert!(
220                min < self.max,
221                "`min` must be less than `max` for Logarithmic scale"
222            );
223        }
224        self.min = min;
225        self.update_thumb_pos();
226        self
227    }
228
229    /// Set the maximum value of the slider, default: 100.0
230    pub fn max(mut self, max: f32) -> Self {
231        if self.scale.is_logarithmic() {
232            assert!(
233                max > self.min,
234                "`max` must be greater than `min` for Logarithmic scale"
235            );
236        }
237        self.max = max;
238        self.update_thumb_pos();
239        self
240    }
241
242    /// Set the step value of the slider, default: 1.0
243    pub fn step(mut self, step: f32) -> Self {
244        self.step = step;
245        self
246    }
247
248    /// Set the scale of the slider, default: [`SliderScale::Linear`].
249    pub fn scale(mut self, scale: SliderScale) -> Self {
250        if scale.is_logarithmic() {
251            assert!(
252                self.min > 0.0,
253                "`min` must be greater than 0 for Logarithmic scale"
254            );
255            assert!(
256                self.max > self.min,
257                "`max` must be greater than `min` for Logarithmic scale"
258            );
259        }
260        self.scale = scale;
261        self.update_thumb_pos();
262        self
263    }
264
265    /// Set the default value of the slider, default: 0.0
266    pub fn default_value(mut self, value: impl Into<SliderValue>) -> Self {
267        self.value = value.into();
268        self.update_thumb_pos();
269        self
270    }
271
272    /// Set the value of the slider.
273    pub fn set_value(
274        &mut self,
275        value: impl Into<SliderValue>,
276        _: &mut Window,
277        cx: &mut Context<Self>,
278    ) {
279        self.value = value.into();
280        self.update_thumb_pos();
281        cx.notify();
282    }
283
284    /// Get the value of the slider.
285    pub fn value(&self) -> SliderValue {
286        self.value
287    }
288
289    /// Get the minimum value.
290    pub fn min_value(&self) -> f32 {
291        self.min
292    }
293
294    /// Get the maximum value.
295    pub fn max_value(&self) -> f32 {
296        self.max
297    }
298
299    /// Get the step value.
300    pub fn step_value(&self) -> f32 {
301        self.step
302    }
303
304    /// Converts a value between 0.0 and 1.0 to a value between the minimum and maximum value,
305    /// depending on the chosen scale.
306    fn percentage_to_value(&self, percentage: f32) -> f32 {
307        match self.scale {
308            SliderScale::Linear => self.min + (self.max - self.min) * percentage,
309            SliderScale::Logarithmic => {
310                // when percentage is 0, this simplifies to (max/min)^0 * min = 1 * min = min
311                // when percentage is 1, this simplifies to (max/min)^1 * min = (max*min)/min = max
312                // we clamp just to make sure we don't have issue with floating point precision
313                let base = self.max / self.min;
314                (base.powf(percentage) * self.min).clamp(self.min, self.max)
315            }
316        }
317    }
318
319    /// Converts a value between the minimum and maximum value to a value between 0.0 and 1.0,
320    /// depending on the chosen scale.
321    fn value_to_percentage(&self, value: f32) -> f32 {
322        match self.scale {
323            SliderScale::Linear => {
324                let range = self.max - self.min;
325                if range <= 0.0 {
326                    0.0
327                } else {
328                    (value - self.min) / range
329                }
330            }
331            SliderScale::Logarithmic => {
332                let base = self.max / self.min;
333                (value / self.min).log(base).clamp(0.0, 1.0)
334            }
335        }
336    }
337
338    fn update_thumb_pos(&mut self) {
339        match self.value {
340            SliderValue::Single(value) => {
341                let percentage = self.value_to_percentage(value.clamp(self.min, self.max));
342                self.percentage = 0.0..percentage;
343            }
344            SliderValue::Range(start, end) => {
345                let clamped_start = start.clamp(self.min, self.max);
346                let clamped_end = end.clamp(self.min, self.max);
347                self.percentage =
348                    self.value_to_percentage(clamped_start)..self.value_to_percentage(clamped_end);
349            }
350        }
351    }
352
353    /// Update value by mouse position
354    #[doc(hidden)]
355    pub fn update_value_by_position(
356        &mut self,
357        axis: Axis,
358        position: Point<Pixels>,
359        is_start: bool,
360        _: &mut Window,
361        cx: &mut Context<Self>,
362    ) {
363        self.dragging = true;
364        let bounds = self.bounds;
365        let step = self.step;
366
367        let inner_pos = if axis.is_horizontal() {
368            position.x - bounds.left()
369        } else {
370            bounds.bottom() - position.y
371        };
372        let total_size = bounds.size.along(axis);
373        let percentage = inner_pos.clamp(px(0.), total_size) / total_size;
374
375        let percentage = if is_start {
376            percentage.clamp(0.0, self.percentage.end)
377        } else {
378            percentage.clamp(self.percentage.start, 1.0)
379        };
380
381        let value = self.percentage_to_value(percentage);
382        let value = (value / step).round() * step;
383
384        if is_start {
385            self.percentage.start = percentage;
386            self.value.set_start(value);
387        } else {
388            self.percentage.end = percentage;
389            self.value.set_end(value);
390        }
391        cx.emit(SliderEvent::Change(self.value));
392        cx.notify();
393    }
394
395    /// Whether `position` is closer to the range start thumb than to the end thumb.
396    fn is_nearer_start(&self, axis: Axis, position: Point<Pixels>) -> bool {
397        let size = self.bounds.size.along(axis);
398        let along = if axis.is_horizontal() {
399            position.x - self.bounds.left()
400        } else {
401            self.bounds.bottom() - position.y
402        };
403        let center =
404            ((self.percentage.end - self.percentage.start) / 2. + self.percentage.start) * size;
405        along < center
406    }
407
408    /// Emit [`SliderEvent::Release`] if the user was actively interacting
409    /// with the slider. Called on mouse-up both inside and outside the slider.
410    #[doc(hidden)]
411    pub fn handle_release(&mut self, cx: &mut Context<Self>) {
412        if !self.dragging {
413            return;
414        }
415        self.dragging = false;
416        cx.emit(SliderEvent::Release(self.value));
417    }
418}
419
420#[derive(Clone)]
421struct DragThumb((EntityId, bool));
422
423impl Render for DragThumb {
424    fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
425        Empty
426    }
427}
428
429#[derive(Clone)]
430struct DragSlider(EntityId);
431
432impl Render for DragSlider {
433    fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
434        Empty
435    }
436}
437
438/// An unstyled slider behavior root.
439///
440/// Applications provide the track, range, and thumb presentation as children.
441#[derive(IntoElement)]
442pub struct Slider {
443    state: Entity<SliderState>,
444    axis: Axis,
445    disabled: bool,
446    base: Div,
447    children: Vec<AnyElement>,
448}
449
450impl Slider {
451    pub fn new(state: &Entity<SliderState>) -> Self {
452        Self {
453            state: state.clone(),
454            axis: Axis::Horizontal,
455            disabled: false,
456            base: div(),
457            children: Vec::new(),
458        }
459    }
460
461    pub fn horizontal(mut self) -> Self {
462        self.axis = Axis::Horizontal;
463        self
464    }
465
466    pub fn vertical(mut self) -> Self {
467        self.axis = Axis::Vertical;
468        self
469    }
470
471    pub fn axis(mut self, axis: Axis) -> Self {
472        self.axis = axis;
473        self
474    }
475
476    pub fn disabled(mut self, disabled: bool) -> Self {
477        self.disabled = disabled;
478        self
479    }
480}
481
482impl ParentElement for Slider {
483    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
484        self.children.extend(elements);
485    }
486}
487
488impl Styled for Slider {
489    fn style(&mut self) -> &mut StyleRefinement {
490        self.base.style()
491    }
492}
493
494impl RenderOnce for Slider {
495    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
496        let axis = self.axis;
497        let entity_id = self.state.entity_id();
498        let state = self.state.read(cx);
499        let slider_state = self.state.clone();
500
501        self.base
502            .id(("slider", entity_id))
503            .test_support()
504            .role(Role::Slider)
505            .aria_numeric_value(state.value().end() as f64)
506            .aria_min_numeric_value(state.min_value() as f64)
507            .aria_max_numeric_value(state.max_value() as f64)
508            .aria_numeric_value_step(state.step_value() as f64)
509            .aria_orientation(if axis.is_vertical() {
510                Orientation::Vertical
511            } else {
512                Orientation::Horizontal
513            })
514            .on_a11y_action(AccessibleAction::Increment, {
515                let state = slider_state.clone();
516                move |_, window, cx| {
517                    state.update(cx, |state, cx| {
518                        let value =
519                            (state.value().end() + state.step_value()).min(state.max_value());
520                        state.set_value(value, window, cx);
521                    });
522                }
523            })
524            .on_a11y_action(AccessibleAction::Decrement, {
525                let state = slider_state.clone();
526                move |_, window, cx| {
527                    state.update(cx, |state, cx| {
528                        let value =
529                            (state.value().end() - state.step_value()).max(state.min_value());
530                        state.set_value(value, window, cx);
531                    });
532                }
533            })
534            .when(!self.disabled, |this| {
535                this.on_mouse_up(
536                    MouseButton::Left,
537                    window.listener_for(&self.state, |state, _, _, cx| state.handle_release(cx)),
538                )
539                .on_mouse_up_out(
540                    MouseButton::Left,
541                    window.listener_for(&self.state, |state, _, _, cx| state.handle_release(cx)),
542                )
543            })
544            .children(self.children)
545    }
546}
547
548/// An unstyled track that records the geometry used to map pointer positions.
549#[derive(IntoElement)]
550pub struct SliderTrack {
551    state: Entity<SliderState>,
552    axis: Axis,
553    disabled: bool,
554    base: Div,
555    children: Vec<AnyElement>,
556}
557
558impl SliderTrack {
559    pub fn new(state: &Entity<SliderState>) -> Self {
560        Self {
561            state: state.clone(),
562            axis: Axis::Horizontal,
563            disabled: false,
564            base: div(),
565            children: Vec::new(),
566        }
567    }
568
569    pub fn axis(mut self, axis: Axis) -> Self {
570        self.axis = axis;
571        self
572    }
573    pub fn disabled(mut self, disabled: bool) -> Self {
574        self.disabled = disabled;
575        self
576    }
577}
578
579impl ParentElement for SliderTrack {
580    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
581        self.children.extend(elements);
582    }
583}
584
585impl Styled for SliderTrack {
586    fn style(&mut self) -> &mut StyleRefinement {
587        self.base.style()
588    }
589}
590
591impl InteractiveElement for SliderTrack {
592    fn interactivity(&mut self) -> &mut gpui::Interactivity {
593        self.base.interactivity()
594    }
595}
596
597impl RenderOnce for SliderTrack {
598    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
599        let axis = self.axis;
600        let entity_id = self.state.entity_id();
601        let state = self.state.read(cx);
602        let is_range = state.value().is_range();
603        let percentage = state.percentage();
604        // Touch: GPUI routes a finger drag to `TouchDragEvent` (claimed with
605        // `prevent_default`) or to scrolling; `on_drag` below is mouse-only. Claim
606        // drags that start on the track, as the scrollbar thumb does.
607        let touch_layer = (!self.disabled).then(|| {
608            let slider_state = self.state.clone();
609            gpui::canvas(
610                |bounds, window, _| window.insert_hitbox(bounds, HitboxBehavior::Normal),
611                move |_, hitbox, window, _| {
612                    let slider_state = slider_state.clone();
613                    window.on_mouse_event(move |event: &TouchDragEvent, phase, window, cx| {
614                        if !phase.bubble() {
615                            return;
616                        }
617                        slider_state.update(cx, |state, cx| {
618                            if event.phase == TouchPhase::Started {
619                                // A drag whose end never arrived (the slider was
620                                // disabled or not painted) ends when another begins.
621                                if state.touch_drag.take().is_some() {
622                                    state.handle_release(cx);
623                                }
624                                // The hitbox, not the bounds, so a layer covering
625                                // the track keeps its own drags.
626                                if window.default_prevented() || !hitbox.is_hovered(window) {
627                                    return;
628                                }
629                                window.prevent_default();
630                                let is_start =
631                                    is_range && state.is_nearer_start(axis, event.start_position);
632                                state.touch_drag = Some(TouchDrag {
633                                    start_position: event.start_position,
634                                    is_start,
635                                });
636                            }
637                            let Some(drag) = state.touch_drag else {
638                                return;
639                            };
640                            if drag.start_position != event.start_position {
641                                return;
642                            }
643                            cx.stop_propagation();
644                            match event.phase {
645                                TouchPhase::Started | TouchPhase::Moved => {
646                                    state.update_value_by_position(
647                                        axis,
648                                        event.position,
649                                        drag.is_start,
650                                        window,
651                                        cx,
652                                    );
653                                }
654                                TouchPhase::Ended | TouchPhase::Cancelled => {
655                                    state.touch_drag = None;
656                                    state.handle_release(cx);
657                                }
658                            }
659                        });
660                    });
661                },
662            )
663            .absolute()
664            .size_full()
665        });
666        self.base
667            .id("slider-bar-container")
668            .test_support()
669            .children(self.children)
670            .children(touch_layer)
671            .when(!self.disabled, |this| {
672                this.on_mouse_down(
673                    MouseButton::Left,
674                    window.listener_for(
675                        &self.state,
676                        move |state, event: &MouseDownEvent, window, cx| {
677                            let is_start = if is_range {
678                                let size = state.bounds().size.along(axis);
679                                let position = if axis.is_horizontal() {
680                                    event.position.x - state.bounds().left()
681                                } else {
682                                    state.bounds().bottom() - event.position.y
683                                };
684                                let center = ((percentage.end - percentage.start) / 2.
685                                    + percentage.start)
686                                    * size;
687                                position < center
688                            } else {
689                                false
690                            };
691                            state.update_value_by_position(
692                                axis,
693                                event.position,
694                                is_start,
695                                window,
696                                cx,
697                            );
698                        },
699                    ),
700                )
701                .when(!is_range, |this| {
702                    this.on_drag(DragSlider(entity_id), |drag, _, _, cx| {
703                        cx.stop_propagation();
704                        cx.new(|_| drag.clone())
705                    })
706                    .on_drag_move(window.listener_for(
707                        &self.state,
708                        move |state, event: &DragMoveEvent<DragSlider>, window, cx| {
709                            let DragSlider(id) = event.drag(cx);
710                            if *id == entity_id {
711                                state.update_value_by_position(
712                                    axis,
713                                    event.event.position,
714                                    false,
715                                    window,
716                                    cx,
717                                );
718                            }
719                        },
720                    ))
721                })
722            })
723    }
724}
725
726/// An unstyled slider indicator that records the value-mapping bounds.
727#[derive(IntoElement)]
728pub struct SliderIndicator {
729    state: Entity<SliderState>,
730    base: Div,
731    children: Vec<AnyElement>,
732}
733
734impl SliderIndicator {
735    pub fn new(state: &Entity<SliderState>) -> Self {
736        Self {
737            state: state.clone(),
738            base: div(),
739            children: Vec::new(),
740        }
741    }
742}
743
744impl ParentElement for SliderIndicator {
745    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
746        self.children.extend(elements);
747    }
748}
749
750impl Styled for SliderIndicator {
751    fn style(&mut self) -> &mut StyleRefinement {
752        self.base.style()
753    }
754}
755
756impl InteractiveElement for SliderIndicator {
757    fn interactivity(&mut self) -> &mut gpui::Interactivity {
758        self.base.interactivity()
759    }
760}
761
762impl StatefulInteractiveElement for SliderIndicator {}
763
764impl RenderOnce for SliderIndicator {
765    fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
766        self.base
767            .id("slider-bar")
768            .children(self.children)
769            .on_prepaint({
770                let state = self.state;
771                move |bounds, _, cx| state.update(cx, |state, _| state.set_bounds(bounds))
772            })
773    }
774}
775
776/// An unstyled draggable slider thumb.
777#[derive(IntoElement)]
778pub struct SliderThumb {
779    state: Entity<SliderState>,
780    axis: Axis,
781    start: bool,
782    disabled: bool,
783    base: Div,
784    children: Vec<AnyElement>,
785}
786
787impl SliderThumb {
788    pub fn new(state: &Entity<SliderState>) -> Self {
789        Self {
790            state: state.clone(),
791            axis: Axis::Horizontal,
792            start: false,
793            disabled: false,
794            base: div(),
795            children: Vec::new(),
796        }
797    }
798
799    pub fn axis(mut self, axis: Axis) -> Self {
800        self.axis = axis;
801        self
802    }
803    pub fn start(mut self, start: bool) -> Self {
804        self.start = start;
805        self
806    }
807    pub fn disabled(mut self, disabled: bool) -> Self {
808        self.disabled = disabled;
809        self
810    }
811}
812
813impl ParentElement for SliderThumb {
814    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
815        self.children.extend(elements);
816    }
817}
818
819impl Styled for SliderThumb {
820    fn style(&mut self) -> &mut StyleRefinement {
821        self.base.style()
822    }
823}
824
825impl InteractiveElement for SliderThumb {
826    fn interactivity(&mut self) -> &mut gpui::Interactivity {
827        self.base.interactivity()
828    }
829}
830
831impl StatefulInteractiveElement for SliderThumb {}
832
833impl RenderOnce for SliderThumb {
834    fn render(self, window: &mut Window, _: &mut App) -> impl IntoElement {
835        let entity_id = self.state.entity_id();
836        let axis = self.axis;
837        let start = self.start;
838        self.base
839            .id(("slider-thumb", start as u32))
840            .test_support()
841            .children(self.children)
842            .when(!self.disabled, |this| {
843                this.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
844                    .on_drag(DragThumb((entity_id, start)), |drag, _, _, cx| {
845                        cx.stop_propagation();
846                        cx.new(|_| drag.clone())
847                    })
848                    .on_drag_move(window.listener_for(
849                        &self.state,
850                        move |state, event: &DragMoveEvent<DragThumb>, window, cx| {
851                            let DragThumb((id, start)) = event.drag(cx);
852                            if *id == entity_id {
853                                state.update_value_by_position(
854                                    axis,
855                                    event.event.position,
856                                    *start,
857                                    window,
858                                    cx,
859                                );
860                            }
861                        },
862                    ))
863            })
864    }
865}
866
867impl EventEmitter<SliderEvent> for SliderState {}
868
869impl SliderState {
870    #[doc(hidden)]
871    pub fn percentage(&self) -> Range<f32> {
872        self.percentage.clone()
873    }
874
875    #[doc(hidden)]
876    pub fn bounds(&self) -> Bounds<Pixels> {
877        self.bounds
878    }
879
880    #[doc(hidden)]
881    pub fn set_bounds(&mut self, bounds: Bounds<Pixels>) {
882        self.bounds = bounds;
883    }
884}
885
886#[cfg(test)]
887mod tests {
888    use super::*;
889
890    #[test]
891    fn legacy_value_conversions_and_clamping_are_preserved() {
892        assert_eq!(SliderValue::from(5.), SliderValue::Single(5.));
893        assert_eq!(SliderValue::from((2., 8.)), SliderValue::Range(2., 8.));
894        assert_eq!(SliderValue::from(2.0..8.0), SliderValue::Range(2., 8.));
895        assert_eq!(
896            SliderValue::Range(-1., 12.).clamp(0., 10.),
897            SliderValue::Range(0., 10.)
898        );
899    }
900
901    #[test]
902    fn legacy_linear_state_keeps_percentage_and_range_ordering() {
903        let state = SliderState::new()
904            .min(0.)
905            .max(200.)
906            .default_value((50., 150.));
907        assert_eq!(state.value(), SliderValue::Range(50., 150.));
908        assert_eq!(state.percentage(), 0.25..0.75);
909    }
910
911    #[test]
912    fn legacy_logarithmic_state_keeps_mapping() {
913        let state = SliderState::new()
914            .min(1.)
915            .max(1000.)
916            .scale(SliderScale::Logarithmic)
917            .default_value(10.);
918        let percentage = state.percentage().end;
919        assert!((percentage - (1. / 3.)).abs() < 0.0001);
920    }
921
922    #[test]
923    #[should_panic(expected = "`min` must be greater than 0")]
924    fn legacy_logarithmic_validation_is_preserved() {
925        let _ = SliderState::new().scale(SliderScale::Logarithmic);
926    }
927}