1use crate::TestSupportExt as _;
2use std::ops::Range;
3
4use gpui::{
5 AccessibleAction, Along, AnyElement, App, AppContext as _, Axis, Bounds, Context, Div,
6 DragMoveEvent, Empty, Entity, EntityId, EventEmitter, HitboxBehavior, InteractiveElement,
7 IntoElement, MouseButton, MouseDownEvent, Orientation, ParentElement, Pixels, Point, Render,
8 RenderOnce, Role, StatefulInteractiveElement, StyleRefinement, Styled, TouchDragEvent,
9 TouchPhase, Window, div, prelude::FluentBuilder as _, px,
10};
11
12use crate::{element_ext::ElementExt, geometry::AxisExt};
13
14pub enum SliderEvent {
16 Change(SliderValue),
18 Release(SliderValue),
20}
21
22#[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 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 #[inline]
80 pub fn is_single(&self) -> bool {
81 matches!(self, SliderValue::Single(_))
82 }
83
84 #[inline]
86 pub fn is_range(&self) -> bool {
87 matches!(self, SliderValue::Range(_, _))
88 }
89
90 pub fn start(&self) -> f32 {
92 match self {
93 SliderValue::Single(value) => *value,
94 SliderValue::Range(start, _) => *start,
95 }
96 }
97
98 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
125pub enum SliderScale {
126 #[default]
129 Linear,
130 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
169pub struct SliderState {
171 min: f32,
172 max: f32,
173 step: f32,
174 value: SliderValue,
175 percentage: Range<f32>,
177 bounds: Bounds<Pixels>,
179 scale: SliderScale,
180 dragging: bool,
183 touch_drag: Option<TouchDrag>,
185}
186
187#[derive(Clone, Copy)]
189struct TouchDrag {
190 start_position: Point<Pixels>,
192 is_start: bool,
194}
195
196impl SliderState {
197 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 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 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 pub fn step(mut self, step: f32) -> Self {
244 self.step = step;
245 self
246 }
247
248 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 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 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 pub fn value(&self) -> SliderValue {
286 self.value
287 }
288
289 pub fn min_value(&self) -> f32 {
291 self.min
292 }
293
294 pub fn max_value(&self) -> f32 {
296 self.max
297 }
298
299 pub fn step_value(&self) -> f32 {
301 self.step
302 }
303
304 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 let base = self.max / self.min;
314 (base.powf(percentage) * self.min).clamp(self.min, self.max)
315 }
316 }
317 }
318
319 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 #[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 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 #[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#[derive(IntoElement)]
442pub struct Slider {
443 state: Entity<SliderState>,
444 axis: Axis,
445 disabled: bool,
446 base: Div,
447 children: Vec<AnyElement>,
448}
449
450impl Slider {
451 pub fn new(state: &Entity<SliderState>) -> Self {
452 Self {
453 state: state.clone(),
454 axis: Axis::Horizontal,
455 disabled: false,
456 base: div(),
457 children: Vec::new(),
458 }
459 }
460
461 pub fn horizontal(mut self) -> Self {
462 self.axis = Axis::Horizontal;
463 self
464 }
465
466 pub fn vertical(mut self) -> Self {
467 self.axis = Axis::Vertical;
468 self
469 }
470
471 pub fn axis(mut self, axis: Axis) -> Self {
472 self.axis = axis;
473 self
474 }
475
476 pub fn disabled(mut self, disabled: bool) -> Self {
477 self.disabled = disabled;
478 self
479 }
480}
481
482impl ParentElement for Slider {
483 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
484 self.children.extend(elements);
485 }
486}
487
488impl Styled for Slider {
489 fn style(&mut self) -> &mut StyleRefinement {
490 self.base.style()
491 }
492}
493
494impl RenderOnce for Slider {
495 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
496 let axis = self.axis;
497 let entity_id = self.state.entity_id();
498 let state = self.state.read(cx);
499 let slider_state = self.state.clone();
500
501 self.base
502 .id(("slider", entity_id))
503 .test_support()
504 .role(Role::Slider)
505 .aria_numeric_value(state.value().end() as f64)
506 .aria_min_numeric_value(state.min_value() as f64)
507 .aria_max_numeric_value(state.max_value() as f64)
508 .aria_numeric_value_step(state.step_value() as f64)
509 .aria_orientation(if axis.is_vertical() {
510 Orientation::Vertical
511 } else {
512 Orientation::Horizontal
513 })
514 .on_a11y_action(AccessibleAction::Increment, {
515 let state = slider_state.clone();
516 move |_, window, cx| {
517 state.update(cx, |state, cx| {
518 let value =
519 (state.value().end() + state.step_value()).min(state.max_value());
520 state.set_value(value, window, cx);
521 });
522 }
523 })
524 .on_a11y_action(AccessibleAction::Decrement, {
525 let state = slider_state.clone();
526 move |_, window, cx| {
527 state.update(cx, |state, cx| {
528 let value =
529 (state.value().end() - state.step_value()).max(state.min_value());
530 state.set_value(value, window, cx);
531 });
532 }
533 })
534 .when(!self.disabled, |this| {
535 this.on_mouse_up(
536 MouseButton::Left,
537 window.listener_for(&self.state, |state, _, _, cx| state.handle_release(cx)),
538 )
539 .on_mouse_up_out(
540 MouseButton::Left,
541 window.listener_for(&self.state, |state, _, _, cx| state.handle_release(cx)),
542 )
543 })
544 .children(self.children)
545 }
546}
547
548#[derive(IntoElement)]
550pub struct SliderTrack {
551 state: Entity<SliderState>,
552 axis: Axis,
553 disabled: bool,
554 base: Div,
555 children: Vec<AnyElement>,
556}
557
558impl SliderTrack {
559 pub fn new(state: &Entity<SliderState>) -> Self {
560 Self {
561 state: state.clone(),
562 axis: Axis::Horizontal,
563 disabled: false,
564 base: div(),
565 children: Vec::new(),
566 }
567 }
568
569 pub fn axis(mut self, axis: Axis) -> Self {
570 self.axis = axis;
571 self
572 }
573 pub fn disabled(mut self, disabled: bool) -> Self {
574 self.disabled = disabled;
575 self
576 }
577}
578
579impl ParentElement for SliderTrack {
580 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
581 self.children.extend(elements);
582 }
583}
584
585impl Styled for SliderTrack {
586 fn style(&mut self) -> &mut StyleRefinement {
587 self.base.style()
588 }
589}
590
591impl InteractiveElement for SliderTrack {
592 fn interactivity(&mut self) -> &mut gpui::Interactivity {
593 self.base.interactivity()
594 }
595}
596
597impl RenderOnce for SliderTrack {
598 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
599 let axis = self.axis;
600 let entity_id = self.state.entity_id();
601 let state = self.state.read(cx);
602 let is_range = state.value().is_range();
603 let percentage = state.percentage();
604 let touch_layer = (!self.disabled).then(|| {
608 let slider_state = self.state.clone();
609 gpui::canvas(
610 |bounds, window, _| window.insert_hitbox(bounds, HitboxBehavior::Normal),
611 move |_, hitbox, window, _| {
612 let slider_state = slider_state.clone();
613 window.on_mouse_event(move |event: &TouchDragEvent, phase, window, cx| {
614 if !phase.bubble() {
615 return;
616 }
617 slider_state.update(cx, |state, cx| {
618 if event.phase == TouchPhase::Started {
619 if state.touch_drag.take().is_some() {
622 state.handle_release(cx);
623 }
624 if window.default_prevented() || !hitbox.is_hovered(window) {
627 return;
628 }
629 window.prevent_default();
630 let is_start =
631 is_range && state.is_nearer_start(axis, event.start_position);
632 state.touch_drag = Some(TouchDrag {
633 start_position: event.start_position,
634 is_start,
635 });
636 }
637 let Some(drag) = state.touch_drag else {
638 return;
639 };
640 if drag.start_position != event.start_position {
641 return;
642 }
643 cx.stop_propagation();
644 match event.phase {
645 TouchPhase::Started | TouchPhase::Moved => {
646 state.update_value_by_position(
647 axis,
648 event.position,
649 drag.is_start,
650 window,
651 cx,
652 );
653 }
654 TouchPhase::Ended | TouchPhase::Cancelled => {
655 state.touch_drag = None;
656 state.handle_release(cx);
657 }
658 }
659 });
660 });
661 },
662 )
663 .absolute()
664 .size_full()
665 });
666 self.base
667 .id("slider-bar-container")
668 .test_support()
669 .children(self.children)
670 .children(touch_layer)
671 .when(!self.disabled, |this| {
672 this.on_mouse_down(
673 MouseButton::Left,
674 window.listener_for(
675 &self.state,
676 move |state, event: &MouseDownEvent, window, cx| {
677 let is_start = if is_range {
678 let size = state.bounds().size.along(axis);
679 let position = if axis.is_horizontal() {
680 event.position.x - state.bounds().left()
681 } else {
682 state.bounds().bottom() - event.position.y
683 };
684 let center = ((percentage.end - percentage.start) / 2.
685 + percentage.start)
686 * size;
687 position < center
688 } else {
689 false
690 };
691 state.update_value_by_position(
692 axis,
693 event.position,
694 is_start,
695 window,
696 cx,
697 );
698 },
699 ),
700 )
701 .when(!is_range, |this| {
702 this.on_drag(DragSlider(entity_id), |drag, _, _, cx| {
703 cx.stop_propagation();
704 cx.new(|_| drag.clone())
705 })
706 .on_drag_move(window.listener_for(
707 &self.state,
708 move |state, event: &DragMoveEvent<DragSlider>, window, cx| {
709 let DragSlider(id) = event.drag(cx);
710 if *id == entity_id {
711 state.update_value_by_position(
712 axis,
713 event.event.position,
714 false,
715 window,
716 cx,
717 );
718 }
719 },
720 ))
721 })
722 })
723 }
724}
725
726#[derive(IntoElement)]
728pub struct SliderIndicator {
729 state: Entity<SliderState>,
730 base: Div,
731 children: Vec<AnyElement>,
732}
733
734impl SliderIndicator {
735 pub fn new(state: &Entity<SliderState>) -> Self {
736 Self {
737 state: state.clone(),
738 base: div(),
739 children: Vec::new(),
740 }
741 }
742}
743
744impl ParentElement for SliderIndicator {
745 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
746 self.children.extend(elements);
747 }
748}
749
750impl Styled for SliderIndicator {
751 fn style(&mut self) -> &mut StyleRefinement {
752 self.base.style()
753 }
754}
755
756impl InteractiveElement for SliderIndicator {
757 fn interactivity(&mut self) -> &mut gpui::Interactivity {
758 self.base.interactivity()
759 }
760}
761
762impl StatefulInteractiveElement for SliderIndicator {}
763
764impl RenderOnce for SliderIndicator {
765 fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
766 self.base
767 .id("slider-bar")
768 .children(self.children)
769 .on_prepaint({
770 let state = self.state;
771 move |bounds, _, cx| state.update(cx, |state, _| state.set_bounds(bounds))
772 })
773 }
774}
775
776#[derive(IntoElement)]
778pub struct SliderThumb {
779 state: Entity<SliderState>,
780 axis: Axis,
781 start: bool,
782 disabled: bool,
783 base: Div,
784 children: Vec<AnyElement>,
785}
786
787impl SliderThumb {
788 pub fn new(state: &Entity<SliderState>) -> Self {
789 Self {
790 state: state.clone(),
791 axis: Axis::Horizontal,
792 start: false,
793 disabled: false,
794 base: div(),
795 children: Vec::new(),
796 }
797 }
798
799 pub fn axis(mut self, axis: Axis) -> Self {
800 self.axis = axis;
801 self
802 }
803 pub fn start(mut self, start: bool) -> Self {
804 self.start = start;
805 self
806 }
807 pub fn disabled(mut self, disabled: bool) -> Self {
808 self.disabled = disabled;
809 self
810 }
811}
812
813impl ParentElement for SliderThumb {
814 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
815 self.children.extend(elements);
816 }
817}
818
819impl Styled for SliderThumb {
820 fn style(&mut self) -> &mut StyleRefinement {
821 self.base.style()
822 }
823}
824
825impl InteractiveElement for SliderThumb {
826 fn interactivity(&mut self) -> &mut gpui::Interactivity {
827 self.base.interactivity()
828 }
829}
830
831impl StatefulInteractiveElement for SliderThumb {}
832
833impl RenderOnce for SliderThumb {
834 fn render(self, window: &mut Window, _: &mut App) -> impl IntoElement {
835 let entity_id = self.state.entity_id();
836 let axis = self.axis;
837 let start = self.start;
838 self.base
839 .id(("slider-thumb", start as u32))
840 .test_support()
841 .children(self.children)
842 .when(!self.disabled, |this| {
843 this.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
844 .on_drag(DragThumb((entity_id, start)), |drag, _, _, cx| {
845 cx.stop_propagation();
846 cx.new(|_| drag.clone())
847 })
848 .on_drag_move(window.listener_for(
849 &self.state,
850 move |state, event: &DragMoveEvent<DragThumb>, window, cx| {
851 let DragThumb((id, start)) = event.drag(cx);
852 if *id == entity_id {
853 state.update_value_by_position(
854 axis,
855 event.event.position,
856 *start,
857 window,
858 cx,
859 );
860 }
861 },
862 ))
863 })
864 }
865}
866
867impl EventEmitter<SliderEvent> for SliderState {}
868
869impl SliderState {
870 #[doc(hidden)]
871 pub fn percentage(&self) -> Range<f32> {
872 self.percentage.clone()
873 }
874
875 #[doc(hidden)]
876 pub fn bounds(&self) -> Bounds<Pixels> {
877 self.bounds
878 }
879
880 #[doc(hidden)]
881 pub fn set_bounds(&mut self, bounds: Bounds<Pixels>) {
882 self.bounds = bounds;
883 }
884}
885
886#[cfg(test)]
887mod tests {
888 use super::*;
889
890 #[test]
891 fn legacy_value_conversions_and_clamping_are_preserved() {
892 assert_eq!(SliderValue::from(5.), SliderValue::Single(5.));
893 assert_eq!(SliderValue::from((2., 8.)), SliderValue::Range(2., 8.));
894 assert_eq!(SliderValue::from(2.0..8.0), SliderValue::Range(2., 8.));
895 assert_eq!(
896 SliderValue::Range(-1., 12.).clamp(0., 10.),
897 SliderValue::Range(0., 10.)
898 );
899 }
900
901 #[test]
902 fn legacy_linear_state_keeps_percentage_and_range_ordering() {
903 let state = SliderState::new()
904 .min(0.)
905 .max(200.)
906 .default_value((50., 150.));
907 assert_eq!(state.value(), SliderValue::Range(50., 150.));
908 assert_eq!(state.percentage(), 0.25..0.75);
909 }
910
911 #[test]
912 fn legacy_logarithmic_state_keeps_mapping() {
913 let state = SliderState::new()
914 .min(1.)
915 .max(1000.)
916 .scale(SliderScale::Logarithmic)
917 .default_value(10.);
918 let percentage = state.percentage().end;
919 assert!((percentage - (1. / 3.)).abs() < 0.0001);
920 }
921
922 #[test]
923 #[should_panic(expected = "`min` must be greater than 0")]
924 fn legacy_logarithmic_validation_is_preserved() {
925 let _ = SliderState::new().scale(SliderScale::Logarithmic);
926 }
927}