1use 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#[derive(Debug, Clone, Copy)]
21pub struct SliderEvent(pub f64);
22
23pub 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 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 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 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
162#[cfg(test)]
163mod tests {
164 use super::*;
165
166 #[test]
167 fn step_grid_starts_at_minimum() {
168 assert_eq!(snap(6.8, 5.0, 15.0, 2.0), 7.0);
169 assert_eq!(snap(14.8, 5.0, 15.0, 2.0), 15.0);
170 }
171}
172
173impl Render for Slider {
174 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
175 let t = theme(cx);
176 let accent = t.color(self.color, t.primary_shade()).hsla();
177 let track_color = if t.scheme.is_dark() {
178 t.color(ColorName::Dark, 4)
179 } else {
180 t.color(ColorName::Gray, 2)
181 }
182 .hsla();
183 let knob_bg = t.surface().hsla();
184 let frac = self.fraction();
185
186 let knob = div()
187 .w(px(16.0))
188 .h(px(16.0))
189 .rounded(px(8.0))
190 .bg(knob_bg)
191 .border_2()
192 .border_color(accent);
193
194 let fill = div()
195 .h_full()
196 .w(relative(frac))
197 .rounded(px(3.0))
198 .bg(accent)
199 .flex()
200 .items_center()
201 .justify_end()
202 .child(knob);
203
204 let track = div()
205 .relative()
206 .w_full()
207 .h(px(6.0))
208 .rounded(px(3.0))
209 .bg(track_color)
210 .flex()
211 .items_center()
212 .child(fill);
213
214 let count = self.segment_count();
215 let min = self.min;
216 let span = self.max - self.min;
217 let mut overlay = div()
218 .absolute()
219 .top(px(0.0))
220 .left(px(0.0))
221 .right(px(0.0))
222 .bottom(px(0.0))
223 .flex()
224 .flex_row()
225 .items_center();
226 for i in 0..count {
227 let raw = min + (i as f64) / ((count - 1).max(1) as f64) * span;
228 overlay = overlay.child(
229 div()
230 .id(("guise-slider-seg", i))
231 .flex_grow()
232 .flex_basis(relative(0.0))
233 .h_full()
234 .on_click(cx.listener(move |this, _ev, _window, cx| this.set_value(raw, cx))),
235 );
236 }
237
238 let slider = div()
239 .id("guise-slider")
240 .track_focus(&self.focus)
241 .on_key_down(cx.listener(Self::on_key))
242 .relative()
243 .w_full()
244 .h(px(20.0))
245 .flex()
246 .items_center()
247 .child(track)
248 .child(overlay);
249
250 let value_label = div()
252 .text_size(px(t.font_size(Size::Xs)))
253 .text_color(t.dimmed().hsla())
254 .child(SharedString::from(format!("{}", self.value)));
255
256 let column = div()
257 .flex()
258 .flex_col()
259 .gap(px(4.0))
260 .child(slider)
261 .child(value_label);
262
263 let element = if self.disabled {
264 column.opacity(0.5)
265 } else {
266 column
267 };
268
269 element.probe("Slider")
270 }
271}