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, FocusHandle, HitboxBehavior,
7 InteractiveElement, IntoElement, MouseButton, MouseDownEvent, Orientation, ParentElement,
8 Pixels, Point, Render, RenderOnce, Role, SharedString, StatefulInteractiveElement,
9 StyleRefinement, Styled, TouchDragEvent, TouchPhase, Window, div, prelude::FluentBuilder as _,
10 px,
11};
12
13use crate::{a11y_disabled::A11yDisabledExt as _, element_ext::ElementExt, geometry::AxisExt};
14
15pub enum SliderEvent {
17 Change(SliderValue),
19 Release(SliderValue),
21}
22
23#[derive(Clone, Copy, Debug, PartialEq)]
30pub enum SliderValue {
31 Single(f32),
32 Range(f32, f32),
33}
34
35impl std::fmt::Display for SliderValue {
36 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37 match self {
38 SliderValue::Single(value) => write!(f, "{}", value),
39 SliderValue::Range(start, end) => write!(f, "{}..{}", start, end),
40 }
41 }
42}
43
44impl From<f32> for SliderValue {
45 fn from(value: f32) -> Self {
46 SliderValue::Single(value)
47 }
48}
49
50impl From<(f32, f32)> for SliderValue {
51 fn from(value: (f32, f32)) -> Self {
52 SliderValue::Range(value.0, value.1)
53 }
54}
55
56impl From<Range<f32>> for SliderValue {
57 fn from(value: Range<f32>) -> Self {
58 SliderValue::Range(value.start, value.end)
59 }
60}
61
62impl Default for SliderValue {
63 fn default() -> Self {
64 SliderValue::Single(0.)
65 }
66}
67
68impl SliderValue {
69 pub fn clamp(self, min: f32, max: f32) -> Self {
71 match self {
72 SliderValue::Single(value) => SliderValue::Single(value.clamp(min, max)),
73 SliderValue::Range(start, end) => {
74 SliderValue::Range(start.clamp(min, max), end.clamp(min, max))
75 }
76 }
77 }
78
79 #[inline]
81 pub fn is_single(&self) -> bool {
82 matches!(self, SliderValue::Single(_))
83 }
84
85 #[inline]
87 pub fn is_range(&self) -> bool {
88 matches!(self, SliderValue::Range(_, _))
89 }
90
91 pub fn start(&self) -> f32 {
93 match self {
94 SliderValue::Single(value) => *value,
95 SliderValue::Range(start, _) => *start,
96 }
97 }
98
99 pub fn end(&self) -> f32 {
101 match self {
102 SliderValue::Single(value) => *value,
103 SliderValue::Range(_, end) => *end,
104 }
105 }
106
107 fn set_start(&mut self, value: f32) {
108 if let SliderValue::Range(_, end) = self {
109 *self = SliderValue::Range(value.min(*end), *end);
110 } else {
111 *self = SliderValue::Single(value);
112 }
113 }
114
115 fn set_end(&mut self, value: f32) {
116 if let SliderValue::Range(start, _) = self {
117 *self = SliderValue::Range(*start, value.max(*start));
118 } else {
119 *self = SliderValue::Single(value);
120 }
121 }
122}
123
124#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
126pub enum SliderScale {
127 #[default]
130 Linear,
131 Logarithmic,
156}
157
158impl SliderScale {
159 #[inline]
160 pub fn is_linear(&self) -> bool {
161 matches!(self, SliderScale::Linear)
162 }
163
164 #[inline]
165 pub fn is_logarithmic(&self) -> bool {
166 matches!(self, SliderScale::Logarithmic)
167 }
168}
169
170pub struct SliderState {
172 min: f32,
173 max: f32,
174 step: f32,
175 value: SliderValue,
176 percentage: Range<f32>,
178 bounds: Bounds<Pixels>,
180 scale: SliderScale,
181 dragging: bool,
184 touch_drag: Option<TouchDrag>,
186}
187
188#[derive(Clone, Copy)]
190struct TouchDrag {
191 start_position: Point<Pixels>,
193 is_start: bool,
195}
196
197impl SliderState {
198 pub fn new() -> Self {
200 Self {
201 min: 0.0,
202 max: 100.0,
203 step: 1.0,
204 value: SliderValue::default(),
205 percentage: (0.0..0.0),
206 bounds: Bounds::default(),
207 scale: SliderScale::default(),
208 dragging: false,
209 touch_drag: None,
210 }
211 }
212
213 pub fn min(mut self, min: f32) -> Self {
215 if self.scale.is_logarithmic() {
216 assert!(
217 min > 0.0,
218 "`min` must be greater than 0 for SliderScale::Logarithmic"
219 );
220 assert!(
221 min < self.max,
222 "`min` must be less than `max` for Logarithmic scale"
223 );
224 }
225 self.min = min;
226 self.update_thumb_pos();
227 self
228 }
229
230 pub fn max(mut self, max: f32) -> Self {
232 if self.scale.is_logarithmic() {
233 assert!(
234 max > self.min,
235 "`max` must be greater than `min` for Logarithmic scale"
236 );
237 }
238 self.max = max;
239 self.update_thumb_pos();
240 self
241 }
242
243 pub fn step(mut self, step: f32) -> Self {
245 self.step = step;
246 self
247 }
248
249 pub fn scale(mut self, scale: SliderScale) -> Self {
251 if scale.is_logarithmic() {
252 assert!(
253 self.min > 0.0,
254 "`min` must be greater than 0 for Logarithmic scale"
255 );
256 assert!(
257 self.max > self.min,
258 "`max` must be greater than `min` for Logarithmic scale"
259 );
260 }
261 self.scale = scale;
262 self.update_thumb_pos();
263 self
264 }
265
266 pub fn default_value(mut self, value: impl Into<SliderValue>) -> Self {
268 self.value = value.into();
269 self.update_thumb_pos();
270 self
271 }
272
273 pub fn set_value(
275 &mut self,
276 value: impl Into<SliderValue>,
277 _: &mut Window,
278 cx: &mut Context<Self>,
279 ) {
280 self.value = value.into();
281 self.update_thumb_pos();
282 cx.notify();
283 }
284
285 pub fn value(&self) -> SliderValue {
287 self.value
288 }
289
290 pub fn min_value(&self) -> f32 {
292 self.min
293 }
294
295 pub fn max_value(&self) -> f32 {
297 self.max
298 }
299
300 pub fn step_value(&self) -> f32 {
302 self.step
303 }
304
305 pub fn accessibility_step(&self) -> f32 {
309 if is_step(self.step) {
310 self.step
311 } else {
312 (self.max - self.min) / 100.
313 }
314 }
315
316 fn percentage_to_value(&self, percentage: f32) -> f32 {
319 match self.scale {
320 SliderScale::Linear => self.min + (self.max - self.min) * percentage,
321 SliderScale::Logarithmic => {
322 let base = self.max / self.min;
326 (base.powf(percentage) * self.min).clamp(self.min, self.max)
327 }
328 }
329 }
330
331 fn value_to_percentage(&self, value: f32) -> f32 {
334 match self.scale {
335 SliderScale::Linear => {
336 let range = self.max - self.min;
337 if range <= 0.0 {
338 0.0
339 } else {
340 (value - self.min) / range
341 }
342 }
343 SliderScale::Logarithmic => {
344 let base = self.max / self.min;
345 (value / self.min).log(base).clamp(0.0, 1.0)
346 }
347 }
348 }
349
350 fn update_thumb_pos(&mut self) {
351 match self.value {
352 SliderValue::Single(value) => {
353 let percentage = self.value_to_percentage(value.min(self.max).max(self.min));
354 self.percentage = 0.0..percentage;
355 }
356 SliderValue::Range(start, end) => {
357 let clamped_start = start.min(self.max).max(self.min);
360 let clamped_end = end.min(self.max).max(self.min);
361 self.percentage =
362 self.value_to_percentage(clamped_start)..self.value_to_percentage(clamped_end);
363 }
364 }
365 }
366
367 #[doc(hidden)]
369 pub fn update_value_by_position(
370 &mut self,
371 axis: Axis,
372 position: Point<Pixels>,
373 is_start: bool,
374 _: &mut Window,
375 cx: &mut Context<Self>,
376 ) {
377 self.dragging = true;
378 let bounds = self.bounds;
379 let step = self.step;
380
381 let inner_pos = if axis.is_horizontal() {
382 position.x - bounds.left()
383 } else {
384 bounds.bottom() - position.y
385 };
386 let total_size = bounds.size.along(axis);
387 let percentage = inner_pos.clamp(px(0.), total_size) / total_size;
388
389 let percentage = if is_start {
390 percentage.clamp(0.0, self.percentage.end)
391 } else {
392 percentage.clamp(self.percentage.start, 1.0)
393 };
394
395 let value = self.percentage_to_value(percentage);
396 let value = snap_to_step(value, self.min, self.max, step);
397
398 if is_start {
399 self.value.set_start(value);
400 } else {
401 self.value.set_end(value);
402 }
403 self.update_thumb_pos();
405 cx.emit(SliderEvent::Change(self.value));
406 cx.notify();
407 }
408
409 #[doc(hidden)]
414 pub fn step_thumb(
415 &mut self,
416 start: bool,
417 delta: f32,
418 window: &mut Window,
419 cx: &mut Context<Self>,
420 ) {
421 if !steppable(self.value, self.min, self.max) {
423 return;
424 }
425 let value = step_thumb(self.value, start, delta, self.min, self.max, self.step);
426 if value != self.value {
427 self.set_value(value, window, cx);
428 cx.emit(SliderEvent::Change(value));
429 }
430 }
431
432 fn is_nearer_start(&self, axis: Axis, position: Point<Pixels>) -> bool {
434 let size = self.bounds.size.along(axis);
435 let along = if axis.is_horizontal() {
436 position.x - self.bounds.left()
437 } else {
438 self.bounds.bottom() - position.y
439 };
440 let center =
441 ((self.percentage.end - self.percentage.start) / 2. + self.percentage.start) * size;
442 along < center
443 }
444
445 #[doc(hidden)]
448 pub fn handle_release(&mut self, cx: &mut Context<Self>) {
449 if !self.dragging {
450 return;
451 }
452 self.dragging = false;
453 cx.emit(SliderEvent::Release(self.value));
454 }
455}
456
457fn is_step(step: f32) -> bool {
459 step > 0. && step.is_finite()
460}
461
462fn snap_to_step(value: f32, min: f32, max: f32, step: f32) -> f32 {
465 if !is_step(step) {
466 return value;
467 }
468 (min + ((value - min) / step).round() * step)
469 .min(max)
470 .max(min)
471}
472
473fn steppable(value: SliderValue, min: f32, max: f32) -> bool {
476 let (start, end) = (value.start(), value.end());
477 min <= max && !start.is_nan() && !end.is_nan() && (!value.is_range() || start <= end)
478}
479
480fn step_thumb(
484 value: SliderValue,
485 start: bool,
486 delta: f32,
487 min: f32,
488 max: f32,
489 step: f32,
490) -> SliderValue {
491 if !steppable(value, min, max) {
492 return value;
493 }
494 let mut value = value;
495 let moves_start = start && value.is_range();
496 let current = if moves_start {
497 value.start()
498 } else {
499 value.end()
500 };
501 let next = snap_to_step(current + delta, min, max, step)
502 .min(max)
503 .max(min);
504 if moves_start {
505 value.set_start(next);
506 } else {
507 value.set_end(next);
508 }
509 value
510}
511
512#[derive(Clone)]
513struct DragThumb((EntityId, bool));
514
515impl Render for DragThumb {
516 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
517 Empty
518 }
519}
520
521#[derive(Clone)]
522struct DragSlider(EntityId);
523
524impl Render for DragSlider {
525 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
526 Empty
527 }
528}
529
530#[derive(IntoElement)]
534pub struct Slider {
535 state: Entity<SliderState>,
536 axis: Axis,
537 disabled: bool,
538 accessibility_label: Option<SharedString>,
539 focus: Option<FocusHandle>,
540 base: Div,
541 children: Vec<AnyElement>,
542}
543
544impl Slider {
545 pub fn new(state: &Entity<SliderState>) -> Self {
546 Self {
547 state: state.clone(),
548 axis: Axis::Horizontal,
549 disabled: false,
550 accessibility_label: None,
551 focus: None,
552 base: div(),
553 children: Vec::new(),
554 }
555 }
556
557 pub fn horizontal(mut self) -> Self {
558 self.axis = Axis::Horizontal;
559 self
560 }
561
562 pub fn vertical(mut self) -> Self {
563 self.axis = Axis::Vertical;
564 self
565 }
566
567 pub fn axis(mut self, axis: Axis) -> Self {
568 self.axis = axis;
569 self
570 }
571
572 pub fn disabled(mut self, disabled: bool) -> Self {
573 self.disabled = disabled;
574 self
575 }
576
577 pub fn accessibility_label(mut self, label: impl Into<SharedString>) -> Self {
579 self.accessibility_label = Some(label.into());
580 self
581 }
582
583 pub fn track_focus(mut self, focus: &FocusHandle) -> Self {
586 self.focus = Some(focus.clone());
587 self
588 }
589}
590
591impl ParentElement for Slider {
592 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
593 self.children.extend(elements);
594 }
595}
596
597impl Styled for Slider {
598 fn style(&mut self) -> &mut StyleRefinement {
599 self.base.style()
600 }
601}
602
603impl RenderOnce for Slider {
604 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
605 let axis = self.axis;
606 let entity_id = self.state.entity_id();
607 let state = self.state.read(cx);
608 let slider_state = self.state.clone();
609
610 self.base
611 .id(("slider", entity_id))
612 .test_support()
613 .when_some(self.focus, |this, focus| this.track_focus(&focus))
614 .role(Role::Slider)
615 .when_some(self.accessibility_label, |this, label| {
616 this.aria_label(label)
617 })
618 .aria_numeric_value(state.value().end() as f64)
619 .aria_min_numeric_value(state.min_value() as f64)
620 .aria_max_numeric_value(state.max_value() as f64)
621 .aria_numeric_value_step(state.accessibility_step() as f64)
622 .aria_orientation(if axis.is_vertical() {
623 Orientation::Vertical
624 } else {
625 Orientation::Horizontal
626 })
627 .when(!self.disabled, |this| {
630 this.on_a11y_action(AccessibleAction::Increment, {
631 let state = slider_state.clone();
632 move |_, window, cx| {
633 state.update(cx, |state, cx| {
634 state.step_thumb(false, state.accessibility_step(), window, cx);
635 });
636 }
637 })
638 .on_a11y_action(AccessibleAction::Decrement, {
639 let state = slider_state.clone();
640 move |_, window, cx| {
641 state.update(cx, |state, cx| {
642 state.step_thumb(false, -state.accessibility_step(), window, cx);
643 });
644 }
645 })
646 .on_mouse_up(
647 MouseButton::Left,
648 window.listener_for(&self.state, |state, _, _, cx| state.handle_release(cx)),
649 )
650 .on_mouse_up_out(
651 MouseButton::Left,
652 window.listener_for(&self.state, |state, _, _, cx| state.handle_release(cx)),
653 )
654 })
655 .children(self.children)
656 .a11y_disabled(self.disabled)
657 }
658}
659
660#[derive(IntoElement)]
662pub struct SliderTrack {
663 state: Entity<SliderState>,
664 axis: Axis,
665 disabled: bool,
666 base: Div,
667 children: Vec<AnyElement>,
668}
669
670impl SliderTrack {
671 pub fn new(state: &Entity<SliderState>) -> Self {
672 Self {
673 state: state.clone(),
674 axis: Axis::Horizontal,
675 disabled: false,
676 base: div(),
677 children: Vec::new(),
678 }
679 }
680
681 pub fn axis(mut self, axis: Axis) -> Self {
682 self.axis = axis;
683 self
684 }
685 pub fn disabled(mut self, disabled: bool) -> Self {
686 self.disabled = disabled;
687 self
688 }
689}
690
691impl ParentElement for SliderTrack {
692 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
693 self.children.extend(elements);
694 }
695}
696
697impl Styled for SliderTrack {
698 fn style(&mut self) -> &mut StyleRefinement {
699 self.base.style()
700 }
701}
702
703impl InteractiveElement for SliderTrack {
704 fn interactivity(&mut self) -> &mut gpui::Interactivity {
705 self.base.interactivity()
706 }
707}
708
709impl RenderOnce for SliderTrack {
710 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
711 let axis = self.axis;
712 let entity_id = self.state.entity_id();
713 let state = self.state.read(cx);
714 let is_range = state.value().is_range();
715 let percentage = state.percentage();
716 let touch_layer = (!self.disabled).then(|| {
720 let slider_state = self.state.clone();
721 gpui::canvas(
722 |bounds, window, _| window.insert_hitbox(bounds, HitboxBehavior::Normal),
723 move |_, hitbox, window, _| {
724 let slider_state = slider_state.clone();
725 window.on_mouse_event(move |event: &TouchDragEvent, phase, window, cx| {
726 if !phase.bubble() {
727 return;
728 }
729 slider_state.update(cx, |state, cx| {
730 if event.phase == TouchPhase::Started {
731 if state.touch_drag.take().is_some() {
734 state.handle_release(cx);
735 }
736 if window.default_prevented() || !hitbox.is_hovered(window) {
739 return;
740 }
741 window.prevent_default();
742 let is_start =
743 is_range && state.is_nearer_start(axis, event.start_position);
744 state.touch_drag = Some(TouchDrag {
745 start_position: event.start_position,
746 is_start,
747 });
748 }
749 let Some(drag) = state.touch_drag else {
750 return;
751 };
752 if drag.start_position != event.start_position {
753 return;
754 }
755 cx.stop_propagation();
756 match event.phase {
757 TouchPhase::Started | TouchPhase::Moved => {
758 state.update_value_by_position(
759 axis,
760 event.position,
761 drag.is_start,
762 window,
763 cx,
764 );
765 }
766 TouchPhase::Ended | TouchPhase::Cancelled => {
767 state.touch_drag = None;
768 state.handle_release(cx);
769 }
770 }
771 });
772 });
773 },
774 )
775 .absolute()
776 .size_full()
777 });
778 self.base
779 .id("slider-bar-container")
780 .test_support()
781 .children(self.children)
782 .children(touch_layer)
783 .when(!self.disabled, |this| {
784 this.on_mouse_down(
785 MouseButton::Left,
786 window.listener_for(
787 &self.state,
788 move |state, event: &MouseDownEvent, window, cx| {
789 let is_start = if is_range {
790 let size = state.bounds().size.along(axis);
791 let position = if axis.is_horizontal() {
792 event.position.x - state.bounds().left()
793 } else {
794 state.bounds().bottom() - event.position.y
795 };
796 let center = ((percentage.end - percentage.start) / 2.
797 + percentage.start)
798 * size;
799 position < center
800 } else {
801 false
802 };
803 state.update_value_by_position(
804 axis,
805 event.position,
806 is_start,
807 window,
808 cx,
809 );
810 },
811 ),
812 )
813 .when(!is_range, |this| {
814 this.on_drag(DragSlider(entity_id), |drag, _, _, cx| {
815 cx.stop_propagation();
816 cx.new(|_| drag.clone())
817 })
818 .on_drag_move(window.listener_for(
819 &self.state,
820 move |state, event: &DragMoveEvent<DragSlider>, window, cx| {
821 let DragSlider(id) = event.drag(cx);
822 if *id == entity_id {
823 state.update_value_by_position(
824 axis,
825 event.event.position,
826 false,
827 window,
828 cx,
829 );
830 }
831 },
832 ))
833 })
834 })
835 }
836}
837
838#[derive(IntoElement)]
840pub struct SliderIndicator {
841 state: Entity<SliderState>,
842 base: Div,
843 children: Vec<AnyElement>,
844}
845
846impl SliderIndicator {
847 pub fn new(state: &Entity<SliderState>) -> Self {
848 Self {
849 state: state.clone(),
850 base: div(),
851 children: Vec::new(),
852 }
853 }
854}
855
856impl ParentElement for SliderIndicator {
857 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
858 self.children.extend(elements);
859 }
860}
861
862impl Styled for SliderIndicator {
863 fn style(&mut self) -> &mut StyleRefinement {
864 self.base.style()
865 }
866}
867
868impl InteractiveElement for SliderIndicator {
869 fn interactivity(&mut self) -> &mut gpui::Interactivity {
870 self.base.interactivity()
871 }
872}
873
874impl StatefulInteractiveElement for SliderIndicator {}
875
876impl RenderOnce for SliderIndicator {
877 fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
878 self.base
879 .id("slider-bar")
880 .children(self.children)
881 .on_prepaint({
882 let state = self.state;
883 move |bounds, _, cx| {
887 if state.read(cx).bounds != bounds {
888 state.update(cx, |state, _| state.set_bounds(bounds));
889 }
890 }
891 })
892 }
893}
894
895#[derive(IntoElement)]
897pub struct SliderThumb {
898 state: Entity<SliderState>,
899 axis: Axis,
900 start: bool,
901 disabled: bool,
902 base: Div,
903 children: Vec<AnyElement>,
904}
905
906impl SliderThumb {
907 pub fn new(state: &Entity<SliderState>) -> Self {
908 Self {
909 state: state.clone(),
910 axis: Axis::Horizontal,
911 start: false,
912 disabled: false,
913 base: div(),
914 children: Vec::new(),
915 }
916 }
917
918 pub fn axis(mut self, axis: Axis) -> Self {
919 self.axis = axis;
920 self
921 }
922 pub fn start(mut self, start: bool) -> Self {
923 self.start = start;
924 self
925 }
926 pub fn disabled(mut self, disabled: bool) -> Self {
927 self.disabled = disabled;
928 self
929 }
930}
931
932impl ParentElement for SliderThumb {
933 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
934 self.children.extend(elements);
935 }
936}
937
938impl Styled for SliderThumb {
939 fn style(&mut self) -> &mut StyleRefinement {
940 self.base.style()
941 }
942}
943
944impl InteractiveElement for SliderThumb {
945 fn interactivity(&mut self) -> &mut gpui::Interactivity {
946 self.base.interactivity()
947 }
948}
949
950impl StatefulInteractiveElement for SliderThumb {}
951
952impl RenderOnce for SliderThumb {
953 fn render(self, window: &mut Window, _: &mut App) -> impl IntoElement {
954 let entity_id = self.state.entity_id();
955 let axis = self.axis;
956 let start = self.start;
957 self.base
958 .id(("slider-thumb", start as u32))
959 .test_support()
960 .children(self.children)
961 .when(!self.disabled, |this| {
962 this.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
963 .on_drag(DragThumb((entity_id, start)), |drag, _, _, cx| {
964 cx.stop_propagation();
965 cx.new(|_| drag.clone())
966 })
967 .on_drag_move(window.listener_for(
968 &self.state,
969 move |state, event: &DragMoveEvent<DragThumb>, window, cx| {
970 let DragThumb((id, start)) = event.drag(cx);
971 if *id == entity_id {
972 state.update_value_by_position(
973 axis,
974 event.event.position,
975 *start,
976 window,
977 cx,
978 );
979 }
980 },
981 ))
982 })
983 .a11y_disabled(self.disabled)
985 }
986}
987
988impl EventEmitter<SliderEvent> for SliderState {}
989
990impl SliderState {
991 #[doc(hidden)]
992 pub fn percentage(&self) -> Range<f32> {
993 self.percentage.clone()
994 }
995
996 #[doc(hidden)]
997 pub fn bounds(&self) -> Bounds<Pixels> {
998 self.bounds
999 }
1000
1001 #[doc(hidden)]
1002 pub fn set_bounds(&mut self, bounds: Bounds<Pixels>) {
1003 self.bounds = bounds;
1004 }
1005}
1006
1007#[cfg(test)]
1008mod tests {
1009 use super::*;
1010
1011 #[test]
1012 fn legacy_value_conversions_and_clamping_are_preserved() {
1013 assert_eq!(SliderValue::from(5.), SliderValue::Single(5.));
1014 assert_eq!(SliderValue::from((2., 8.)), SliderValue::Range(2., 8.));
1015 assert_eq!(SliderValue::from(2.0..8.0), SliderValue::Range(2., 8.));
1016 assert_eq!(
1017 SliderValue::Range(-1., 12.).clamp(0., 10.),
1018 SliderValue::Range(0., 10.)
1019 );
1020 }
1021
1022 #[test]
1023 fn legacy_linear_state_keeps_percentage_and_range_ordering() {
1024 let state = SliderState::new()
1025 .min(0.)
1026 .max(200.)
1027 .default_value((50., 150.));
1028 assert_eq!(state.value(), SliderValue::Range(50., 150.));
1029 assert_eq!(state.percentage(), 0.25..0.75);
1030 }
1031
1032 #[test]
1033 fn legacy_logarithmic_state_keeps_mapping() {
1034 let state = SliderState::new()
1035 .min(1.)
1036 .max(1000.)
1037 .scale(SliderScale::Logarithmic)
1038 .default_value(10.);
1039 let percentage = state.percentage().end;
1040 assert!((percentage - (1. / 3.)).abs() < 0.0001);
1041 }
1042
1043 #[gpui::test]
1044 fn a_disabled_thumb_marks_its_node_disabled(cx: &mut gpui::TestAppContext) {
1045 struct Thumb {
1046 state: Entity<SliderState>,
1047 disabled: bool,
1048 }
1049
1050 impl Render for Thumb {
1051 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1052 SliderThumb::new(&self.state)
1053 .role(Role::Slider)
1054 .disabled(self.disabled)
1055 }
1056 }
1057
1058 for disabled in [false, true] {
1059 let (_, cx) = cx.add_window_view(|_, cx| Thumb {
1060 state: cx.new(|_| SliderState::new()),
1061 disabled,
1062 });
1063 cx.update(|window, cx| {
1064 window.draw(cx).clear(cx);
1065 let thumb = crate::test_support::snapshots(window)
1066 .into_iter()
1067 .find(|node| node.role() == Some(Role::Slider))
1068 .expect("a thumb node");
1069 assert_eq!(thumb.disabled(), disabled.then_some(true));
1070 });
1071 }
1072 }
1073
1074 #[gpui::test]
1075 fn a_slider_takes_focus_on_its_own_node(cx: &mut gpui::TestAppContext) {
1076 struct Focusable {
1077 state: Entity<SliderState>,
1078 focus: FocusHandle,
1079 }
1080
1081 impl Render for Focusable {
1082 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1083 Slider::new(&self.state).track_focus(&self.focus)
1084 }
1085 }
1086
1087 let (view, cx) = cx.add_window_view(|_, cx| Focusable {
1088 state: cx.new(|_| SliderState::new()),
1089 focus: cx.focus_handle(),
1090 });
1091 cx.update(|window, cx| {
1092 let focus = view.read(cx).focus.clone();
1093 focus.focus(window, cx);
1094 window.draw(cx).clear(cx);
1095 let slider = crate::test_support::snapshots(window)
1096 .into_iter()
1097 .find(|node| node.role() == Some(Role::Slider))
1098 .expect("a slider node");
1099 assert_eq!(slider.focused(), Some(true));
1100 });
1101 }
1102
1103 #[test]
1104 fn min_can_be_set_above_the_default_max_before_max() {
1105 let state = SliderState::new().min(150.).max(200.).default_value(175.);
1106 assert_eq!(state.value(), SliderValue::Single(175.));
1107 assert_eq!(state.percentage(), 0.0..0.5);
1108
1109 let state = SliderState::new()
1110 .default_value((160., 180.))
1111 .min(150.)
1112 .max(200.);
1113 assert_eq!(state.percentage(), 0.2..0.6);
1114 }
1115
1116 #[test]
1117 fn dragging_snaps_from_min() {
1118 assert_eq!(snap_to_step(9.4, 3., 100., 5.), 8.);
1120 assert_eq!(snap_to_step(11., 3., 100., 5.), 13.);
1121 assert_eq!(snap_to_step(99., 3., 100., 5.), 98.);
1123 assert_eq!(snap_to_step(100., 0., 99., 10.), 99.);
1124 assert_eq!(snap_to_step(42.5, 0., 100., 0.), 42.5);
1126 assert_eq!(snap_to_step(42.5, 0., 100., -5.), 42.5);
1127 assert_eq!(snap_to_step(42.5, 0., 100., f32::NAN), 42.5);
1128 assert_eq!(snap_to_step(42.5, 0., 100., f32::INFINITY), 42.5);
1129 }
1130
1131 #[test]
1132 fn accessibility_steps_move_only_the_stepped_thumb() {
1133 assert_eq!(
1134 step_thumb(SliderValue::Range(20., 60.), false, 1., 0., 100., 1.),
1135 SliderValue::Range(20., 61.)
1136 );
1137 assert_eq!(
1138 step_thumb(SliderValue::Range(20., 20.), false, -1., 0., 100., 1.),
1139 SliderValue::Range(20., 20.),
1140 "the end never passes the start"
1141 );
1142 assert_eq!(
1143 step_thumb(SliderValue::Single(99.5), false, 1., 0., 100., 1.),
1144 SliderValue::Single(100.)
1145 );
1146 assert_eq!(
1147 step_thumb(SliderValue::Range(20., 60.), true, -1., 0., 100., 1.),
1148 SliderValue::Range(19., 60.)
1149 );
1150 assert_eq!(
1151 step_thumb(SliderValue::Range(60., 60.), true, 1., 0., 100., 1.),
1152 SliderValue::Range(60., 60.),
1153 "the start never passes the end"
1154 );
1155 }
1156
1157 #[test]
1158 fn accessibility_steps_land_on_the_step_grid() {
1159 assert_eq!(
1160 step_thumb(SliderValue::Single(33.), false, 10., 0., 100., 10.),
1161 SliderValue::Single(40.)
1162 );
1163 assert_eq!(
1164 step_thumb(SliderValue::Single(33.), false, -10., 0., 100., 10.),
1165 SliderValue::Single(20.)
1166 );
1167 assert_eq!(
1168 step_thumb(SliderValue::Single(33.), false, 0.5, 0., 100., 0.),
1169 SliderValue::Single(33.5),
1170 "a continuous slider does not snap"
1171 );
1172 }
1173
1174 #[test]
1175 fn a_continuous_slider_steps_by_a_hundredth_of_its_range() {
1176 for step in [0., -1., f32::NAN] {
1177 let state = SliderState::new().min(0.).max(200.).step(step);
1178 assert_eq!(state.accessibility_step(), 2., "step {step}");
1179 }
1180 assert_eq!(SliderState::new().step(5.).accessibility_step(), 5.);
1181 }
1182
1183 #[test]
1184 fn stepping_leaves_an_invalid_value_or_range_alone() {
1185 let nan = step_thumb(SliderValue::Single(f32::NAN), false, 1., 0., 100., 1.);
1186 assert!(nan.end().is_nan(), "a NaN value does not jump to max");
1187 assert_eq!(
1188 step_thumb(SliderValue::Single(50.), false, 1., 100., 0., 1.),
1189 SliderValue::Single(50.),
1190 "min above max"
1191 );
1192 assert_eq!(
1193 step_thumb(SliderValue::Range(60., 20.), true, 1., 0., 100., 1.),
1194 SliderValue::Range(60., 20.),
1195 "a reversed range"
1196 );
1197 }
1198
1199 struct Holder(Entity<SliderState>);
1200
1201 impl Render for Holder {
1202 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1203 div()
1204 }
1205 }
1206
1207 fn sized(
1209 state: SliderState,
1210 cx: &mut gpui::TestAppContext,
1211 ) -> (Entity<SliderState>, &mut gpui::VisualTestContext) {
1212 let (holder, cx) = cx.add_window_view(|_, cx| Holder(cx.new(|_| state)));
1213 let state = holder.read_with(cx, |holder, _| holder.0.clone());
1214 state.update(cx, |state, _| {
1215 state.set_bounds(Bounds::new(
1216 gpui::point(px(0.), px(0.)),
1217 gpui::size(px(100.), px(10.)),
1218 ));
1219 });
1220 (state, cx)
1221 }
1222
1223 fn drag(state: &Entity<SliderState>, x: f32, start: bool, cx: &mut gpui::VisualTestContext) {
1224 state.update_in(cx, |state, window, cx| {
1225 let position = gpui::point(px(x), px(0.));
1226 state.update_value_by_position(Axis::Horizontal, position, start, window, cx);
1227 })
1228 }
1229
1230 #[gpui::test]
1231 fn a_drag_snaps_to_steps_counted_from_min(cx: &mut gpui::TestAppContext) {
1232 let (state, cx) = sized(SliderState::new().min(3.).max(103.).step(5.), cx);
1233 drag(&state, 6., false, cx);
1235 assert_eq!(
1236 state.read_with(cx, |state, _| state.value()),
1237 SliderValue::Single(8.)
1238 );
1239 }
1240
1241 #[gpui::test]
1242 fn a_dropped_thumb_stands_at_its_value(cx: &mut gpui::TestAppContext) {
1243 let (state, cx) = sized(SliderState::new().step(25.), cx);
1244 drag(&state, 30., false, cx);
1245 state.read_with(cx, |state, _| {
1246 assert_eq!(state.value(), SliderValue::Single(25.));
1247 assert_eq!(state.percentage(), 0.0..0.25);
1248 });
1249
1250 state.update_in(cx, |state, window, cx| {
1251 state.set_value((25., 75.), window, cx);
1252 });
1253 drag(&state, 40., true, cx);
1254 drag(&state, 60., false, cx);
1255 state.read_with(cx, |state, _| {
1256 assert_eq!(state.value(), SliderValue::Range(50., 50.));
1257 assert_eq!(state.percentage(), 0.5..0.5);
1258 });
1259 }
1260
1261 #[gpui::test]
1262 fn a_zero_step_drags_continuously(cx: &mut gpui::TestAppContext) {
1263 for step in [0., -5., f32::NAN, f32::INFINITY] {
1264 let (state, cx) = sized(SliderState::new().step(step), cx);
1265 drag(&state, 30., false, cx);
1266 state.read_with(cx, |state, _| {
1267 let value = state.value().end();
1268 assert!((value - 30.).abs() < 1e-3, "step {step}: {value}");
1269 assert!((state.percentage().end - 0.3).abs() < 1e-5, "step {step}");
1270 });
1271 }
1272 }
1273
1274 #[gpui::test]
1275 fn an_accessibility_step_reports_a_change(cx: &mut gpui::TestAppContext) {
1276 let (state, cx) = sized(SliderState::new().default_value(99.), cx);
1277 let changes = std::rc::Rc::new(std::cell::Cell::new(0));
1278 cx.update(|_, cx| {
1279 let changes = changes.clone();
1280 cx.subscribe(&state, move |_, event: &SliderEvent, _| {
1281 assert!(matches!(event, SliderEvent::Change(_)));
1282 changes.set(changes.get() + 1);
1283 })
1284 .detach();
1285 });
1286 let step = |delta: f32, cx: &mut gpui::VisualTestContext| {
1287 state.update_in(cx, |state, window, cx| {
1288 state.step_thumb(false, delta, window, cx)
1289 })
1290 };
1291 step(1., cx);
1292 assert_eq!(changes.get(), 1);
1293 assert_eq!(state.read_with(cx, |state, _| state.value().end()), 100.);
1294 step(1., cx);
1296 assert_eq!(changes.get(), 1);
1297 step(-1., cx);
1298 assert_eq!(changes.get(), 2);
1299 }
1300
1301 #[gpui::test]
1302 fn an_accessibility_step_leaves_an_invalid_value_alone(cx: &mut gpui::TestAppContext) {
1303 let cases = [
1304 SliderState::new().default_value(f32::NAN),
1305 SliderState::new().default_value((60., 20.)),
1306 ];
1307 for state in cases {
1308 let (state, cx) = sized(state, cx);
1309 let before = state.read_with(cx, |state, _| state.value());
1310 let changes = std::rc::Rc::new(std::cell::Cell::new(0));
1311 cx.update(|_, cx| {
1312 let changes = changes.clone();
1313 cx.subscribe(&state, move |_, _: &SliderEvent, _| {
1314 changes.set(changes.get() + 1)
1315 })
1316 .detach();
1317 });
1318 for start in [false, true] {
1319 state.update_in(cx, |state, window, cx| {
1320 state.step_thumb(start, 1., window, cx)
1321 });
1322 }
1323 let after = state.read_with(cx, |state, _| state.value());
1324 assert_eq!(
1325 (after.start().to_bits(), after.end().to_bits()),
1326 (before.start().to_bits(), before.end().to_bits()),
1327 "{before:?} became {after:?}"
1328 );
1329 assert_eq!(changes.get(), 0, "{before:?} reported a change");
1330 }
1331 }
1332
1333 #[gpui::test]
1334 fn an_accessibility_step_keeps_a_range(cx: &mut gpui::TestAppContext) {
1335 let (state, cx) = sized(SliderState::new().default_value((20., 60.)), cx);
1336 let step = |start: bool, delta: f32, cx: &mut gpui::VisualTestContext| {
1337 state.update_in(cx, |state, window, cx| {
1338 state.step_thumb(start, delta, window, cx)
1339 });
1340 state.read_with(cx, |state, _| state.value())
1341 };
1342 assert_eq!(step(false, 1., cx), SliderValue::Range(20., 61.));
1343 assert_eq!(step(true, 5., cx), SliderValue::Range(25., 61.));
1344 assert_eq!(
1345 step(true, 100., cx),
1346 SliderValue::Range(61., 61.),
1347 "a thumb stops at the other"
1348 );
1349 }
1350
1351 #[gpui::test]
1352 fn a_continuous_slider_reports_a_usable_step(cx: &mut gpui::TestAppContext) {
1353 use gpui::{Element as _, accesskit, canvas};
1354 use std::{cell::Cell, rc::Rc};
1355
1356 struct Probe(Rc<Cell<Option<f64>>>);
1357
1358 impl Render for Probe {
1359 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1360 let reported = self.0.clone();
1361 canvas(
1362 move |_, window, cx| {
1363 let state = cx.new(|_| SliderState::new().step(0.));
1364 let element = Slider::new(&state).render(window, cx).into_element();
1365 let mut node = accesskit::Node::new(Role::Slider);
1366 element.write_a11y_info(&mut node);
1367 reported.set(node.numeric_value_step());
1368 },
1369 |_, _, _, _| {},
1370 )
1371 }
1372 }
1373
1374 let reported = Rc::new(Cell::new(None));
1375 let (_, cx) = cx.add_window_view({
1376 let reported = reported.clone();
1377 move |_, _| Probe(reported)
1378 });
1379 cx.update(|window, cx| window.draw(cx).clear(cx));
1380 assert_eq!(reported.get(), Some(1.), "a hundredth of 0..100");
1381 }
1382
1383 #[test]
1384 #[should_panic(expected = "`min` must be greater than 0")]
1385 fn legacy_logarithmic_validation_is_preserved() {
1386 let _ = SliderState::new().scale(SliderScale::Logarithmic);
1387 }
1388
1389 #[cfg(feature = "gpui-fast")]
1391 mod retained {
1392 use std::{cell::Cell, rc::Rc};
1393
1394 use gpui::{
1395 AppContext as _, Context, Entity, IntoElement, ParentElement as _, Render, Styled as _,
1396 TestAppContext, Window, div, px,
1397 };
1398
1399 use super::super::{SliderIndicator, SliderState};
1400
1401 struct Readout {
1403 state: Entity<SliderState>,
1404 renders: Rc<Cell<usize>>,
1405 }
1406
1407 impl Render for Readout {
1408 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1409 self.renders.set(self.renders.get() + 1);
1410 div().child(self.state.read(cx).value().to_string())
1411 }
1412 }
1413
1414 struct Bar {
1416 state: Entity<SliderState>,
1417 }
1418
1419 impl Render for Bar {
1420 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1421 div()
1422 .w(px(120.))
1423 .h(px(24.))
1424 .child(SliderIndicator::new(&self.state).size_full())
1425 }
1426 }
1427
1428 struct Pair {
1430 bar: Entity<Bar>,
1431 readout: Entity<Readout>,
1432 }
1433
1434 impl Render for Pair {
1435 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1436 div()
1437 .flex()
1438 .child(self.bar.clone())
1439 .child(self.readout.clone())
1440 }
1441 }
1442
1443 #[gpui::test]
1447 fn drawing_a_slider_again_leaves_its_readers_retained(cx: &mut TestAppContext) {
1448 let renders = Rc::new(Cell::new(0));
1449 let (view, cx) = cx.add_window_view({
1450 let renders = renders.clone();
1451 move |_, cx| {
1452 let state = cx.new(|_| SliderState::new());
1453 Pair {
1454 bar: cx.new(|_| Bar {
1455 state: state.clone(),
1456 }),
1457 readout: cx.new(|_| Readout { state, renders }),
1458 }
1459 }
1460 });
1461 cx.update(|window, cx| window.draw(cx).clear(cx));
1462 cx.update(|window, cx| window.draw(cx).clear(cx));
1463 let rendered = renders.get();
1464 for _ in 0..3 {
1465 view.update(cx, |pair, cx| pair.bar.update(cx, |_, cx| cx.notify()));
1466 cx.update(|window, cx| window.draw(cx).clear(cx));
1467 }
1468 cx.update(|window, cx| window.draw(cx).clear(cx));
1469 assert_eq!(renders.get(), rendered, "the readout was built again");
1470 }
1471 }
1472}