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, SharedString, StatefulInteractiveElement, StyleRefinement, Styled,
9    TouchDragEvent, TouchPhase, Window, div, prelude::FluentBuilder as _, px,
10};
11
12use crate::{a11y_disabled::A11yDisabledExt as _, 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    accessibility_label: Option<SharedString>,
447    base: Div,
448    children: Vec<AnyElement>,
449}
450
451impl Slider {
452    pub fn new(state: &Entity<SliderState>) -> Self {
453        Self {
454            state: state.clone(),
455            axis: Axis::Horizontal,
456            disabled: false,
457            accessibility_label: None,
458            base: div(),
459            children: Vec::new(),
460        }
461    }
462
463    pub fn horizontal(mut self) -> Self {
464        self.axis = Axis::Horizontal;
465        self
466    }
467
468    pub fn vertical(mut self) -> Self {
469        self.axis = Axis::Vertical;
470        self
471    }
472
473    pub fn axis(mut self, axis: Axis) -> Self {
474        self.axis = axis;
475        self
476    }
477
478    pub fn disabled(mut self, disabled: bool) -> Self {
479        self.disabled = disabled;
480        self
481    }
482
483    /// Sets the label exposed to accessibility clients.
484    pub fn accessibility_label(mut self, label: impl Into<SharedString>) -> Self {
485        self.accessibility_label = Some(label.into());
486        self
487    }
488}
489
490impl ParentElement for Slider {
491    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
492        self.children.extend(elements);
493    }
494}
495
496impl Styled for Slider {
497    fn style(&mut self) -> &mut StyleRefinement {
498        self.base.style()
499    }
500}
501
502impl RenderOnce for Slider {
503    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
504        let axis = self.axis;
505        let entity_id = self.state.entity_id();
506        let state = self.state.read(cx);
507        let slider_state = self.state.clone();
508
509        self.base
510            .id(("slider", entity_id))
511            .test_support()
512            .role(Role::Slider)
513            .when_some(self.accessibility_label, |this, label| {
514                this.aria_label(label)
515            })
516            .aria_numeric_value(state.value().end() as f64)
517            .aria_min_numeric_value(state.min_value() as f64)
518            .aria_max_numeric_value(state.max_value() as f64)
519            .aria_numeric_value_step(state.step_value() as f64)
520            .aria_orientation(if axis.is_vertical() {
521                Orientation::Vertical
522            } else {
523                Orientation::Horizontal
524            })
525            // A disabled slider takes no value, from the pointer or from
526            // assistive technology.
527            .when(!self.disabled, |this| {
528                this.on_a11y_action(AccessibleAction::Increment, {
529                    let state = slider_state.clone();
530                    move |_, window, cx| {
531                        state.update(cx, |state, cx| {
532                            let value =
533                                (state.value().end() + state.step_value()).min(state.max_value());
534                            state.set_value(value, window, cx);
535                        });
536                    }
537                })
538                .on_a11y_action(AccessibleAction::Decrement, {
539                    let state = slider_state.clone();
540                    move |_, window, cx| {
541                        state.update(cx, |state, cx| {
542                            let value =
543                                (state.value().end() - state.step_value()).max(state.min_value());
544                            state.set_value(value, window, cx);
545                        });
546                    }
547                })
548                .on_mouse_up(
549                    MouseButton::Left,
550                    window.listener_for(&self.state, |state, _, _, cx| state.handle_release(cx)),
551                )
552                .on_mouse_up_out(
553                    MouseButton::Left,
554                    window.listener_for(&self.state, |state, _, _, cx| state.handle_release(cx)),
555                )
556            })
557            .children(self.children)
558            .a11y_disabled(self.disabled)
559    }
560}
561
562/// An unstyled track that records the geometry used to map pointer positions.
563#[derive(IntoElement)]
564pub struct SliderTrack {
565    state: Entity<SliderState>,
566    axis: Axis,
567    disabled: bool,
568    base: Div,
569    children: Vec<AnyElement>,
570}
571
572impl SliderTrack {
573    pub fn new(state: &Entity<SliderState>) -> Self {
574        Self {
575            state: state.clone(),
576            axis: Axis::Horizontal,
577            disabled: false,
578            base: div(),
579            children: Vec::new(),
580        }
581    }
582
583    pub fn axis(mut self, axis: Axis) -> Self {
584        self.axis = axis;
585        self
586    }
587    pub fn disabled(mut self, disabled: bool) -> Self {
588        self.disabled = disabled;
589        self
590    }
591}
592
593impl ParentElement for SliderTrack {
594    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
595        self.children.extend(elements);
596    }
597}
598
599impl Styled for SliderTrack {
600    fn style(&mut self) -> &mut StyleRefinement {
601        self.base.style()
602    }
603}
604
605impl InteractiveElement for SliderTrack {
606    fn interactivity(&mut self) -> &mut gpui::Interactivity {
607        self.base.interactivity()
608    }
609}
610
611impl RenderOnce for SliderTrack {
612    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
613        let axis = self.axis;
614        let entity_id = self.state.entity_id();
615        let state = self.state.read(cx);
616        let is_range = state.value().is_range();
617        let percentage = state.percentage();
618        // Touch: GPUI routes a finger drag to `TouchDragEvent` (claimed with
619        // `prevent_default`) or to scrolling; `on_drag` below is mouse-only. Claim
620        // drags that start on the track, as the scrollbar thumb does.
621        let touch_layer = (!self.disabled).then(|| {
622            let slider_state = self.state.clone();
623            gpui::canvas(
624                |bounds, window, _| window.insert_hitbox(bounds, HitboxBehavior::Normal),
625                move |_, hitbox, window, _| {
626                    let slider_state = slider_state.clone();
627                    window.on_mouse_event(move |event: &TouchDragEvent, phase, window, cx| {
628                        if !phase.bubble() {
629                            return;
630                        }
631                        slider_state.update(cx, |state, cx| {
632                            if event.phase == TouchPhase::Started {
633                                // A drag whose end never arrived (the slider was
634                                // disabled or not painted) ends when another begins.
635                                if state.touch_drag.take().is_some() {
636                                    state.handle_release(cx);
637                                }
638                                // The hitbox, not the bounds, so a layer covering
639                                // the track keeps its own drags.
640                                if window.default_prevented() || !hitbox.is_hovered(window) {
641                                    return;
642                                }
643                                window.prevent_default();
644                                let is_start =
645                                    is_range && state.is_nearer_start(axis, event.start_position);
646                                state.touch_drag = Some(TouchDrag {
647                                    start_position: event.start_position,
648                                    is_start,
649                                });
650                            }
651                            let Some(drag) = state.touch_drag else {
652                                return;
653                            };
654                            if drag.start_position != event.start_position {
655                                return;
656                            }
657                            cx.stop_propagation();
658                            match event.phase {
659                                TouchPhase::Started | TouchPhase::Moved => {
660                                    state.update_value_by_position(
661                                        axis,
662                                        event.position,
663                                        drag.is_start,
664                                        window,
665                                        cx,
666                                    );
667                                }
668                                TouchPhase::Ended | TouchPhase::Cancelled => {
669                                    state.touch_drag = None;
670                                    state.handle_release(cx);
671                                }
672                            }
673                        });
674                    });
675                },
676            )
677            .absolute()
678            .size_full()
679        });
680        self.base
681            .id("slider-bar-container")
682            .test_support()
683            .children(self.children)
684            .children(touch_layer)
685            .when(!self.disabled, |this| {
686                this.on_mouse_down(
687                    MouseButton::Left,
688                    window.listener_for(
689                        &self.state,
690                        move |state, event: &MouseDownEvent, window, cx| {
691                            let is_start = if is_range {
692                                let size = state.bounds().size.along(axis);
693                                let position = if axis.is_horizontal() {
694                                    event.position.x - state.bounds().left()
695                                } else {
696                                    state.bounds().bottom() - event.position.y
697                                };
698                                let center = ((percentage.end - percentage.start) / 2.
699                                    + percentage.start)
700                                    * size;
701                                position < center
702                            } else {
703                                false
704                            };
705                            state.update_value_by_position(
706                                axis,
707                                event.position,
708                                is_start,
709                                window,
710                                cx,
711                            );
712                        },
713                    ),
714                )
715                .when(!is_range, |this| {
716                    this.on_drag(DragSlider(entity_id), |drag, _, _, cx| {
717                        cx.stop_propagation();
718                        cx.new(|_| drag.clone())
719                    })
720                    .on_drag_move(window.listener_for(
721                        &self.state,
722                        move |state, event: &DragMoveEvent<DragSlider>, window, cx| {
723                            let DragSlider(id) = event.drag(cx);
724                            if *id == entity_id {
725                                state.update_value_by_position(
726                                    axis,
727                                    event.event.position,
728                                    false,
729                                    window,
730                                    cx,
731                                );
732                            }
733                        },
734                    ))
735                })
736            })
737    }
738}
739
740/// An unstyled slider indicator that records the value-mapping bounds.
741#[derive(IntoElement)]
742pub struct SliderIndicator {
743    state: Entity<SliderState>,
744    base: Div,
745    children: Vec<AnyElement>,
746}
747
748impl SliderIndicator {
749    pub fn new(state: &Entity<SliderState>) -> Self {
750        Self {
751            state: state.clone(),
752            base: div(),
753            children: Vec::new(),
754        }
755    }
756}
757
758impl ParentElement for SliderIndicator {
759    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
760        self.children.extend(elements);
761    }
762}
763
764impl Styled for SliderIndicator {
765    fn style(&mut self) -> &mut StyleRefinement {
766        self.base.style()
767    }
768}
769
770impl InteractiveElement for SliderIndicator {
771    fn interactivity(&mut self) -> &mut gpui::Interactivity {
772        self.base.interactivity()
773    }
774}
775
776impl StatefulInteractiveElement for SliderIndicator {}
777
778impl RenderOnce for SliderIndicator {
779    fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
780        self.base
781            .id("slider-bar")
782            .children(self.children)
783            .on_prepaint({
784                let state = self.state;
785                // Only when they moved: with retained views, an update while
786                // the window draws renders every view that reads the state
787                // again.
788                move |bounds, _, cx| {
789                    if state.read(cx).bounds != bounds {
790                        state.update(cx, |state, _| state.set_bounds(bounds));
791                    }
792                }
793            })
794    }
795}
796
797/// An unstyled draggable slider thumb.
798#[derive(IntoElement)]
799pub struct SliderThumb {
800    state: Entity<SliderState>,
801    axis: Axis,
802    start: bool,
803    disabled: bool,
804    base: Div,
805    children: Vec<AnyElement>,
806}
807
808impl SliderThumb {
809    pub fn new(state: &Entity<SliderState>) -> Self {
810        Self {
811            state: state.clone(),
812            axis: Axis::Horizontal,
813            start: false,
814            disabled: false,
815            base: div(),
816            children: Vec::new(),
817        }
818    }
819
820    pub fn axis(mut self, axis: Axis) -> Self {
821        self.axis = axis;
822        self
823    }
824    pub fn start(mut self, start: bool) -> Self {
825        self.start = start;
826        self
827    }
828    pub fn disabled(mut self, disabled: bool) -> Self {
829        self.disabled = disabled;
830        self
831    }
832}
833
834impl ParentElement for SliderThumb {
835    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
836        self.children.extend(elements);
837    }
838}
839
840impl Styled for SliderThumb {
841    fn style(&mut self) -> &mut StyleRefinement {
842        self.base.style()
843    }
844}
845
846impl InteractiveElement for SliderThumb {
847    fn interactivity(&mut self) -> &mut gpui::Interactivity {
848        self.base.interactivity()
849    }
850}
851
852impl StatefulInteractiveElement for SliderThumb {}
853
854impl RenderOnce for SliderThumb {
855    fn render(self, window: &mut Window, _: &mut App) -> impl IntoElement {
856        let entity_id = self.state.entity_id();
857        let axis = self.axis;
858        let start = self.start;
859        self.base
860            .id(("slider-thumb", start as u32))
861            .test_support()
862            .children(self.children)
863            .when(!self.disabled, |this| {
864                this.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
865                    .on_drag(DragThumb((entity_id, start)), |drag, _, _, cx| {
866                        cx.stop_propagation();
867                        cx.new(|_| drag.clone())
868                    })
869                    .on_drag_move(window.listener_for(
870                        &self.state,
871                        move |state, event: &DragMoveEvent<DragThumb>, window, cx| {
872                            let DragThumb((id, start)) = event.drag(cx);
873                            if *id == entity_id {
874                                state.update_value_by_position(
875                                    axis,
876                                    event.event.position,
877                                    *start,
878                                    window,
879                                    cx,
880                                );
881                            }
882                        },
883                    ))
884            })
885    }
886}
887
888impl EventEmitter<SliderEvent> for SliderState {}
889
890impl SliderState {
891    #[doc(hidden)]
892    pub fn percentage(&self) -> Range<f32> {
893        self.percentage.clone()
894    }
895
896    #[doc(hidden)]
897    pub fn bounds(&self) -> Bounds<Pixels> {
898        self.bounds
899    }
900
901    #[doc(hidden)]
902    pub fn set_bounds(&mut self, bounds: Bounds<Pixels>) {
903        self.bounds = bounds;
904    }
905}
906
907#[cfg(test)]
908mod tests {
909    use super::*;
910
911    #[test]
912    fn legacy_value_conversions_and_clamping_are_preserved() {
913        assert_eq!(SliderValue::from(5.), SliderValue::Single(5.));
914        assert_eq!(SliderValue::from((2., 8.)), SliderValue::Range(2., 8.));
915        assert_eq!(SliderValue::from(2.0..8.0), SliderValue::Range(2., 8.));
916        assert_eq!(
917            SliderValue::Range(-1., 12.).clamp(0., 10.),
918            SliderValue::Range(0., 10.)
919        );
920    }
921
922    #[test]
923    fn legacy_linear_state_keeps_percentage_and_range_ordering() {
924        let state = SliderState::new()
925            .min(0.)
926            .max(200.)
927            .default_value((50., 150.));
928        assert_eq!(state.value(), SliderValue::Range(50., 150.));
929        assert_eq!(state.percentage(), 0.25..0.75);
930    }
931
932    #[test]
933    fn legacy_logarithmic_state_keeps_mapping() {
934        let state = SliderState::new()
935            .min(1.)
936            .max(1000.)
937            .scale(SliderScale::Logarithmic)
938            .default_value(10.);
939        let percentage = state.percentage().end;
940        assert!((percentage - (1. / 3.)).abs() < 0.0001);
941    }
942
943    #[test]
944    #[should_panic(expected = "`min` must be greater than 0")]
945    fn legacy_logarithmic_validation_is_preserved() {
946        let _ = SliderState::new().scale(SliderScale::Logarithmic);
947    }
948
949    /// A slider's bar beside a view that reads its state, under retained views.
950    #[cfg(feature = "gpui-fast")]
951    mod retained {
952        use std::{cell::Cell, rc::Rc};
953
954        use gpui::{
955            AppContext as _, Context, Entity, IntoElement, ParentElement as _, Render, Styled as _,
956            TestAppContext, Window, div, px,
957        };
958
959        use super::super::{SliderIndicator, SliderState};
960
961        /// A view that shows a slider's value, counting its renders.
962        struct Readout {
963            state: Entity<SliderState>,
964            renders: Rc<Cell<usize>>,
965        }
966
967        impl Render for Readout {
968            fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
969                self.renders.set(self.renders.get() + 1);
970                div().child(self.state.read(cx).value().to_string())
971            }
972        }
973
974        /// A slider's bar in a view of its own.
975        struct Bar {
976            state: Entity<SliderState>,
977        }
978
979        impl Render for Bar {
980            fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
981                div()
982                    .w(px(120.))
983                    .h(px(24.))
984                    .child(SliderIndicator::new(&self.state).size_full())
985            }
986        }
987
988        /// The bar and the readout side by side.
989        struct Pair {
990            bar: Entity<Bar>,
991            readout: Entity<Readout>,
992        }
993
994        impl Render for Pair {
995            fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
996                div()
997                    .flex()
998                    .child(self.bar.clone())
999                    .child(self.readout.clone())
1000            }
1001        }
1002
1003        /// Drawing a slider's bar again where it was changes nothing in the
1004        /// slider's state: a view beside it that reads the state is not built
1005        /// again for it.
1006        #[gpui::test]
1007        fn drawing_a_slider_again_leaves_its_readers_retained(cx: &mut TestAppContext) {
1008            let renders = Rc::new(Cell::new(0));
1009            let (view, cx) = cx.add_window_view({
1010                let renders = renders.clone();
1011                move |_, cx| {
1012                    let state = cx.new(|_| SliderState::new());
1013                    Pair {
1014                        bar: cx.new(|_| Bar {
1015                            state: state.clone(),
1016                        }),
1017                        readout: cx.new(|_| Readout { state, renders }),
1018                    }
1019                }
1020            });
1021            cx.update(|window, cx| window.draw(cx).clear(cx));
1022            cx.update(|window, cx| window.draw(cx).clear(cx));
1023            let rendered = renders.get();
1024            for _ in 0..3 {
1025                view.update(cx, |pair, cx| pair.bar.update(cx, |_, cx| cx.notify()));
1026                cx.update(|window, cx| window.draw(cx).clear(cx));
1027            }
1028            cx.update(|window, cx| window.draw(cx).clear(cx));
1029            assert_eq!(renders.get(), rendered, "the readout was built again");
1030        }
1031    }
1032}