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, App, Context, Entity, EventEmitter, FocusHandle, IntoElement, KeyDownEvent,
12  SharedString, Window,
13};
14
15use crate::devtools::Probed;
16use crate::reactive::Signal;
17use crate::theme::{theme, ColorName, Size};
18
19/// Emitted when the slider value changes.
20#[derive(Debug, Clone, Copy)]
21pub struct SliderEvent(pub f64);
22
23/// A horizontal slider. Create with `cx.new(|cx| Slider::new(cx))`.
24pub struct Slider {
25  value: f64,
26  min: f64,
27  max: f64,
28  step: f64,
29  color: ColorName,
30  focus: FocusHandle,
31  disabled: bool,
32}
33
34impl EventEmitter<SliderEvent> for Slider {}
35
36impl Slider {
37  pub fn new(cx: &mut Context<Self>) -> Self {
38    Slider {
39      value: 0.0,
40      min: 0.0,
41      max: 100.0,
42      step: 1.0,
43      color: ColorName::Blue,
44      focus: cx.focus_handle(),
45      disabled: false,
46    }
47  }
48
49  pub fn value(mut self, value: f64) -> Self {
50    self.value = value;
51    self
52  }
53
54  pub fn min(mut self, min: f64) -> Self {
55    self.min = min;
56    self
57  }
58
59  pub fn max(mut self, max: f64) -> Self {
60    self.max = max;
61    self
62  }
63
64  pub fn step(mut self, step: f64) -> Self {
65    self.step = step.max(f64::EPSILON);
66    self
67  }
68
69  pub fn color(mut self, color: ColorName) -> Self {
70    self.color = color;
71    self
72  }
73
74  pub fn disabled(mut self, disabled: bool) -> Self {
75    self.disabled = disabled;
76    self
77  }
78
79  pub fn value_f64(&self) -> f64 {
80    self.value
81  }
82
83  /// Two-way bind this slider's value to a `Signal<f64>`. The signal is the
84  /// source of truth: the slider adopts its value now (snapped to `step`),
85  /// drags write back through [`Signal::set_if_changed`], and signal writes
86  /// move the knob without emitting [`SliderEvent`]. Equality guards on both
87  /// directions prevent update loops.
88  pub fn bind(entity: &Entity<Slider>, signal: &Signal<f64>, cx: &mut App) {
89    let initial = signal.get(cx);
90    entity.update(cx, |this, cx| this.sync_value(initial, cx));
91    let sink = signal.clone();
92    cx.subscribe(entity, move |_slider, event: &SliderEvent, cx| {
93      sink.set_if_changed(cx, event.0);
94    })
95    .detach();
96    let slider = entity.downgrade();
97    cx.observe(signal.entity(), move |observed, cx| {
98      let value = *observed.read(cx);
99      slider
100        .update(cx, |this, cx| this.sync_value(value, cx))
101        .ok();
102    })
103    .detach();
104  }
105
106  /// Programmatic set: snap and repaint without emitting an event.
107  fn sync_value(&mut self, raw: f64, cx: &mut Context<Self>) {
108    let next = self.snap(raw);
109    if next != self.value {
110      self.value = next;
111      cx.notify();
112    }
113  }
114
115  fn fraction(&self) -> f32 {
116    if self.max <= self.min {
117      0.0
118    } else {
119      (((self.value - self.min) / (self.max - self.min)) as f32).clamp(0.0, 1.0)
120    }
121  }
122
123  fn snap(&self, raw: f64) -> f64 {
124    snap(raw, self.min, self.max, self.step)
125  }
126
127  /// Move the handle programmatically. The value is clamped to the track
128  /// and snapped to the step, so a caller can pass a raw number.
129  pub fn set_value(&mut self, raw: f64, cx: &mut Context<Self>) {
130    if self.disabled {
131      return;
132    }
133    let next = self.snap(raw);
134    if next != self.value {
135      self.value = next;
136      cx.emit(SliderEvent(next));
137      cx.notify();
138    }
139  }
140
141  fn segment_count(&self) -> usize {
142    (((self.max - self.min) / self.step).round() as usize).clamp(1, 200)
143  }
144
145  fn on_key(&mut self, event: &KeyDownEvent, _window: &mut Window, cx: &mut Context<Self>) {
146    match event.keystroke.key.as_str() {
147      "left" | "down" => self.set_value(self.value - self.step, cx),
148      "right" | "up" => self.set_value(self.value + self.step, cx),
149      "home" => self.set_value(self.min, cx),
150      "end" => self.set_value(self.max, cx),
151      _ => return,
152    }
153    cx.stop_propagation();
154  }
155}
156
157fn snap(raw: f64, min: f64, max: f64, step: f64) -> f64 {
158  let stepped = min + ((raw - min) / step).round() * step;
159  stepped.clamp(min, max)
160}
161
162impl Render for Slider {
163  fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
164    let t = theme(cx);
165    let accent = t.color(self.color, t.primary_shade()).hsla();
166    let track_color = if t.scheme.is_dark() {
167      t.color(ColorName::Dark, 4)
168    } else {
169      t.color(ColorName::Gray, 2)
170    }
171    .hsla();
172    let knob_bg = t.surface().hsla();
173    let frac = self.fraction();
174
175    let knob = div()
176      .w(px(16.0))
177      .h(px(16.0))
178      .rounded(px(8.0))
179      .bg(knob_bg)
180      .border_2()
181      .border_color(accent);
182
183    let fill = div()
184      .h_full()
185      .w(relative(frac))
186      .rounded(px(3.0))
187      .bg(accent)
188      .flex()
189      .items_center()
190      .justify_end()
191      .child(knob);
192
193    let track = div()
194      .relative()
195      .w_full()
196      .h(px(6.0))
197      .rounded(px(3.0))
198      .bg(track_color)
199      .flex()
200      .items_center()
201      .child(fill);
202
203    let count = self.segment_count();
204    let min = self.min;
205    let span = self.max - self.min;
206    let mut overlay = div()
207      .absolute()
208      .top(px(0.0))
209      .left(px(0.0))
210      .right(px(0.0))
211      .bottom(px(0.0))
212      .flex()
213      .flex_row()
214      .items_center();
215    for i in 0..count {
216      let raw = min + (i as f64) / ((count - 1).max(1) as f64) * span;
217      overlay = overlay.child(
218        div()
219          .id(("guise-slider-seg", i))
220          .flex_grow()
221          .flex_basis(relative(0.0))
222          .h_full()
223          .on_click(cx.listener(move |this, _ev, _window, cx| this.set_value(raw, cx))),
224      );
225    }
226
227    let slider = div()
228      .id("guise-slider")
229      .track_focus(&self.focus)
230      .on_key_down(cx.listener(Self::on_key))
231      .relative()
232      .w_full()
233      .h(px(20.0))
234      .flex()
235      .items_center()
236      .child(track)
237      .child(overlay);
238
239    // A label keeps the value visible while dragging via clicks.
240    let value_label = div()
241      .text_size(px(t.font_size(Size::Xs)))
242      .text_color(t.dimmed().hsla())
243      .child(SharedString::from(format!("{}", self.value)));
244
245    let column = div()
246      .flex()
247      .flex_col()
248      .gap(px(4.0))
249      .child(slider)
250      .child(value_label);
251
252    let element = if self.disabled {
253      column.opacity(0.5)
254    } else {
255      column
256    };
257
258    element.probe("Slider")
259  }
260}
261
262#[cfg(test)]
263mod tests {
264  use super::*;
265
266  #[test]
267  fn step_grid_starts_at_minimum() {
268    assert_eq!(snap(6.8, 5.0, 15.0, 2.0), 7.0);
269    assert_eq!(snap(14.8, 5.0, 15.0, 2.0), 15.0);
270  }
271}