1use std::cell::{Cell, RefCell};
4use std::rc::Rc;
5
6use gpui_kit::component::h_flex;
7use gpui_kit::prelude::FluentBuilder as _;
8use gpui_kit::{
9 App, Bounds, DispatchPhase, InteractiveElement as _, IntoElement, MouseButton, MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels,
10 SharedString, Styled as _, TestSupportExt as _, Window, canvas, div, px, relative,
11};
12
13use crate::theme::{self, color};
14
15pub type OnSlide = Rc<dyn Fn(f32, bool, &mut Window, &mut App)>;
17
18thread_local! {
19 static SLIDING: RefCell<Option<(String, f32, f32)>> = const { RefCell::new(None) };
21}
22
23const TRACK_H: f32 = theme::TARGET_MIN;
25const THUMB: f32 = 12.;
27
28pub fn slider(id: impl Into<String>, value: f32, (min, max): (f32, f32), fmt: impl Fn(f32) -> String, on_change: impl Fn(f32, bool, &mut Window, &mut App) + 'static) -> impl IntoElement {
33 let on_change: OnSlide = Rc::new(on_change);
34 let id: String = id.into();
35 let t = ((value - min) / (max - min).max(1e-12)).clamp(0.0, 1.0);
36 let active = SLIDING.with(|s| s.borrow().as_ref().is_some_and(|(k, _, _)| *k == id));
37 let bounds = Rc::new(Cell::new(Bounds::<Pixels>::default()));
38 let to_value = move |x: f32, left: f32, width: f32| min + ((x - left) / width.max(1.0)).clamp(0.0, 1.0) * (max - min);
39 let (b1, id1, ch1) = (bounds.clone(), id.clone(), on_change.clone());
41 let listener = canvas(
42 move |b, _, _| b1.set(b),
43 move |_, _, window, _| {
44 if !active {
45 return;
46 }
47 let (id, ch) = (id1.clone(), ch1.clone());
48 window.on_mouse_event(move |e: &MouseMoveEvent, phase, window, cx| {
49 if phase != DispatchPhase::Bubble {
50 return;
51 }
52 let Some((_, left, width)) = SLIDING.with(|s| s.borrow().clone()).filter(|(k, _, _)| *k == id) else { return };
53 if e.pressed_button != Some(MouseButton::Left) {
54 SLIDING.with(|s| *s.borrow_mut() = None);
55 ch(to_value(e.position.x.as_f32(), left, width), true, window, cx);
56 window.refresh();
57 return;
58 }
59 ch(to_value(e.position.x.as_f32(), left, width), false, window, cx);
60 });
61 let (id, ch) = (id1.clone(), ch1.clone());
62 window.on_mouse_event(move |e: &MouseUpEvent, phase, window, cx| {
63 if phase != DispatchPhase::Bubble {
64 return;
65 }
66 if let Some((_, left, width)) = SLIDING.with(|s| s.borrow_mut().take()).filter(|(k, _, _)| *k == id) {
67 ch(to_value(e.position.x.as_f32(), left, width), true, window, cx);
68 window.refresh();
69 }
70 });
71 },
72 )
73 .absolute()
74 .size_full();
75 let (b2, id2, ch2) = (bounds, id.clone(), on_change);
76 let bar_top = (TRACK_H - 4.) / 2.;
77 let track = div()
78 .id(SharedString::from(format!("{id}-track")))
79 .relative()
80 .flex_1()
81 .min_w(px(60.))
82 .h(px(TRACK_H))
83 .cursor_pointer()
84 .on_mouse_down(MouseButton::Left, move |e, window, cx| {
85 cx.stop_propagation();
86 let b = b2.get();
87 let (left, width) = (b.origin.x.as_f32(), b.size.width.as_f32());
88 SLIDING.with(|s| *s.borrow_mut() = Some((id2.clone(), left, width)));
89 ch2(to_value(e.position.x.as_f32(), left, width), false, window, cx);
90 window.refresh();
91 })
92 .child(div().absolute().left_0().right_0().top(px(bar_top)).h(px(4.)).rounded_full().bg(color::control()))
94 .child(div().absolute().left_0().top(px(bar_top)).h(px(4.)).w(relative(t)).rounded_full().bg(color::accent_mark()))
95 .child(
96 div()
97 .absolute()
98 .top(px((TRACK_H - THUMB) / 2.))
99 .left(relative(t))
100 .ml(px(-THUMB / 2.))
101 .size(px(THUMB))
102 .rounded_full()
103 .bg(gpui_kit::white())
104 .border_color(color::accent_mark())
105 .map(|d| if active { d.border_2() } else { d.border_1() }),
106 )
107 .child(listener)
108 .test_support();
109 h_flex()
110 .w_full()
111 .min_w_0()
112 .gap(px(theme::SM))
113 .child(track)
114 .child(div().flex_none().w(px(44.)).text_right().text_xs().font_family("monospace").child(fmt(value)))
115}