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
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 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}