1use std::ops::Range;
2
3use gpui::{
4 AccessibleAction, Along, AnyElement, App, AppContext as _, Axis, Bounds, Context, Div,
5 DragMoveEvent, Empty, Entity, EntityId, EventEmitter, InteractiveElement, IntoElement,
6 MouseButton, MouseDownEvent, Orientation, ParentElement, Pixels, Point, Render, RenderOnce,
7 Role, StatefulInteractiveElement, StyleRefinement, Styled, Window, div,
8 prelude::FluentBuilder as _, px,
9};
10
11use crate::{element_ext::ElementExt, geometry::AxisExt};
12
13pub enum SliderEvent {
15 Change(SliderValue),
17 Release(SliderValue),
19}
20
21#[derive(Clone, Copy, Debug, PartialEq)]
28pub enum SliderValue {
29 Single(f32),
30 Range(f32, f32),
31}
32
33impl std::fmt::Display for SliderValue {
34 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35 match self {
36 SliderValue::Single(value) => write!(f, "{}", value),
37 SliderValue::Range(start, end) => write!(f, "{}..{}", start, end),
38 }
39 }
40}
41
42impl From<f32> for SliderValue {
43 fn from(value: f32) -> Self {
44 SliderValue::Single(value)
45 }
46}
47
48impl From<(f32, f32)> for SliderValue {
49 fn from(value: (f32, f32)) -> Self {
50 SliderValue::Range(value.0, value.1)
51 }
52}
53
54impl From<Range<f32>> for SliderValue {
55 fn from(value: Range<f32>) -> Self {
56 SliderValue::Range(value.start, value.end)
57 }
58}
59
60impl Default for SliderValue {
61 fn default() -> Self {
62 SliderValue::Single(0.)
63 }
64}
65
66impl SliderValue {
67 pub fn clamp(self, min: f32, max: f32) -> Self {
69 match self {
70 SliderValue::Single(value) => SliderValue::Single(value.clamp(min, max)),
71 SliderValue::Range(start, end) => {
72 SliderValue::Range(start.clamp(min, max), end.clamp(min, max))
73 }
74 }
75 }
76
77 #[inline]
79 pub fn is_single(&self) -> bool {
80 matches!(self, SliderValue::Single(_))
81 }
82
83 #[inline]
85 pub fn is_range(&self) -> bool {
86 matches!(self, SliderValue::Range(_, _))
87 }
88
89 pub fn start(&self) -> f32 {
91 match self {
92 SliderValue::Single(value) => *value,
93 SliderValue::Range(start, _) => *start,
94 }
95 }
96
97 pub fn end(&self) -> f32 {
99 match self {
100 SliderValue::Single(value) => *value,
101 SliderValue::Range(_, end) => *end,
102 }
103 }
104
105 fn set_start(&mut self, value: f32) {
106 if let SliderValue::Range(_, end) = self {
107 *self = SliderValue::Range(value.min(*end), *end);
108 } else {
109 *self = SliderValue::Single(value);
110 }
111 }
112
113 fn set_end(&mut self, value: f32) {
114 if let SliderValue::Range(start, _) = self {
115 *self = SliderValue::Range(*start, value.max(*start));
116 } else {
117 *self = SliderValue::Single(value);
118 }
119 }
120}
121
122#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
124pub enum SliderScale {
125 #[default]
128 Linear,
129 Logarithmic,
154}
155
156impl SliderScale {
157 #[inline]
158 pub fn is_linear(&self) -> bool {
159 matches!(self, SliderScale::Linear)
160 }
161
162 #[inline]
163 pub fn is_logarithmic(&self) -> bool {
164 matches!(self, SliderScale::Logarithmic)
165 }
166}
167
168pub struct SliderState {
170 min: f32,
171 max: f32,
172 step: f32,
173 value: SliderValue,
174 percentage: Range<f32>,
176 bounds: Bounds<Pixels>,
178 scale: SliderScale,
179 dragging: bool,
182}
183
184impl SliderState {
185 pub fn new() -> Self {
187 Self {
188 min: 0.0,
189 max: 100.0,
190 step: 1.0,
191 value: SliderValue::default(),
192 percentage: (0.0..0.0),
193 bounds: Bounds::default(),
194 scale: SliderScale::default(),
195 dragging: false,
196 }
197 }
198
199 pub fn min(mut self, min: f32) -> Self {
201 if self.scale.is_logarithmic() {
202 assert!(
203 min > 0.0,
204 "`min` must be greater than 0 for SliderScale::Logarithmic"
205 );
206 assert!(
207 min < self.max,
208 "`min` must be less than `max` for Logarithmic scale"
209 );
210 }
211 self.min = min;
212 self.update_thumb_pos();
213 self
214 }
215
216 pub fn max(mut self, max: f32) -> Self {
218 if self.scale.is_logarithmic() {
219 assert!(
220 max > self.min,
221 "`max` must be greater than `min` for Logarithmic scale"
222 );
223 }
224 self.max = max;
225 self.update_thumb_pos();
226 self
227 }
228
229 pub fn step(mut self, step: f32) -> Self {
231 self.step = step;
232 self
233 }
234
235 pub fn scale(mut self, scale: SliderScale) -> Self {
237 if scale.is_logarithmic() {
238 assert!(
239 self.min > 0.0,
240 "`min` must be greater than 0 for Logarithmic scale"
241 );
242 assert!(
243 self.max > self.min,
244 "`max` must be greater than `min` for Logarithmic scale"
245 );
246 }
247 self.scale = scale;
248 self.update_thumb_pos();
249 self
250 }
251
252 pub fn default_value(mut self, value: impl Into<SliderValue>) -> Self {
254 self.value = value.into();
255 self.update_thumb_pos();
256 self
257 }
258
259 pub fn set_value(
261 &mut self,
262 value: impl Into<SliderValue>,
263 _: &mut Window,
264 cx: &mut Context<Self>,
265 ) {
266 self.value = value.into();
267 self.update_thumb_pos();
268 cx.notify();
269 }
270
271 pub fn value(&self) -> SliderValue {
273 self.value
274 }
275
276 pub fn min_value(&self) -> f32 {
278 self.min
279 }
280
281 pub fn max_value(&self) -> f32 {
283 self.max
284 }
285
286 pub fn step_value(&self) -> f32 {
288 self.step
289 }
290
291 fn percentage_to_value(&self, percentage: f32) -> f32 {
294 match self.scale {
295 SliderScale::Linear => self.min + (self.max - self.min) * percentage,
296 SliderScale::Logarithmic => {
297 let base = self.max / self.min;
301 (base.powf(percentage) * self.min).clamp(self.min, self.max)
302 }
303 }
304 }
305
306 fn value_to_percentage(&self, value: f32) -> f32 {
309 match self.scale {
310 SliderScale::Linear => {
311 let range = self.max - self.min;
312 if range <= 0.0 {
313 0.0
314 } else {
315 (value - self.min) / range
316 }
317 }
318 SliderScale::Logarithmic => {
319 let base = self.max / self.min;
320 (value / self.min).log(base).clamp(0.0, 1.0)
321 }
322 }
323 }
324
325 fn update_thumb_pos(&mut self) {
326 match self.value {
327 SliderValue::Single(value) => {
328 let percentage = self.value_to_percentage(value.clamp(self.min, self.max));
329 self.percentage = 0.0..percentage;
330 }
331 SliderValue::Range(start, end) => {
332 let clamped_start = start.clamp(self.min, self.max);
333 let clamped_end = end.clamp(self.min, self.max);
334 self.percentage =
335 self.value_to_percentage(clamped_start)..self.value_to_percentage(clamped_end);
336 }
337 }
338 }
339
340 #[doc(hidden)]
342 pub fn update_value_by_position(
343 &mut self,
344 axis: Axis,
345 position: Point<Pixels>,
346 is_start: bool,
347 _: &mut Window,
348 cx: &mut Context<Self>,
349 ) {
350 self.dragging = true;
351 let bounds = self.bounds;
352 let step = self.step;
353
354 let inner_pos = if axis.is_horizontal() {
355 position.x - bounds.left()
356 } else {
357 bounds.bottom() - position.y
358 };
359 let total_size = bounds.size.along(axis);
360 let percentage = inner_pos.clamp(px(0.), total_size) / total_size;
361
362 let percentage = if is_start {
363 percentage.clamp(0.0, self.percentage.end)
364 } else {
365 percentage.clamp(self.percentage.start, 1.0)
366 };
367
368 let value = self.percentage_to_value(percentage);
369 let value = (value / step).round() * step;
370
371 if is_start {
372 self.percentage.start = percentage;
373 self.value.set_start(value);
374 } else {
375 self.percentage.end = percentage;
376 self.value.set_end(value);
377 }
378 cx.emit(SliderEvent::Change(self.value));
379 cx.notify();
380 }
381
382 #[doc(hidden)]
385 pub fn handle_release(&mut self, cx: &mut Context<Self>) {
386 if !self.dragging {
387 return;
388 }
389 self.dragging = false;
390 cx.emit(SliderEvent::Release(self.value));
391 }
392}
393
394#[derive(Clone)]
395struct DragThumb((EntityId, bool));
396
397impl Render for DragThumb {
398 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
399 Empty
400 }
401}
402
403#[derive(Clone)]
404struct DragSlider(EntityId);
405
406impl Render for DragSlider {
407 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
408 Empty
409 }
410}
411
412#[derive(IntoElement)]
416pub struct Slider {
417 state: Entity<SliderState>,
418 axis: Axis,
419 disabled: bool,
420 base: Div,
421 children: Vec<AnyElement>,
422}
423
424impl Slider {
425 pub fn new(state: &Entity<SliderState>) -> Self {
426 Self {
427 state: state.clone(),
428 axis: Axis::Horizontal,
429 disabled: false,
430 base: div(),
431 children: Vec::new(),
432 }
433 }
434
435 pub fn horizontal(mut self) -> Self {
436 self.axis = Axis::Horizontal;
437 self
438 }
439
440 pub fn vertical(mut self) -> Self {
441 self.axis = Axis::Vertical;
442 self
443 }
444
445 pub fn axis(mut self, axis: Axis) -> Self {
446 self.axis = axis;
447 self
448 }
449
450 pub fn disabled(mut self, disabled: bool) -> Self {
451 self.disabled = disabled;
452 self
453 }
454}
455
456impl ParentElement for Slider {
457 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
458 self.children.extend(elements);
459 }
460}
461
462impl Styled for Slider {
463 fn style(&mut self) -> &mut StyleRefinement {
464 self.base.style()
465 }
466}
467
468impl RenderOnce for Slider {
469 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
470 let axis = self.axis;
471 let entity_id = self.state.entity_id();
472 let state = self.state.read(cx);
473 let slider_state = self.state.clone();
474
475 self.base
476 .id(("slider", entity_id))
477 .role(Role::Slider)
478 .aria_numeric_value(state.value().end() as f64)
479 .aria_min_numeric_value(state.min_value() as f64)
480 .aria_max_numeric_value(state.max_value() as f64)
481 .aria_numeric_value_step(state.step_value() as f64)
482 .aria_orientation(if axis.is_vertical() {
483 Orientation::Vertical
484 } else {
485 Orientation::Horizontal
486 })
487 .on_a11y_action(AccessibleAction::Increment, {
488 let state = slider_state.clone();
489 move |_, window, cx| {
490 state.update(cx, |state, cx| {
491 let value =
492 (state.value().end() + state.step_value()).min(state.max_value());
493 state.set_value(value, window, cx);
494 });
495 }
496 })
497 .on_a11y_action(AccessibleAction::Decrement, {
498 let state = slider_state.clone();
499 move |_, window, cx| {
500 state.update(cx, |state, cx| {
501 let value =
502 (state.value().end() - state.step_value()).max(state.min_value());
503 state.set_value(value, window, cx);
504 });
505 }
506 })
507 .when(!self.disabled, |this| {
508 this.on_mouse_up(
509 MouseButton::Left,
510 window.listener_for(&self.state, |state, _, _, cx| state.handle_release(cx)),
511 )
512 .on_mouse_up_out(
513 MouseButton::Left,
514 window.listener_for(&self.state, |state, _, _, cx| state.handle_release(cx)),
515 )
516 })
517 .children(self.children)
518 }
519}
520
521#[derive(IntoElement)]
523pub struct SliderTrack {
524 state: Entity<SliderState>,
525 axis: Axis,
526 disabled: bool,
527 base: Div,
528 children: Vec<AnyElement>,
529}
530
531impl SliderTrack {
532 pub fn new(state: &Entity<SliderState>) -> Self {
533 Self {
534 state: state.clone(),
535 axis: Axis::Horizontal,
536 disabled: false,
537 base: div(),
538 children: Vec::new(),
539 }
540 }
541
542 pub fn axis(mut self, axis: Axis) -> Self {
543 self.axis = axis;
544 self
545 }
546 pub fn disabled(mut self, disabled: bool) -> Self {
547 self.disabled = disabled;
548 self
549 }
550}
551
552impl ParentElement for SliderTrack {
553 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
554 self.children.extend(elements);
555 }
556}
557
558impl Styled for SliderTrack {
559 fn style(&mut self) -> &mut StyleRefinement {
560 self.base.style()
561 }
562}
563
564impl InteractiveElement for SliderTrack {
565 fn interactivity(&mut self) -> &mut gpui::Interactivity {
566 self.base.interactivity()
567 }
568}
569
570impl RenderOnce for SliderTrack {
571 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
572 let axis = self.axis;
573 let entity_id = self.state.entity_id();
574 let state = self.state.read(cx);
575 let is_range = state.value().is_range();
576 let percentage = state.percentage();
577 self.base
578 .id("slider-bar-container")
579 .children(self.children)
580 .when(!self.disabled, |this| {
581 this.on_mouse_down(
582 MouseButton::Left,
583 window.listener_for(
584 &self.state,
585 move |state, event: &MouseDownEvent, window, cx| {
586 let is_start = if is_range {
587 let size = state.bounds().size.along(axis);
588 let position = if axis.is_horizontal() {
589 event.position.x - state.bounds().left()
590 } else {
591 state.bounds().bottom() - event.position.y
592 };
593 let center = ((percentage.end - percentage.start) / 2.
594 + percentage.start)
595 * size;
596 position < center
597 } else {
598 false
599 };
600 state.update_value_by_position(
601 axis,
602 event.position,
603 is_start,
604 window,
605 cx,
606 );
607 },
608 ),
609 )
610 .when(!is_range, |this| {
611 this.on_drag(DragSlider(entity_id), |drag, _, _, cx| {
612 cx.stop_propagation();
613 cx.new(|_| drag.clone())
614 })
615 .on_drag_move(window.listener_for(
616 &self.state,
617 move |state, event: &DragMoveEvent<DragSlider>, window, cx| {
618 let DragSlider(id) = event.drag(cx);
619 if *id == entity_id {
620 state.update_value_by_position(
621 axis,
622 event.event.position,
623 false,
624 window,
625 cx,
626 );
627 }
628 },
629 ))
630 })
631 })
632 }
633}
634
635#[derive(IntoElement)]
637pub struct SliderIndicator {
638 state: Entity<SliderState>,
639 base: Div,
640 children: Vec<AnyElement>,
641}
642
643impl SliderIndicator {
644 pub fn new(state: &Entity<SliderState>) -> Self {
645 Self {
646 state: state.clone(),
647 base: div(),
648 children: Vec::new(),
649 }
650 }
651}
652
653impl ParentElement for SliderIndicator {
654 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
655 self.children.extend(elements);
656 }
657}
658
659impl Styled for SliderIndicator {
660 fn style(&mut self) -> &mut StyleRefinement {
661 self.base.style()
662 }
663}
664
665impl InteractiveElement for SliderIndicator {
666 fn interactivity(&mut self) -> &mut gpui::Interactivity {
667 self.base.interactivity()
668 }
669}
670
671impl StatefulInteractiveElement for SliderIndicator {}
672
673impl RenderOnce for SliderIndicator {
674 fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
675 self.base
676 .id("slider-bar")
677 .children(self.children)
678 .on_prepaint({
679 let state = self.state;
680 move |bounds, _, cx| state.update(cx, |state, _| state.set_bounds(bounds))
681 })
682 }
683}
684
685#[derive(IntoElement)]
687pub struct SliderThumb {
688 state: Entity<SliderState>,
689 axis: Axis,
690 start: bool,
691 disabled: bool,
692 base: Div,
693 children: Vec<AnyElement>,
694}
695
696impl SliderThumb {
697 pub fn new(state: &Entity<SliderState>) -> Self {
698 Self {
699 state: state.clone(),
700 axis: Axis::Horizontal,
701 start: false,
702 disabled: false,
703 base: div(),
704 children: Vec::new(),
705 }
706 }
707
708 pub fn axis(mut self, axis: Axis) -> Self {
709 self.axis = axis;
710 self
711 }
712 pub fn start(mut self, start: bool) -> Self {
713 self.start = start;
714 self
715 }
716 pub fn disabled(mut self, disabled: bool) -> Self {
717 self.disabled = disabled;
718 self
719 }
720}
721
722impl ParentElement for SliderThumb {
723 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
724 self.children.extend(elements);
725 }
726}
727
728impl Styled for SliderThumb {
729 fn style(&mut self) -> &mut StyleRefinement {
730 self.base.style()
731 }
732}
733
734impl InteractiveElement for SliderThumb {
735 fn interactivity(&mut self) -> &mut gpui::Interactivity {
736 self.base.interactivity()
737 }
738}
739
740impl StatefulInteractiveElement for SliderThumb {}
741
742impl RenderOnce for SliderThumb {
743 fn render(self, window: &mut Window, _: &mut App) -> impl IntoElement {
744 let entity_id = self.state.entity_id();
745 let axis = self.axis;
746 let start = self.start;
747 self.base
748 .id(("slider-thumb", start as u32))
749 .children(self.children)
750 .when(!self.disabled, |this| {
751 this.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
752 .on_drag(DragThumb((entity_id, start)), |drag, _, _, cx| {
753 cx.stop_propagation();
754 cx.new(|_| drag.clone())
755 })
756 .on_drag_move(window.listener_for(
757 &self.state,
758 move |state, event: &DragMoveEvent<DragThumb>, window, cx| {
759 let DragThumb((id, start)) = event.drag(cx);
760 if *id == entity_id {
761 state.update_value_by_position(
762 axis,
763 event.event.position,
764 *start,
765 window,
766 cx,
767 );
768 }
769 },
770 ))
771 })
772 }
773}
774
775impl EventEmitter<SliderEvent> for SliderState {}
776
777impl SliderState {
778 #[doc(hidden)]
779 pub fn percentage(&self) -> Range<f32> {
780 self.percentage.clone()
781 }
782
783 #[doc(hidden)]
784 pub fn bounds(&self) -> Bounds<Pixels> {
785 self.bounds
786 }
787
788 #[doc(hidden)]
789 pub fn set_bounds(&mut self, bounds: Bounds<Pixels>) {
790 self.bounds = bounds;
791 }
792}
793
794#[cfg(test)]
795mod tests {
796 use super::*;
797
798 #[test]
799 fn legacy_value_conversions_and_clamping_are_preserved() {
800 assert_eq!(SliderValue::from(5.), SliderValue::Single(5.));
801 assert_eq!(SliderValue::from((2., 8.)), SliderValue::Range(2., 8.));
802 assert_eq!(SliderValue::from(2.0..8.0), SliderValue::Range(2., 8.));
803 assert_eq!(
804 SliderValue::Range(-1., 12.).clamp(0., 10.),
805 SliderValue::Range(0., 10.)
806 );
807 }
808
809 #[test]
810 fn legacy_linear_state_keeps_percentage_and_range_ordering() {
811 let state = SliderState::new()
812 .min(0.)
813 .max(200.)
814 .default_value((50., 150.));
815 assert_eq!(state.value(), SliderValue::Range(50., 150.));
816 assert_eq!(state.percentage(), 0.25..0.75);
817 }
818
819 #[test]
820 fn legacy_logarithmic_state_keeps_mapping() {
821 let state = SliderState::new()
822 .min(1.)
823 .max(1000.)
824 .scale(SliderScale::Logarithmic)
825 .default_value(10.);
826 let percentage = state.percentage().end;
827 assert!((percentage - (1. / 3.)).abs() < 0.0001);
828 }
829
830 #[test]
831 #[should_panic(expected = "`min` must be greater than 0")]
832 fn legacy_logarithmic_validation_is_preserved() {
833 let _ = SliderState::new().scale(SliderScale::Logarithmic);
834 }
835}