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)]
12pub struct 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
20pub enum SliderEvent {
21 Change(SliderValue),
22}
23
24#[derive(Clone, Copy, Debug, PartialEq)]
31pub enum SliderValue {
32 Single(f32),
33 Range(f32, f32),
34}
35
36impl std::fmt::Display for SliderValue {
37 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38 match self {
39 SliderValue::Single(value) => write!(f, "{}", value),
40 SliderValue::Range(start, end) => write!(f, "{}..{}", start, end),
41 }
42 }
43}
44
45impl From<f32> for SliderValue {
46 fn from(value: f32) -> Self {
47 SliderValue::Single(value)
48 }
49}
50
51impl From<(f32, f32)> for SliderValue {
52 fn from(value: (f32, f32)) -> Self {
53 SliderValue::Range(value.0, value.1)
54 }
55}
56
57impl From<Range<f32>> for SliderValue {
58 fn from(value: Range<f32>) -> Self {
59 SliderValue::Range(value.start, value.end)
60 }
61}
62
63impl Default for SliderValue {
64 fn default() -> Self {
65 SliderValue::Single(0.)
66 }
67}
68
69impl SliderValue {
70 pub fn clamp(self, min: f32, max: f32) -> Self {
72 match self {
73 SliderValue::Single(value) => SliderValue::Single(value.clamp(min, max)),
74 SliderValue::Range(start, end) => {
75 SliderValue::Range(start.clamp(min, max), end.clamp(min, max))
76 }
77 }
78 }
79
80 #[inline]
81 pub fn is_single(&self) -> bool {
82 matches!(self, SliderValue::Single(_))
83 }
84
85 #[inline]
86 pub fn is_range(&self) -> bool {
87 matches!(self, SliderValue::Range(_, _))
88 }
89
90 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 {
98 match self {
99 SliderValue::Single(value) => *value,
100 SliderValue::Range(_, end) => *end,
101 }
102 }
103
104 fn set_start(&mut self, value: f32) {
105 if let SliderValue::Range(_, end) = self {
106 *self = SliderValue::Range(value.min(*end), *end);
107 } else {
108 *self = SliderValue::Single(value);
109 }
110 }
111
112 fn set_end(&mut self, value: f32) {
113 if let SliderValue::Range(start, _) = self {
114 *self = SliderValue::Range(*start, value.max(*start));
115 } else {
116 *self = SliderValue::Single(value);
117 }
118 }
119}
120
121pub struct SliderState {
123 min: f32,
124 max: f32,
125 step: f32,
126 value: SliderValue,
127 percentage: Range<f32>,
129 bounds: Bounds<Pixels>,
131}
132
133impl SliderState {
134 pub fn new() -> Self {
135 Self {
136 min: 0.0,
137 max: 100.0,
138 step: 1.0,
139 value: SliderValue::default(),
140 percentage: (0.0..0.0),
141 bounds: Bounds::default(),
142 }
143 }
144
145 pub fn min(mut self, min: f32) -> Self {
147 self.min = min;
148 self.update_thumb_pos();
149 self
150 }
151
152 pub fn max(mut self, max: f32) -> Self {
154 self.max = max;
155 self.update_thumb_pos();
156 self
157 }
158
159 pub fn step(mut self, step: f32) -> Self {
161 self.step = step;
162 self
163 }
164
165 pub fn default_value(mut self, value: impl Into<SliderValue>) -> Self {
167 self.value = value.into();
168 self.update_thumb_pos();
169 self
170 }
171
172 pub fn set_value(
174 &mut self,
175 value: impl Into<SliderValue>,
176 _: &mut Window,
177 cx: &mut Context<Self>,
178 ) {
179 self.value = value.into();
180 self.update_thumb_pos();
181 cx.notify();
182 }
183
184 pub fn value(&self) -> SliderValue {
186 self.value
187 }
188
189 fn update_thumb_pos(&mut self) {
190 match self.value {
191 SliderValue::Single(value) => {
192 let percentage = value.clamp(self.min, self.max) / self.max;
193 self.percentage = 0.0..percentage;
194 }
195 SliderValue::Range(start, end) => {
196 let clamped_start = start.clamp(self.min, self.max);
197 let clamped_end = end.clamp(self.min, self.max);
198 self.percentage = (clamped_start / self.max)..(clamped_end / self.max);
199 }
200 }
201 }
202
203 fn update_value_by_position(
205 &mut self,
206 axis: Axis,
207 position: Point<Pixels>,
208 is_start: bool,
209 _: &mut Window,
210 cx: &mut Context<Self>,
211 ) {
212 let bounds = self.bounds;
213 let min = self.min;
214 let max = self.max;
215 let step = self.step;
216
217 let inner_pos = if axis.is_horizontal() {
218 position.x - bounds.left()
219 } else {
220 bounds.bottom() - position.y
221 };
222 let total_size = bounds.size.along(axis);
223 let percentage = inner_pos.clamp(px(0.), total_size) / total_size;
224
225 let percentage = if is_start {
226 percentage.clamp(0.0, self.percentage.end)
227 } else {
228 percentage.clamp(self.percentage.start, 1.0)
229 };
230
231 let value = min + (max - min) * percentage;
232 let value = (value / step).round() * step;
233
234 if is_start {
235 self.percentage.start = percentage;
236 self.value.set_start(value);
237 } else {
238 self.percentage.end = percentage;
239 self.value.set_end(value);
240 }
241 cx.emit(SliderEvent::Change(self.value));
242 cx.notify();
243 }
244}
245
246impl EventEmitter<SliderEvent> for SliderState {}
247impl Render for SliderState {
248 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
249 Empty
250 }
251}
252
253#[derive(IntoElement)]
255pub struct Slider {
256 state: Entity<SliderState>,
257 axis: Axis,
258 style: StyleRefinement,
259 disabled: bool,
260}
261
262impl Slider {
263 pub fn new(state: &Entity<SliderState>) -> Self {
265 Self {
266 axis: Axis::Horizontal,
267 state: state.clone(),
268 style: StyleRefinement::default(),
269 disabled: false,
270 }
271 }
272
273 pub fn horizontal(mut self) -> Self {
275 self.axis = Axis::Horizontal;
276 self
277 }
278
279 pub fn vertical(mut self) -> Self {
281 self.axis = Axis::Vertical;
282 self
283 }
284
285 pub fn disabled(mut self, disabled: bool) -> Self {
287 self.disabled = disabled;
288 self
289 }
290
291 #[allow(clippy::too_many_arguments)]
292 fn render_thumb(
293 &self,
294 start_pos: Pixels,
295 is_start: bool,
296 bar_color: Background,
297 thumb_color: Hsla,
298 radius: Corners<Pixels>,
299 window: &mut Window,
300 cx: &mut App,
301 ) -> impl gpui::IntoElement {
302 let state = self.state.read(cx);
303 let entity_id = self.state.entity_id();
304 let value = state.value;
305 let axis = self.axis;
306 let id = ("slider-thumb", is_start as u32);
307
308 if self.disabled {
309 return div().id(id);
310 }
311
312 div()
313 .id(id)
314 .absolute()
315 .when(axis.is_horizontal(), |this| {
316 this.top(px(-5.)).left(start_pos).ml(-px(8.))
317 })
318 .when(axis.is_vertical(), |this| {
319 this.bottom(start_pos).left(px(-5.)).mb(-px(8.))
320 })
321 .flex()
322 .items_center()
323 .justify_center()
324 .flex_shrink_0()
325 .corner_radii(radius)
326 .bg(bar_color.opacity(0.5))
327 .when(cx.theme().shadow, |this| this.shadow_md())
328 .size_4()
329 .p(px(1.))
330 .child(
331 div()
332 .flex_shrink_0()
333 .size_full()
334 .corner_radii(radius)
335 .bg(thumb_color),
336 )
337 .on_mouse_down(MouseButton::Left, |_, _, cx| {
338 cx.stop_propagation();
339 })
340 .on_drag(DragThumb((entity_id, is_start)), |drag, _, _, cx| {
341 cx.stop_propagation();
342 cx.new(|_| drag.clone())
343 })
344 .on_drag_move(window.listener_for(
345 &self.state,
346 move |view, e: &DragMoveEvent<DragThumb>, window, cx| {
347 match e.drag(cx) {
348 DragThumb((id, is_start)) => {
349 if *id != entity_id {
350 return;
351 }
352
353 view.update_value_by_position(
355 axis,
356 e.event.position,
357 *is_start,
358 window,
359 cx,
360 )
361 }
362 }
363 },
364 ))
365 .tooltip(move |window, cx| {
366 Tooltip::new(format!(
367 "{}",
368 if is_start { value.start() } else { value.end() }
369 ))
370 .build(window, cx)
371 })
372 }
373}
374
375impl Styled for Slider {
376 fn style(&mut self) -> &mut StyleRefinement {
377 &mut self.style
378 }
379}
380
381impl RenderOnce for Slider {
382 fn render(self, window: &mut Window, cx: &mut gpui::App) -> impl IntoElement {
383 let axis = self.axis;
384 let state = self.state.read(cx);
385 let is_range = state.value().is_range();
386 let bar_size = state.bounds.size.along(axis);
387 let bar_start = state.percentage.start * bar_size;
388 let bar_end = state.percentage.end * bar_size;
389 let rem_size = window.rem_size();
390
391 let bar_color = self
392 .style
393 .background
394 .clone()
395 .and_then(|bg| bg.color())
396 .unwrap_or(cx.theme().slider_bar.into());
397 let thumb_color = self
398 .style
399 .text
400 .clone()
401 .and_then(|text| text.color)
402 .unwrap_or_else(|| cx.theme().slider_thumb);
403 let corner_radii = self.style.corner_radii.clone();
404 let default_radius = px(999.);
405 let radius = Corners {
406 top_left: corner_radii
407 .top_left
408 .map(|v| v.to_pixels(rem_size))
409 .unwrap_or(default_radius),
410 top_right: corner_radii
411 .top_right
412 .map(|v| v.to_pixels(rem_size))
413 .unwrap_or(default_radius),
414 bottom_left: corner_radii
415 .bottom_left
416 .map(|v| v.to_pixels(rem_size))
417 .unwrap_or(default_radius),
418 bottom_right: corner_radii
419 .bottom_right
420 .map(|v| v.to_pixels(rem_size))
421 .unwrap_or(default_radius),
422 };
423
424 div()
425 .id(("slider", self.state.entity_id()))
426 .flex()
427 .flex_1()
428 .items_center()
429 .justify_center()
430 .when(axis.is_vertical(), |this| this.h(px(120.)))
431 .when(axis.is_horizontal(), |this| this.w_full())
432 .refine_style(&self.style)
433 .bg(cx.theme().transparent)
434 .text_color(cx.theme().foreground)
435 .child(
436 h_flex()
437 .when(!self.disabled, |this| {
438 this.on_mouse_down(
439 MouseButton::Left,
440 window.listener_for(
441 &self.state,
442 move |state, e: &MouseDownEvent, window, cx| {
443 let mut is_start = false;
444 if is_range {
445 let inner_pos = if axis.is_horizontal() {
446 e.position.x - state.bounds.left()
447 } else {
448 state.bounds.bottom() - e.position.y
449 };
450 let center = (bar_end - bar_start) / 2.0 + bar_start;
451 is_start = inner_pos < center;
452 }
453
454 state.update_value_by_position(
455 axis, e.position, is_start, window, cx,
456 )
457 },
458 ),
459 )
460 })
461 .when(axis.is_horizontal(), |this| {
462 this.items_center().h_6().w_full()
463 })
464 .when(axis.is_vertical(), |this| {
465 this.justify_center().w_6().h_full()
466 })
467 .flex_shrink_0()
468 .child(
469 div()
470 .id("slider-bar")
471 .relative()
472 .when(axis.is_horizontal(), |this| this.w_full().h_1p5())
473 .when(axis.is_vertical(), |this| this.h_full().w_1p5())
474 .bg(bar_color.opacity(0.2))
475 .active(|this| this.bg(bar_color.opacity(0.4)))
476 .corner_radii(radius)
477 .child(
478 div()
479 .absolute()
480 .when(axis.is_horizontal(), |this| {
481 this.h_full().left(bar_start).right(bar_size - bar_end)
482 })
483 .when(axis.is_vertical(), |this| {
484 this.w_full().bottom(bar_start).top(bar_size - bar_end)
485 })
486 .bg(bar_color)
487 .rounded_full(),
488 )
489 .when(is_range, |this| {
490 this.child(self.render_thumb(
491 bar_start,
492 true,
493 bar_color,
494 thumb_color,
495 radius,
496 window,
497 cx,
498 ))
499 })
500 .child(self.render_thumb(
501 bar_end,
502 false,
503 bar_color,
504 thumb_color,
505 radius,
506 window,
507 cx,
508 ))
509 .child({
510 let state = self.state.clone();
511 canvas(
512 move |bounds, _, cx| state.update(cx, |r, _| r.bounds = bounds),
513 |_, _, _, _| {},
514 )
515 .absolute()
516 .size_full()
517 }),
518 ),
519 )
520 }
521}