use std::cell::{Cell, RefCell};
use std::rc::Rc;
use gpui_kit::component::h_flex;
use gpui_kit::prelude::FluentBuilder as _;
use gpui_kit::{
App, Bounds, DispatchPhase, InteractiveElement as _, IntoElement, MouseButton, MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels,
SharedString, Styled as _, TestSupportExt as _, Window, canvas, div, px, relative,
};
use crate::theme::{self, color};
pub type OnSlide = Rc<dyn Fn(f32, bool, &mut Window, &mut App)>;
thread_local! {
static SLIDING: RefCell<Option<(String, f32, f32)>> = const { RefCell::new(None) };
}
const TRACK_H: f32 = theme::TARGET_MIN;
const THUMB: f32 = 12.;
pub 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 {
let on_change: OnSlide = Rc::new(on_change);
let id: String = id.into();
let t = ((value - min) / (max - min).max(1e-12)).clamp(0.0, 1.0);
let active = SLIDING.with(|s| s.borrow().as_ref().is_some_and(|(k, _, _)| *k == id));
let bounds = Rc::new(Cell::new(Bounds::<Pixels>::default()));
let to_value = move |x: f32, left: f32, width: f32| min + ((x - left) / width.max(1.0)).clamp(0.0, 1.0) * (max - min);
let (b1, id1, ch1) = (bounds.clone(), id.clone(), on_change.clone());
let listener = canvas(
move |b, _, _| b1.set(b),
move |_, _, window, _| {
if !active {
return;
}
let (id, ch) = (id1.clone(), ch1.clone());
window.on_mouse_event(move |e: &MouseMoveEvent, phase, window, cx| {
if phase != DispatchPhase::Bubble {
return;
}
let Some((_, left, width)) = SLIDING.with(|s| s.borrow().clone()).filter(|(k, _, _)| *k == id) else { return };
if e.pressed_button != Some(MouseButton::Left) {
SLIDING.with(|s| *s.borrow_mut() = None);
ch(to_value(e.position.x.as_f32(), left, width), true, window, cx);
window.refresh();
return;
}
ch(to_value(e.position.x.as_f32(), left, width), false, window, cx);
});
let (id, ch) = (id1.clone(), ch1.clone());
window.on_mouse_event(move |e: &MouseUpEvent, phase, window, cx| {
if phase != DispatchPhase::Bubble {
return;
}
if let Some((_, left, width)) = SLIDING.with(|s| s.borrow_mut().take()).filter(|(k, _, _)| *k == id) {
ch(to_value(e.position.x.as_f32(), left, width), true, window, cx);
window.refresh();
}
});
},
)
.absolute()
.size_full();
let (b2, id2, ch2) = (bounds, id.clone(), on_change);
let bar_top = (TRACK_H - 4.) / 2.;
let track = div()
.id(SharedString::from(format!("{id}-track")))
.relative()
.flex_1()
.min_w(px(60.))
.h(px(TRACK_H))
.cursor_pointer()
.on_mouse_down(MouseButton::Left, move |e, window, cx| {
cx.stop_propagation();
let b = b2.get();
let (left, width) = (b.origin.x.as_f32(), b.size.width.as_f32());
SLIDING.with(|s| *s.borrow_mut() = Some((id2.clone(), left, width)));
ch2(to_value(e.position.x.as_f32(), left, width), false, window, cx);
window.refresh();
})
.child(div().absolute().left_0().right_0().top(px(bar_top)).h(px(4.)).rounded_full().bg(color::control()))
.child(div().absolute().left_0().top(px(bar_top)).h(px(4.)).w(relative(t)).rounded_full().bg(color::accent_mark()))
.child(
div()
.absolute()
.top(px((TRACK_H - THUMB) / 2.))
.left(relative(t))
.ml(px(-THUMB / 2.))
.size(px(THUMB))
.rounded_full()
.bg(gpui_kit::white())
.border_color(color::accent_mark())
.map(|d| if active { d.border_2() } else { d.border_1() }),
)
.child(listener)
.test_support();
h_flex()
.w_full()
.min_w_0()
.gap(px(theme::SM))
.child(track)
.child(div().flex_none().w(px(44.)).text_right().text_xs().font_family("monospace").child(fmt(value)))
}