Skip to main content

guise/input/
slider.rs

1//! `Slider` — a draggable value track (gpui entity).
2//!
3//! Holds a continuous value in `min..=max` snapped to `step`. The track paints a
4//! filled portion and a knob; an overlaid row of invisible segment cells turns
5//! clicks into values (gpui doesn't hand elements their own bounds, so position
6//! is derived from discrete cells rather than the raw pointer x). Arrow keys
7//! nudge by one step. Emits [`SliderEvent`] on change.
8
9use gpui::prelude::*;
10use gpui::{
11    div, px, relative, Context, EventEmitter, FocusHandle, IntoElement, KeyDownEvent, SharedString,
12    Window,
13};
14
15use crate::theme::{theme, ColorName, Size};
16
17/// Emitted when the slider value changes.
18#[derive(Debug, Clone, Copy)]
19pub struct SliderEvent(pub f64);
20
21/// A horizontal slider. Create with `cx.new(|cx| Slider::new(cx))`.
22pub struct Slider {
23    value: f64,
24    min: f64,
25    max: f64,
26    step: f64,
27    color: ColorName,
28    focus: FocusHandle,
29    disabled: bool,
30}
31
32impl EventEmitter<SliderEvent> for Slider {}
33
34impl Slider {
35    pub fn new(cx: &mut Context<Self>) -> Self {
36        Slider {
37            value: 0.0,
38            min: 0.0,
39            max: 100.0,
40            step: 1.0,
41            color: ColorName::Blue,
42            focus: cx.focus_handle(),
43            disabled: false,
44        }
45    }
46
47    pub fn value(mut self, value: f64) -> Self {
48        self.value = value;
49        self
50    }
51
52    pub fn min(mut self, min: f64) -> Self {
53        self.min = min;
54        self
55    }
56
57    pub fn max(mut self, max: f64) -> Self {
58        self.max = max;
59        self
60    }
61
62    pub fn step(mut self, step: f64) -> Self {
63        self.step = step.max(f64::EPSILON);
64        self
65    }
66
67    pub fn color(mut self, color: ColorName) -> Self {
68        self.color = color;
69        self
70    }
71
72    pub fn disabled(mut self, disabled: bool) -> Self {
73        self.disabled = disabled;
74        self
75    }
76
77    pub fn value_f64(&self) -> f64 {
78        self.value
79    }
80
81    fn fraction(&self) -> f32 {
82        if self.max <= self.min {
83            0.0
84        } else {
85            (((self.value - self.min) / (self.max - self.min)) as f32).clamp(0.0, 1.0)
86        }
87    }
88
89    fn snap(&self, raw: f64) -> f64 {
90        let stepped = (raw / self.step).round() * self.step;
91        stepped.clamp(self.min, self.max)
92    }
93
94    fn set_value(&mut self, raw: f64, cx: &mut Context<Self>) {
95        if self.disabled {
96            return;
97        }
98        let next = self.snap(raw);
99        if next != self.value {
100            self.value = next;
101            cx.emit(SliderEvent(next));
102            cx.notify();
103        }
104    }
105
106    fn segment_count(&self) -> usize {
107        (((self.max - self.min) / self.step).round() as usize).clamp(1, 200)
108    }
109
110    fn on_key(&mut self, event: &KeyDownEvent, _window: &mut Window, cx: &mut Context<Self>) {
111        match event.keystroke.key.as_str() {
112            "left" | "down" => self.set_value(self.value - self.step, cx),
113            "right" | "up" => self.set_value(self.value + self.step, cx),
114            "home" => self.set_value(self.min, cx),
115            "end" => self.set_value(self.max, cx),
116            _ => return,
117        }
118        cx.stop_propagation();
119    }
120}
121
122impl Render for Slider {
123    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
124        let t = theme(cx);
125        let accent = t.color(self.color, t.primary_shade()).hsla();
126        let track_color = if t.scheme.is_dark() {
127            t.color(ColorName::Dark, 4)
128        } else {
129            t.color(ColorName::Gray, 2)
130        }
131        .hsla();
132        let knob_bg = t.surface().hsla();
133        let frac = self.fraction();
134
135        let knob = div()
136            .w(px(16.0))
137            .h(px(16.0))
138            .rounded(px(8.0))
139            .bg(knob_bg)
140            .border_2()
141            .border_color(accent);
142
143        let fill = div()
144            .h_full()
145            .w(relative(frac))
146            .rounded(px(3.0))
147            .bg(accent)
148            .flex()
149            .items_center()
150            .justify_end()
151            .child(knob);
152
153        let track = div()
154            .relative()
155            .w_full()
156            .h(px(6.0))
157            .rounded(px(3.0))
158            .bg(track_color)
159            .flex()
160            .items_center()
161            .child(fill);
162
163        let count = self.segment_count();
164        let min = self.min;
165        let span = self.max - self.min;
166        let mut overlay = div()
167            .absolute()
168            .top(px(0.0))
169            .left(px(0.0))
170            .right(px(0.0))
171            .bottom(px(0.0))
172            .flex()
173            .flex_row()
174            .items_center();
175        for i in 0..count {
176            let raw = min + (i as f64) / ((count - 1).max(1) as f64) * span;
177            overlay = overlay.child(
178                div()
179                    .id(("guise-slider-seg", i))
180                    .flex_grow()
181                    .flex_basis(relative(0.0))
182                    .h_full()
183                    .on_click(cx.listener(move |this, _ev, _window, cx| this.set_value(raw, cx))),
184            );
185        }
186
187        let slider = div()
188            .id("guise-slider")
189            .track_focus(&self.focus)
190            .on_key_down(cx.listener(Self::on_key))
191            .relative()
192            .w_full()
193            .h(px(20.0))
194            .flex()
195            .items_center()
196            .child(track)
197            .child(overlay);
198
199        // A label keeps the value visible while dragging via clicks.
200        let value_label = div()
201            .text_size(px(t.font_size(Size::Xs)))
202            .text_color(t.dimmed().hsla())
203            .child(SharedString::from(format!("{}", self.value)));
204
205        let column = div()
206            .flex()
207            .flex_col()
208            .gap(px(4.0))
209            .child(slider)
210            .child(value_label);
211
212        if self.disabled {
213            column.opacity(0.5)
214        } else {
215            column
216        }
217    }
218}