kitchen_ui/widgets/
stepper.rs1use std::cell::RefCell;
4use std::rc::Rc;
5
6use gpui_kit::component::button::{Button, ButtonVariants as _};
7use gpui_kit::component::{IconName, Sizable as _, h_flex};
8use gpui_kit::prelude::FluentBuilder as _;
9use gpui_kit::{
10 App, CursorStyle, DispatchPhase, InteractiveElement as _, IntoElement, MouseButton, MouseMoveEvent, MouseUpEvent, ParentElement as _,
11 SharedString, Styled as _, Window, canvas, div, px,
12};
13
14use crate::theme::{self, color};
15
16pub type OnStep = Rc<dyn Fn(f32, &mut Window, &mut App)>;
18
19pub const SCRUB_PX: f32 = 6.0;
21
22thread_local! {
23 static SCRUBBING: RefCell<Option<(String, f32)>> = const { RefCell::new(None) };
25}
26
27pub fn stepper(id: impl Into<String>, value: impl Into<SharedString>, on_step: impl Fn(f32, &mut Window, &mut App) + 'static) -> impl IntoElement {
32 let id: String = id.into();
33 let on_step: OnStep = Rc::new(on_step);
34 let (a, b, c) = (on_step.clone(), on_step.clone(), on_step);
35 h_flex()
36 .gap(px(theme::XS))
37 .child(Button::new(SharedString::from(format!("{id}-minus"))).ghost().small().icon(IconName::Minus).tooltip("Decrease").on_click(move |_, w, cx| a(-1.0, w, cx)))
38 .child(scrub(format!("{id}-value"), value.into(), 52.0, c))
39 .child(Button::new(SharedString::from(format!("{id}-plus"))).ghost().small().icon(IconName::Plus).tooltip("Increase").on_click(move |_, w, cx| b(1.0, w, cx)))
40}
41
42pub fn scrub_label(id: impl Into<String>, value: impl Into<SharedString>, on_step: impl Fn(f32, &mut Window, &mut App) + 'static) -> impl IntoElement {
46 scrub(id.into(), value.into(), 0.0, Rc::new(on_step))
47}
48
49fn scrub(id: String, value: SharedString, min_w: f32, on_step: OnStep) -> impl IntoElement {
50 let active = SCRUBBING.with(|s| s.borrow().as_ref().is_some_and(|(k, _)| *k == id));
51 let down_id = id.clone();
52 let listener = canvas(
54 |_, _, _| {},
55 move |_, _, window, _| {
56 if !active {
57 return;
58 }
59 let (id, step) = (id.clone(), on_step.clone());
60 window.on_mouse_event(move |e: &MouseMoveEvent, phase, window, cx| {
61 if phase != DispatchPhase::Bubble {
62 return;
63 }
64 if e.pressed_button != Some(MouseButton::Left) {
65 SCRUBBING.with(|s| *s.borrow_mut() = None);
66 window.refresh();
67 return;
68 }
69 let x = e.position.x.as_f32();
70 let steps = SCRUBBING.with(|s| match &mut *s.borrow_mut() {
71 Some((k, last)) if *k == id => {
72 let n = ((x - *last) / SCRUB_PX).trunc();
73 *last += n * SCRUB_PX;
74 n as i32
75 }
76 _ => 0,
77 });
78 for _ in 0..steps.unsigned_abs() {
79 step(steps.signum() as f32, window, cx);
80 }
81 });
82 window.on_mouse_event(|_: &MouseUpEvent, phase, window, _| {
83 if phase == DispatchPhase::Bubble {
84 SCRUBBING.with(|s| *s.borrow_mut() = None);
85 window.refresh();
86 }
87 });
88 },
89 )
90 .absolute()
91 .size_full();
92 div()
93 .id(SharedString::from(down_id.clone()))
94 .relative()
95 .min_w(px(min_w))
96 .min_h(px(theme::TARGET_MIN))
97 .flex()
98 .items_center()
99 .justify_center()
100 .px(px(theme::XS))
101 .rounded(px(theme::RADIUS_SM))
102 .text_sm()
103 .font_family("monospace")
104 .cursor(CursorStyle::ResizeLeftRight)
105 .when(active, |d| d.bg(color::accent().opacity(0.35)))
106 .when(!active, |d| d.hover(|d| d.bg(color::hover())))
107 .on_mouse_down(MouseButton::Left, move |e, window, cx| {
108 cx.stop_propagation();
109 SCRUBBING.with(|s| *s.borrow_mut() = Some((down_id.clone(), e.position.x.as_f32())));
110 window.refresh();
111 })
112 .child(value)
113 .child(listener)
114}