1use std::ops::Range;
2
3use crate::{h_flex, tooltip::Tooltip, ActiveTheme, AxisExt, StyledExt};
4use gpui::{
5 canvas, div, prelude::FluentBuilder as _, px, Along, App, AppContext as _, Axis, Background,
6 Bounds, Context, Corners, DragMoveEvent, Empty, Entity, EntityId, EventEmitter, Hsla,
7 InteractiveElement, IntoElement, MouseButton, MouseDownEvent, ParentElement as _, Pixels,
8 Point, Render, RenderOnce, StatefulInteractiveElement as _, StyleRefinement, Styled, Window,
9};
10
11#[derive(Clone)]
12struct DragThumb((EntityId, bool));
13
14impl Render for DragThumb {
15 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
16 Empty
17 }
18}
19
20#[derive(Clone)]
21struct DragSlider(EntityId);
22
23impl Render for DragSlider {
24 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
25 Empty
26 }
27}
28
29pub enum SliderEvent {
31 Change(SliderValue),
32}
33
34#[derive(Clone, Copy, Debug, PartialEq)]
41pub enum SliderValue {
42 Single(f32),
43 Range(f32, f32),
44}
45
46impl std::fmt::Display for SliderValue {
47 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48 match self {
49 SliderValue::Single(value) => write!(f, "{}", value),
50 SliderValue::Range(start, end) => write!(f, "{}..{}", start, end),
51 }
52 }
53}
54
55impl From<f32> for SliderValue {
56 fn from(value: f32) -> Self {
57 SliderValue::Single(value)
58 }
59}
60
61impl From<(f32, f32)> for SliderValue {
62 fn from(value: (f32, f32)) -> Self {
63 SliderValue::Range(value.0, value.1)
64 }
65}
66
67impl From<Range<f32>> for SliderValue {
68 fn from(value: Range<f32>) -> Self {
69 SliderValue::Range(value.start, value.end)
70 }
71}
72
73impl Default for SliderValue {
74 fn default() -> Self {
75 SliderValue::Single(0.)
76 }
77}
78
79impl SliderValue {
80 pub fn clamp(self, min: f32, max: f32) -> Self {
82 match self {
83 SliderValue::Single(value) => SliderValue::Single(value.clamp(min, max)),
84 SliderValue::Range(start, end) => {
85 SliderValue::Range(start.clamp(min, max), end.clamp(min, max))
86 }
87 }
88 }
89
90 #[inline]
92 pub fn is_single(&self) -> bool {
93 matches!(self, SliderValue::Single(_))
94 }
95
96 #[inline]
98 pub fn is_range(&self) -> bool {
99 matches!(self, SliderValue::Range(_, _))
100 }
101
102 pub fn start(&self) -> f32 {
104 match self {
105 SliderValue::Single(value) => *value,
106 SliderValue::Range(start, _) => *start,
107 }
108 }
109
110 pub fn end(&self) -> f32 {
112 match self {
113 SliderValue::Single(value) => *value,
114 SliderValue::Range(_, end) => *end,
115 }
116 }
117
118 fn set_start(&mut self, value: f32) {
119 if let SliderValue::Range(_, end) = self {
120 *self = SliderValue::Range(value.min(*end), *end);
121 } else {
122 *self = SliderValue::Single(value);
123 }
124 }
125
126 fn set_end(&mut self, value: f32) {
127 if let SliderValue::Range(start, _) = self {
128 *self = SliderValue::Range(*start, value.max(*start));
129 } else {
130 *self = SliderValue::Single(value);
131 }
132 }
133}
134
135#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
137pub enum SliderScale {
138 #[default]
141 Linear,
142 Logarithmic,
167}
168
169impl SliderScale {
170 #[inline]
171 pub fn is_linear(&self) -> bool {
172 matches!(self, SliderScale::Linear)
173 }
174
175 #[inline]
176 pub fn is_logarithmic(&self) -> bool {
177 matches!(self, SliderScale::Logarithmic)
178 }
179}
180
181pub struct SliderState {
183 min: f32,
184 max: f32,
185 step: f32,
186 value: SliderValue,
187 percentage: Range<f32>,
189 bounds: Bounds<Pixels>,
191 scale: SliderScale,
192}
193
194impl SliderState {
195 pub fn new() -> Self {
197 Self {
198 min: 0.0,
199 max: 100.0,
200 step: 1.0,
201 value: SliderValue::default(),
202 percentage: (0.0..0.0),
203 bounds: Bounds::default(),
204 scale: SliderScale::default(),
205 }
206 }
207
208 pub fn min(mut self, min: f32) -> Self {
210 if self.scale.is_logarithmic() {
211 assert!(
212 min > 0.0,
213 "`min` must be greater than 0 for SliderScale::Logarithmic"
214 );
215 assert!(
216 min < self.max,
217 "`min` must be less than `max` for Logarithmic scale"
218 );
219 }
220 self.min = min;
221 self.update_thumb_pos();
222 self
223 }
224
225 pub fn max(mut self, max: f32) -> Self {
227 if self.scale.is_logarithmic() {
228 assert!(
229 max > self.min,
230 "`max` must be greater than `min` for Logarithmic scale"
231 );
232 }
233 self.max = max;
234 self.update_thumb_pos();
235 self
236 }
237
238 pub fn step(mut self, step: f32) -> Self {
240 self.step = step;
241 self
242 }
243
244 pub fn scale(mut self, scale: SliderScale) -> Self {
246 if scale.is_logarithmic() {
247 assert!(
248 self.min > 0.0,
249 "`min` must be greater than 0 for Logarithmic scale"
250 );
251 assert!(
252 self.max > self.min,
253 "`max` must be greater than `min` for Logarithmic scale"
254 );
255 }
256 self.scale = scale;
257 self.update_thumb_pos();
258 self
259 }
260
261 pub fn default_value(mut self, value: impl Into<SliderValue>) -> Self {
263 self.value = value.into();
264 self.update_thumb_pos();
265 self
266 }
267
268 pub fn set_value(
270 &mut self,
271 value: impl Into<SliderValue>,
272 _: &mut Window,
273 cx: &mut Context<Self>,
274 ) {
275 self.value = value.into();
276 self.update_thumb_pos();
277 cx.notify();
278 }
279
280 pub fn value(&self) -> SliderValue {
282 self.value
283 }
284
285 fn percentage_to_value(&self, percentage: f32) -> f32 {
288 match self.scale {
289 SliderScale::Linear => self.min + (self.max - self.min) * percentage,
290 SliderScale::Logarithmic => {
291 let base = self.max / self.min;
295 (base.powf(percentage) * self.min).clamp(self.min, self.max)
296 }
297 }
298 }
299
300 fn value_to_percentage(&self, value: f32) -> f32 {
303 match self.scale {
304 SliderScale::Linear => {
305 let range = self.max - self.min;
306 if range <= 0.0 {
307 0.0
308 } else {
309 (value - self.min) / range
310 }
311 }
312 SliderScale::Logarithmic => {
313 let base = self.max / self.min;
314 (value / self.min).log(base).clamp(0.0, 1.0)
315 }
316 }
317 }
318
319 fn update_thumb_pos(&mut self) {
320 match self.value {
321 SliderValue::Single(value) => {
322 let percentage = self.value_to_percentage(value.clamp(self.min, self.max));
323 self.percentage = 0.0..percentage;
324 }
325 SliderValue::Range(start, end) => {
326 let clamped_start = start.clamp(self.min, self.max);
327 let clamped_end = end.clamp(self.min, self.max);
328 self.percentage =
329 self.value_to_percentage(clamped_start)..self.value_to_percentage(clamped_end);
330 }
331 }
332 }
333
334 fn update_value_by_position(
336 &mut self,
337 axis: Axis,
338 position: Point<Pixels>,
339 is_start: bool,
340 _: &mut Window,
341 cx: &mut Context<Self>,
342 ) {
343 let bounds = self.bounds;
344 let step = self.step;
345
346 let inner_pos = if axis.is_horizontal() {
347 position.x - bounds.left()
348 } else {
349 bounds.bottom() - position.y
350 };
351 let total_size = bounds.size.along(axis);
352 let percentage = inner_pos.clamp(px(0.), total_size) / total_size;
353
354 let percentage = if is_start {
355 percentage.clamp(0.0, self.percentage.end)
356 } else {
357 percentage.clamp(self.percentage.start, 1.0)
358 };
359
360 let value = self.percentage_to_value(percentage);
361 let value = (value / step).round() * step;
362
363 if is_start {
364 self.percentage.start = percentage;
365 self.value.set_start(value);
366 } else {
367 self.percentage.end = percentage;
368 self.value.set_end(value);
369 }
370 cx.emit(SliderEvent::Change(self.value));
371 cx.notify();
372 }
373}
374
375impl EventEmitter<SliderEvent> for SliderState {}
376impl Render for SliderState {
377 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
378 Empty
379 }
380}
381
382#[derive(IntoElement)]
384pub struct Slider {
385 state: Entity<SliderState>,
386 axis: Axis,
387 style: StyleRefinement,
388 disabled: bool,
389}
390
391impl Slider {
392 pub fn new(state: &Entity<SliderState>) -> Self {
394 Self {
395 axis: Axis::Horizontal,
396 state: state.clone(),
397 style: StyleRefinement::default(),
398 disabled: false,
399 }
400 }
401
402 pub fn horizontal(mut self) -> Self {
404 self.axis = Axis::Horizontal;
405 self
406 }
407
408 pub fn vertical(mut self) -> Self {
410 self.axis = Axis::Vertical;
411 self
412 }
413
414 pub fn disabled(mut self, disabled: bool) -> Self {
416 self.disabled = disabled;
417 self
418 }
419
420 #[allow(clippy::too_many_arguments)]
421 fn render_thumb(
422 &self,
423 start_pos: Pixels,
424 is_start: bool,
425 bar_color: Background,
426 thumb_color: Hsla,
427 radius: Corners<Pixels>,
428 window: &mut Window,
429 cx: &mut App,
430 ) -> impl gpui::IntoElement {
431 let state = self.state.read(cx);
432 let entity_id = self.state.entity_id();
433 let value = state.value;
434 let axis = self.axis;
435 let id = ("slider-thumb", is_start as u32);
436
437 if self.disabled {
438 return div().id(id);
439 }
440
441 div()
442 .id(id)
443 .absolute()
444 .when(axis.is_horizontal(), |this| {
445 this.top(px(-5.)).left(start_pos).ml(-px(8.))
446 })
447 .when(axis.is_vertical(), |this| {
448 this.bottom(start_pos).left(px(-5.)).mb(-px(8.))
449 })
450 .flex()
451 .items_center()
452 .justify_center()
453 .flex_shrink_0()
454 .corner_radii(radius)
455 .bg(bar_color.opacity(0.5))
456 .when(cx.theme().shadow, |this| this.shadow_md())
457 .size_4()
458 .p(px(1.))
459 .child(
460 div()
461 .flex_shrink_0()
462 .size_full()
463 .corner_radii(radius)
464 .bg(thumb_color),
465 )
466 .on_mouse_down(MouseButton::Left, |_, _, cx| {
467 cx.stop_propagation();
468 })
469 .on_drag(DragThumb((entity_id, is_start)), |drag, _, _, cx| {
470 cx.stop_propagation();
471 cx.new(|_| drag.clone())
472 })
473 .on_drag_move(window.listener_for(
474 &self.state,
475 move |view, e: &DragMoveEvent<DragThumb>, window, cx| {
476 match e.drag(cx) {
477 DragThumb((id, is_start)) => {
478 if *id != entity_id {
479 return;
480 }
481
482 view.update_value_by_position(
484 axis,
485 e.event.position,
486 *is_start,
487 window,
488 cx,
489 )
490 }
491 }
492 },
493 ))
494 .tooltip(move |window, cx| {
495 Tooltip::new(format!(
496 "{}",
497 if is_start { value.start() } else { value.end() }
498 ))
499 .build(window, cx)
500 })
501 }
502}
503
504impl Styled for Slider {
505 fn style(&mut self) -> &mut StyleRefinement {
506 &mut self.style
507 }
508}
509
510impl RenderOnce for Slider {
511 fn render(self, window: &mut Window, cx: &mut gpui::App) -> impl IntoElement {
512 let axis = self.axis;
513 let entity_id = self.state.entity_id();
514 let state = self.state.read(cx);
515 let is_range = state.value().is_range();
516 let bar_size = state.bounds.size.along(axis);
517 let bar_start = state.percentage.start * bar_size;
518 let bar_end = state.percentage.end * bar_size;
519 let rem_size = window.rem_size();
520
521 let bar_color = self
522 .style
523 .background
524 .clone()
525 .and_then(|bg| bg.color())
526 .unwrap_or(cx.theme().slider_bar.into());
527 let thumb_color = self
528 .style
529 .text
530 .clone()
531 .and_then(|text| text.color)
532 .unwrap_or_else(|| cx.theme().slider_thumb);
533 let corner_radii = self.style.corner_radii.clone();
534 let default_radius = px(999.);
535 let radius = Corners {
536 top_left: corner_radii
537 .top_left
538 .map(|v| v.to_pixels(rem_size))
539 .unwrap_or(default_radius),
540 top_right: corner_radii
541 .top_right
542 .map(|v| v.to_pixels(rem_size))
543 .unwrap_or(default_radius),
544 bottom_left: corner_radii
545 .bottom_left
546 .map(|v| v.to_pixels(rem_size))
547 .unwrap_or(default_radius),
548 bottom_right: corner_radii
549 .bottom_right
550 .map(|v| v.to_pixels(rem_size))
551 .unwrap_or(default_radius),
552 };
553
554 div()
555 .id(("slider", self.state.entity_id()))
556 .flex()
557 .flex_1()
558 .items_center()
559 .justify_center()
560 .when(axis.is_vertical(), |this| this.h(px(120.)))
561 .when(axis.is_horizontal(), |this| this.w_full())
562 .refine_style(&self.style)
563 .bg(cx.theme().transparent)
564 .text_color(cx.theme().foreground)
565 .child(
566 h_flex()
567 .id("slider-bar-container")
568 .when(!self.disabled, |this| {
569 this.on_mouse_down(
570 MouseButton::Left,
571 window.listener_for(
572 &self.state,
573 move |state, e: &MouseDownEvent, window, cx| {
574 let mut is_start = false;
575 if is_range {
576 let inner_pos = if axis.is_horizontal() {
577 e.position.x - state.bounds.left()
578 } else {
579 state.bounds.bottom() - e.position.y
580 };
581 let center = (bar_end - bar_start) / 2.0 + bar_start;
582 is_start = inner_pos < center;
583 }
584
585 state.update_value_by_position(
586 axis, e.position, is_start, window, cx,
587 )
588 },
589 ),
590 )
591 })
592 .when(!self.disabled && !is_range, |this| {
593 this.on_drag(DragSlider(entity_id), |drag, _, _, cx| {
594 cx.stop_propagation();
595 cx.new(|_| drag.clone())
596 })
597 .on_drag_move(window.listener_for(
598 &self.state,
599 move |view, e: &DragMoveEvent<DragSlider>, window, cx| match e.drag(cx)
600 {
601 DragSlider(id) => {
602 if *id != entity_id {
603 return;
604 }
605
606 view.update_value_by_position(
607 axis,
608 e.event.position,
609 false,
610 window,
611 cx,
612 )
613 }
614 },
615 ))
616 })
617 .when(axis.is_horizontal(), |this| {
618 this.items_center().h_6().w_full()
619 })
620 .when(axis.is_vertical(), |this| {
621 this.justify_center().w_6().h_full()
622 })
623 .flex_shrink_0()
624 .child(
625 div()
626 .id("slider-bar")
627 .relative()
628 .when(axis.is_horizontal(), |this| this.w_full().h_1p5())
629 .when(axis.is_vertical(), |this| this.h_full().w_1p5())
630 .bg(bar_color.opacity(0.2))
631 .active(|this| this.bg(bar_color.opacity(0.4)))
632 .corner_radii(radius)
633 .child(
634 div()
635 .absolute()
636 .when(axis.is_horizontal(), |this| {
637 this.h_full().left(bar_start).right(bar_size - bar_end)
638 })
639 .when(axis.is_vertical(), |this| {
640 this.w_full().bottom(bar_start).top(bar_size - bar_end)
641 })
642 .bg(bar_color)
643 .rounded_full(),
644 )
645 .when(is_range, |this| {
646 this.child(self.render_thumb(
647 bar_start,
648 true,
649 bar_color,
650 thumb_color,
651 radius,
652 window,
653 cx,
654 ))
655 })
656 .child(self.render_thumb(
657 bar_end,
658 false,
659 bar_color,
660 thumb_color,
661 radius,
662 window,
663 cx,
664 ))
665 .child({
666 let state = self.state.clone();
667 canvas(
668 move |bounds, _, cx| state.update(cx, |r, _| r.bounds = bounds),
669 |_, _, _, _| {},
670 )
671 .absolute()
672 .size_full()
673 }),
674 ),
675 )
676 }
677}