1use std::ops::Range;
2
3use crate::{h_flex, 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 entity_id = self.state.entity_id();
432 let axis = self.axis;
433 let id = ("slider-thumb", is_start as u32);
434
435 if self.disabled {
436 return div().id(id);
437 }
438
439 div()
440 .id(id)
441 .absolute()
442 .when(axis.is_horizontal(), |this| {
443 this.top(px(-5.)).left(start_pos).ml(-px(8.))
444 })
445 .when(axis.is_vertical(), |this| {
446 this.bottom(start_pos).left(px(-5.)).mb(-px(8.))
447 })
448 .flex()
449 .items_center()
450 .justify_center()
451 .flex_shrink_0()
452 .corner_radii(radius)
453 .bg(bar_color.opacity(0.5))
454 .when(cx.theme().shadow, |this| this.shadow_md())
455 .size_4()
456 .p(px(1.))
457 .child(
458 div()
459 .flex_shrink_0()
460 .size_full()
461 .corner_radii(radius)
462 .bg(thumb_color),
463 )
464 .on_mouse_down(MouseButton::Left, |_, _, cx| {
465 cx.stop_propagation();
466 })
467 .on_drag(DragThumb((entity_id, is_start)), |drag, _, _, cx| {
468 cx.stop_propagation();
469 cx.new(|_| drag.clone())
470 })
471 .on_drag_move(window.listener_for(
472 &self.state,
473 move |view, e: &DragMoveEvent<DragThumb>, window, cx| {
474 match e.drag(cx) {
475 DragThumb((id, is_start)) => {
476 if *id != entity_id {
477 return;
478 }
479
480 view.update_value_by_position(
482 axis,
483 e.event.position,
484 *is_start,
485 window,
486 cx,
487 )
488 }
489 }
490 },
491 ))
492 }
493}
494
495impl Styled for Slider {
496 fn style(&mut self) -> &mut StyleRefinement {
497 &mut self.style
498 }
499}
500
501impl RenderOnce for Slider {
502 fn render(self, window: &mut Window, cx: &mut gpui::App) -> impl IntoElement {
503 let axis = self.axis;
504 let entity_id = self.state.entity_id();
505 let state = self.state.read(cx);
506 let is_range = state.value().is_range();
507 let bar_size = state.bounds.size.along(axis);
508 let bar_start = state.percentage.start * bar_size;
509 let bar_end = state.percentage.end * bar_size;
510 let rem_size = window.rem_size();
511
512 let bar_color = self
513 .style
514 .background
515 .clone()
516 .and_then(|bg| bg.color())
517 .unwrap_or(cx.theme().slider_bar.into());
518 let thumb_color = self
519 .style
520 .text
521 .clone()
522 .and_then(|text| text.color)
523 .unwrap_or_else(|| cx.theme().slider_thumb);
524 let corner_radii = self.style.corner_radii.clone();
525 let default_radius = px(999.);
526 let radius = Corners {
527 top_left: corner_radii
528 .top_left
529 .map(|v| v.to_pixels(rem_size))
530 .unwrap_or(default_radius),
531 top_right: corner_radii
532 .top_right
533 .map(|v| v.to_pixels(rem_size))
534 .unwrap_or(default_radius),
535 bottom_left: corner_radii
536 .bottom_left
537 .map(|v| v.to_pixels(rem_size))
538 .unwrap_or(default_radius),
539 bottom_right: corner_radii
540 .bottom_right
541 .map(|v| v.to_pixels(rem_size))
542 .unwrap_or(default_radius),
543 };
544
545 div()
546 .id(("slider", self.state.entity_id()))
547 .flex()
548 .flex_1()
549 .items_center()
550 .justify_center()
551 .when(axis.is_vertical(), |this| this.h(px(120.)))
552 .when(axis.is_horizontal(), |this| this.w_full())
553 .refine_style(&self.style)
554 .bg(cx.theme().transparent)
555 .text_color(cx.theme().foreground)
556 .child(
557 h_flex()
558 .id("slider-bar-container")
559 .when(!self.disabled, |this| {
560 this.on_mouse_down(
561 MouseButton::Left,
562 window.listener_for(
563 &self.state,
564 move |state, e: &MouseDownEvent, window, cx| {
565 let mut is_start = false;
566 if is_range {
567 let inner_pos = if axis.is_horizontal() {
568 e.position.x - state.bounds.left()
569 } else {
570 state.bounds.bottom() - e.position.y
571 };
572 let center = (bar_end - bar_start) / 2.0 + bar_start;
573 is_start = inner_pos < center;
574 }
575
576 state.update_value_by_position(
577 axis, e.position, is_start, window, cx,
578 )
579 },
580 ),
581 )
582 })
583 .when(!self.disabled && !is_range, |this| {
584 this.on_drag(DragSlider(entity_id), |drag, _, _, cx| {
585 cx.stop_propagation();
586 cx.new(|_| drag.clone())
587 })
588 .on_drag_move(window.listener_for(
589 &self.state,
590 move |view, e: &DragMoveEvent<DragSlider>, window, cx| match e.drag(cx)
591 {
592 DragSlider(id) => {
593 if *id != entity_id {
594 return;
595 }
596
597 view.update_value_by_position(
598 axis,
599 e.event.position,
600 false,
601 window,
602 cx,
603 )
604 }
605 },
606 ))
607 })
608 .when(axis.is_horizontal(), |this| {
609 this.items_center().h_6().w_full()
610 })
611 .when(axis.is_vertical(), |this| {
612 this.justify_center().w_6().h_full()
613 })
614 .flex_shrink_0()
615 .child(
616 div()
617 .id("slider-bar")
618 .relative()
619 .when(axis.is_horizontal(), |this| this.w_full().h_1p5())
620 .when(axis.is_vertical(), |this| this.h_full().w_1p5())
621 .bg(bar_color.opacity(0.2))
622 .active(|this| this.bg(bar_color.opacity(0.4)))
623 .corner_radii(radius)
624 .child(
625 div()
626 .absolute()
627 .when(axis.is_horizontal(), |this| {
628 this.h_full().left(bar_start).right(bar_size - bar_end)
629 })
630 .when(axis.is_vertical(), |this| {
631 this.w_full().bottom(bar_start).top(bar_size - bar_end)
632 })
633 .bg(bar_color)
634 .rounded_full(),
635 )
636 .when(is_range, |this| {
637 this.child(self.render_thumb(
638 bar_start,
639 true,
640 bar_color,
641 thumb_color,
642 radius,
643 window,
644 cx,
645 ))
646 })
647 .child(self.render_thumb(
648 bar_end,
649 false,
650 bar_color,
651 thumb_color,
652 radius,
653 window,
654 cx,
655 ))
656 .child({
657 let state = self.state.clone();
658 canvas(
659 move |bounds, _, cx| state.update(cx, |r, _| r.bounds = bounds),
660 |_, _, _, _| {},
661 )
662 .absolute()
663 .size_full()
664 }),
665 ),
666 )
667 }
668}