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
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}
184
185impl SliderState {
186 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 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 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 pub fn step(mut self, step: f32) -> Self {
232 self.step = step;
233 self
234 }
235
236 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 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 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 pub fn value(&self) -> SliderValue {
274 self.value
275 }
276
277 pub fn min_value(&self) -> f32 {
279 self.min
280 }
281
282 pub fn max_value(&self) -> f32 {
284 self.max
285 }
286
287 pub fn step_value(&self) -> f32 {
289 self.step
290 }
291
292 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 let base = self.max / self.min;
302 (base.powf(percentage) * self.min).clamp(self.min, self.max)
303 }
304 }
305 }
306
307 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 #[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 #[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#[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#[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#[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#[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}