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, InteractiveElement, IntoElement,
7    MouseButton, MouseDownEvent, Orientation, ParentElement, Pixels, Point, Render, RenderOnce,
8    Role, StatefulInteractiveElement, StyleRefinement, Styled, Window, div,
9    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}
184
185impl SliderState {
186    /// Create a new [`SliderState`].
187    pub fn new() -> Self {
188        Self {
189            min: 0.0,
190            max: 100.0,
191            step: 1.0,
192            value: SliderValue::default(),
193            percentage: (0.0..0.0),
194            bounds: Bounds::default(),
195            scale: SliderScale::default(),
196            dragging: false,
197        }
198    }
199
200    /// Set the minimum value of the slider, default: 0.0
201    pub fn min(mut self, min: f32) -> Self {
202        if self.scale.is_logarithmic() {
203            assert!(
204                min > 0.0,
205                "`min` must be greater than 0 for SliderScale::Logarithmic"
206            );
207            assert!(
208                min < self.max,
209                "`min` must be less than `max` for Logarithmic scale"
210            );
211        }
212        self.min = min;
213        self.update_thumb_pos();
214        self
215    }
216
217    /// Set the maximum value of the slider, default: 100.0
218    pub fn max(mut self, max: f32) -> Self {
219        if self.scale.is_logarithmic() {
220            assert!(
221                max > self.min,
222                "`max` must be greater than `min` for Logarithmic scale"
223            );
224        }
225        self.max = max;
226        self.update_thumb_pos();
227        self
228    }
229
230    /// Set the step value of the slider, default: 1.0
231    pub fn step(mut self, step: f32) -> Self {
232        self.step = step;
233        self
234    }
235
236    /// Set the scale of the slider, default: [`SliderScale::Linear`].
237    pub fn scale(mut self, scale: SliderScale) -> Self {
238        if scale.is_logarithmic() {
239            assert!(
240                self.min > 0.0,
241                "`min` must be greater than 0 for Logarithmic scale"
242            );
243            assert!(
244                self.max > self.min,
245                "`max` must be greater than `min` for Logarithmic scale"
246            );
247        }
248        self.scale = scale;
249        self.update_thumb_pos();
250        self
251    }
252
253    /// Set the default value of the slider, default: 0.0
254    pub fn default_value(mut self, value: impl Into<SliderValue>) -> Self {
255        self.value = value.into();
256        self.update_thumb_pos();
257        self
258    }
259
260    /// Set the value of the slider.
261    pub fn set_value(
262        &mut self,
263        value: impl Into<SliderValue>,
264        _: &mut Window,
265        cx: &mut Context<Self>,
266    ) {
267        self.value = value.into();
268        self.update_thumb_pos();
269        cx.notify();
270    }
271
272    /// Get the value of the slider.
273    pub fn value(&self) -> SliderValue {
274        self.value
275    }
276
277    /// Get the minimum value.
278    pub fn min_value(&self) -> f32 {
279        self.min
280    }
281
282    /// Get the maximum value.
283    pub fn max_value(&self) -> f32 {
284        self.max
285    }
286
287    /// Get the step value.
288    pub fn step_value(&self) -> f32 {
289        self.step
290    }
291
292    /// Converts a value between 0.0 and 1.0 to a value between the minimum and maximum value,
293    /// depending on the chosen scale.
294    fn percentage_to_value(&self, percentage: f32) -> f32 {
295        match self.scale {
296            SliderScale::Linear => self.min + (self.max - self.min) * percentage,
297            SliderScale::Logarithmic => {
298                // when percentage is 0, this simplifies to (max/min)^0 * min = 1 * min = min
299                // when percentage is 1, this simplifies to (max/min)^1 * min = (max*min)/min = max
300                // we clamp just to make sure we don't have issue with floating point precision
301                let base = self.max / self.min;
302                (base.powf(percentage) * self.min).clamp(self.min, self.max)
303            }
304        }
305    }
306
307    /// Converts a value between the minimum and maximum value to a value between 0.0 and 1.0,
308    /// depending on the chosen scale.
309    fn value_to_percentage(&self, value: f32) -> f32 {
310        match self.scale {
311            SliderScale::Linear => {
312                let range = self.max - self.min;
313                if range <= 0.0 {
314                    0.0
315                } else {
316                    (value - self.min) / range
317                }
318            }
319            SliderScale::Logarithmic => {
320                let base = self.max / self.min;
321                (value / self.min).log(base).clamp(0.0, 1.0)
322            }
323        }
324    }
325
326    fn update_thumb_pos(&mut self) {
327        match self.value {
328            SliderValue::Single(value) => {
329                let percentage = self.value_to_percentage(value.clamp(self.min, self.max));
330                self.percentage = 0.0..percentage;
331            }
332            SliderValue::Range(start, end) => {
333                let clamped_start = start.clamp(self.min, self.max);
334                let clamped_end = end.clamp(self.min, self.max);
335                self.percentage =
336                    self.value_to_percentage(clamped_start)..self.value_to_percentage(clamped_end);
337            }
338        }
339    }
340
341    /// Update value by mouse position
342    #[doc(hidden)]
343    pub fn update_value_by_position(
344        &mut self,
345        axis: Axis,
346        position: Point<Pixels>,
347        is_start: bool,
348        _: &mut Window,
349        cx: &mut Context<Self>,
350    ) {
351        self.dragging = true;
352        let bounds = self.bounds;
353        let step = self.step;
354
355        let inner_pos = if axis.is_horizontal() {
356            position.x - bounds.left()
357        } else {
358            bounds.bottom() - position.y
359        };
360        let total_size = bounds.size.along(axis);
361        let percentage = inner_pos.clamp(px(0.), total_size) / total_size;
362
363        let percentage = if is_start {
364            percentage.clamp(0.0, self.percentage.end)
365        } else {
366            percentage.clamp(self.percentage.start, 1.0)
367        };
368
369        let value = self.percentage_to_value(percentage);
370        let value = (value / step).round() * step;
371
372        if is_start {
373            self.percentage.start = percentage;
374            self.value.set_start(value);
375        } else {
376            self.percentage.end = percentage;
377            self.value.set_end(value);
378        }
379        cx.emit(SliderEvent::Change(self.value));
380        cx.notify();
381    }
382
383    /// Emit [`SliderEvent::Release`] if the user was actively interacting
384    /// with the slider. Called on mouse-up both inside and outside the slider.
385    #[doc(hidden)]
386    pub fn handle_release(&mut self, cx: &mut Context<Self>) {
387        if !self.dragging {
388            return;
389        }
390        self.dragging = false;
391        cx.emit(SliderEvent::Release(self.value));
392    }
393}
394
395#[derive(Clone)]
396struct DragThumb((EntityId, bool));
397
398impl Render for DragThumb {
399    fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
400        Empty
401    }
402}
403
404#[derive(Clone)]
405struct DragSlider(EntityId);
406
407impl Render for DragSlider {
408    fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
409        Empty
410    }
411}
412
413/// An unstyled slider behavior root.
414///
415/// Applications provide the track, range, and thumb presentation as children.
416#[derive(IntoElement)]
417pub struct Slider {
418    state: Entity<SliderState>,
419    axis: Axis,
420    disabled: bool,
421    base: Div,
422    children: Vec<AnyElement>,
423}
424
425impl Slider {
426    pub fn new(state: &Entity<SliderState>) -> Self {
427        Self {
428            state: state.clone(),
429            axis: Axis::Horizontal,
430            disabled: false,
431            base: div(),
432            children: Vec::new(),
433        }
434    }
435
436    pub fn horizontal(mut self) -> Self {
437        self.axis = Axis::Horizontal;
438        self
439    }
440
441    pub fn vertical(mut self) -> Self {
442        self.axis = Axis::Vertical;
443        self
444    }
445
446    pub fn axis(mut self, axis: Axis) -> Self {
447        self.axis = axis;
448        self
449    }
450
451    pub fn disabled(mut self, disabled: bool) -> Self {
452        self.disabled = disabled;
453        self
454    }
455}
456
457impl ParentElement for Slider {
458    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
459        self.children.extend(elements);
460    }
461}
462
463impl Styled for Slider {
464    fn style(&mut self) -> &mut StyleRefinement {
465        self.base.style()
466    }
467}
468
469impl RenderOnce for Slider {
470    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
471        let axis = self.axis;
472        let entity_id = self.state.entity_id();
473        let state = self.state.read(cx);
474        let slider_state = self.state.clone();
475
476        self.base
477            .id(("slider", entity_id))
478            .test_support()
479            .role(Role::Slider)
480            .aria_numeric_value(state.value().end() as f64)
481            .aria_min_numeric_value(state.min_value() as f64)
482            .aria_max_numeric_value(state.max_value() as f64)
483            .aria_numeric_value_step(state.step_value() as f64)
484            .aria_orientation(if axis.is_vertical() {
485                Orientation::Vertical
486            } else {
487                Orientation::Horizontal
488            })
489            .on_a11y_action(AccessibleAction::Increment, {
490                let state = slider_state.clone();
491                move |_, window, cx| {
492                    state.update(cx, |state, cx| {
493                        let value =
494                            (state.value().end() + state.step_value()).min(state.max_value());
495                        state.set_value(value, window, cx);
496                    });
497                }
498            })
499            .on_a11y_action(AccessibleAction::Decrement, {
500                let state = slider_state.clone();
501                move |_, window, cx| {
502                    state.update(cx, |state, cx| {
503                        let value =
504                            (state.value().end() - state.step_value()).max(state.min_value());
505                        state.set_value(value, window, cx);
506                    });
507                }
508            })
509            .when(!self.disabled, |this| {
510                this.on_mouse_up(
511                    MouseButton::Left,
512                    window.listener_for(&self.state, |state, _, _, cx| state.handle_release(cx)),
513                )
514                .on_mouse_up_out(
515                    MouseButton::Left,
516                    window.listener_for(&self.state, |state, _, _, cx| state.handle_release(cx)),
517                )
518            })
519            .children(self.children)
520    }
521}
522
523/// An unstyled track that records the geometry used to map pointer positions.
524#[derive(IntoElement)]
525pub struct SliderTrack {
526    state: Entity<SliderState>,
527    axis: Axis,
528    disabled: bool,
529    base: Div,
530    children: Vec<AnyElement>,
531}
532
533impl SliderTrack {
534    pub fn new(state: &Entity<SliderState>) -> Self {
535        Self {
536            state: state.clone(),
537            axis: Axis::Horizontal,
538            disabled: false,
539            base: div(),
540            children: Vec::new(),
541        }
542    }
543
544    pub fn axis(mut self, axis: Axis) -> Self {
545        self.axis = axis;
546        self
547    }
548    pub fn disabled(mut self, disabled: bool) -> Self {
549        self.disabled = disabled;
550        self
551    }
552}
553
554impl ParentElement for SliderTrack {
555    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
556        self.children.extend(elements);
557    }
558}
559
560impl Styled for SliderTrack {
561    fn style(&mut self) -> &mut StyleRefinement {
562        self.base.style()
563    }
564}
565
566impl InteractiveElement for SliderTrack {
567    fn interactivity(&mut self) -> &mut gpui::Interactivity {
568        self.base.interactivity()
569    }
570}
571
572impl RenderOnce for SliderTrack {
573    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
574        let axis = self.axis;
575        let entity_id = self.state.entity_id();
576        let state = self.state.read(cx);
577        let is_range = state.value().is_range();
578        let percentage = state.percentage();
579        self.base
580            .id("slider-bar-container")
581            .test_support()
582            .children(self.children)
583            .when(!self.disabled, |this| {
584                this.on_mouse_down(
585                    MouseButton::Left,
586                    window.listener_for(
587                        &self.state,
588                        move |state, event: &MouseDownEvent, window, cx| {
589                            let is_start = if is_range {
590                                let size = state.bounds().size.along(axis);
591                                let position = if axis.is_horizontal() {
592                                    event.position.x - state.bounds().left()
593                                } else {
594                                    state.bounds().bottom() - event.position.y
595                                };
596                                let center = ((percentage.end - percentage.start) / 2.
597                                    + percentage.start)
598                                    * size;
599                                position < center
600                            } else {
601                                false
602                            };
603                            state.update_value_by_position(
604                                axis,
605                                event.position,
606                                is_start,
607                                window,
608                                cx,
609                            );
610                        },
611                    ),
612                )
613                .when(!is_range, |this| {
614                    this.on_drag(DragSlider(entity_id), |drag, _, _, cx| {
615                        cx.stop_propagation();
616                        cx.new(|_| drag.clone())
617                    })
618                    .on_drag_move(window.listener_for(
619                        &self.state,
620                        move |state, event: &DragMoveEvent<DragSlider>, window, cx| {
621                            let DragSlider(id) = event.drag(cx);
622                            if *id == entity_id {
623                                state.update_value_by_position(
624                                    axis,
625                                    event.event.position,
626                                    false,
627                                    window,
628                                    cx,
629                                );
630                            }
631                        },
632                    ))
633                })
634            })
635    }
636}
637
638/// An unstyled slider indicator that records the value-mapping bounds.
639#[derive(IntoElement)]
640pub struct SliderIndicator {
641    state: Entity<SliderState>,
642    base: Div,
643    children: Vec<AnyElement>,
644}
645
646impl SliderIndicator {
647    pub fn new(state: &Entity<SliderState>) -> Self {
648        Self {
649            state: state.clone(),
650            base: div(),
651            children: Vec::new(),
652        }
653    }
654}
655
656impl ParentElement for SliderIndicator {
657    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
658        self.children.extend(elements);
659    }
660}
661
662impl Styled for SliderIndicator {
663    fn style(&mut self) -> &mut StyleRefinement {
664        self.base.style()
665    }
666}
667
668impl InteractiveElement for SliderIndicator {
669    fn interactivity(&mut self) -> &mut gpui::Interactivity {
670        self.base.interactivity()
671    }
672}
673
674impl StatefulInteractiveElement for SliderIndicator {}
675
676impl RenderOnce for SliderIndicator {
677    fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
678        self.base
679            .id("slider-bar")
680            .children(self.children)
681            .on_prepaint({
682                let state = self.state;
683                move |bounds, _, cx| state.update(cx, |state, _| state.set_bounds(bounds))
684            })
685    }
686}
687
688/// An unstyled draggable slider thumb.
689#[derive(IntoElement)]
690pub struct SliderThumb {
691    state: Entity<SliderState>,
692    axis: Axis,
693    start: bool,
694    disabled: bool,
695    base: Div,
696    children: Vec<AnyElement>,
697}
698
699impl SliderThumb {
700    pub fn new(state: &Entity<SliderState>) -> Self {
701        Self {
702            state: state.clone(),
703            axis: Axis::Horizontal,
704            start: false,
705            disabled: false,
706            base: div(),
707            children: Vec::new(),
708        }
709    }
710
711    pub fn axis(mut self, axis: Axis) -> Self {
712        self.axis = axis;
713        self
714    }
715    pub fn start(mut self, start: bool) -> Self {
716        self.start = start;
717        self
718    }
719    pub fn disabled(mut self, disabled: bool) -> Self {
720        self.disabled = disabled;
721        self
722    }
723}
724
725impl ParentElement for SliderThumb {
726    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
727        self.children.extend(elements);
728    }
729}
730
731impl Styled for SliderThumb {
732    fn style(&mut self) -> &mut StyleRefinement {
733        self.base.style()
734    }
735}
736
737impl InteractiveElement for SliderThumb {
738    fn interactivity(&mut self) -> &mut gpui::Interactivity {
739        self.base.interactivity()
740    }
741}
742
743impl StatefulInteractiveElement for SliderThumb {}
744
745impl RenderOnce for SliderThumb {
746    fn render(self, window: &mut Window, _: &mut App) -> impl IntoElement {
747        let entity_id = self.state.entity_id();
748        let axis = self.axis;
749        let start = self.start;
750        self.base
751            .id(("slider-thumb", start as u32))
752            .test_support()
753            .children(self.children)
754            .when(!self.disabled, |this| {
755                this.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
756                    .on_drag(DragThumb((entity_id, start)), |drag, _, _, cx| {
757                        cx.stop_propagation();
758                        cx.new(|_| drag.clone())
759                    })
760                    .on_drag_move(window.listener_for(
761                        &self.state,
762                        move |state, event: &DragMoveEvent<DragThumb>, window, cx| {
763                            let DragThumb((id, start)) = event.drag(cx);
764                            if *id == entity_id {
765                                state.update_value_by_position(
766                                    axis,
767                                    event.event.position,
768                                    *start,
769                                    window,
770                                    cx,
771                                );
772                            }
773                        },
774                    ))
775            })
776    }
777}
778
779impl EventEmitter<SliderEvent> for SliderState {}
780
781impl SliderState {
782    #[doc(hidden)]
783    pub fn percentage(&self) -> Range<f32> {
784        self.percentage.clone()
785    }
786
787    #[doc(hidden)]
788    pub fn bounds(&self) -> Bounds<Pixels> {
789        self.bounds
790    }
791
792    #[doc(hidden)]
793    pub fn set_bounds(&mut self, bounds: Bounds<Pixels>) {
794        self.bounds = bounds;
795    }
796}
797
798#[cfg(test)]
799mod tests {
800    use super::*;
801
802    #[test]
803    fn legacy_value_conversions_and_clamping_are_preserved() {
804        assert_eq!(SliderValue::from(5.), SliderValue::Single(5.));
805        assert_eq!(SliderValue::from((2., 8.)), SliderValue::Range(2., 8.));
806        assert_eq!(SliderValue::from(2.0..8.0), SliderValue::Range(2., 8.));
807        assert_eq!(
808            SliderValue::Range(-1., 12.).clamp(0., 10.),
809            SliderValue::Range(0., 10.)
810        );
811    }
812
813    #[test]
814    fn legacy_linear_state_keeps_percentage_and_range_ordering() {
815        let state = SliderState::new()
816            .min(0.)
817            .max(200.)
818            .default_value((50., 150.));
819        assert_eq!(state.value(), SliderValue::Range(50., 150.));
820        assert_eq!(state.percentage(), 0.25..0.75);
821    }
822
823    #[test]
824    fn legacy_logarithmic_state_keeps_mapping() {
825        let state = SliderState::new()
826            .min(1.)
827            .max(1000.)
828            .scale(SliderScale::Logarithmic)
829            .default_value(10.);
830        let percentage = state.percentage().end;
831        assert!((percentage - (1. / 3.)).abs() < 0.0001);
832    }
833
834    #[test]
835    #[should_panic(expected = "`min` must be greater than 0")]
836    fn legacy_logarithmic_validation_is_preserved() {
837        let _ = SliderState::new().scale(SliderScale::Logarithmic);
838    }
839}