1use gpui::prelude::*;
10use gpui::{
11 div, px, relative, App, Context, Entity, EventEmitter, FocusHandle, IntoElement, KeyDownEvent,
12 SharedString, Window,
13};
14
15use crate::reactive::Signal;
16use crate::theme::{theme, ColorName, Size};
17
18#[derive(Debug, Clone, Copy)]
20pub struct SliderEvent(pub f64);
21
22pub struct Slider {
24 value: f64,
25 min: f64,
26 max: f64,
27 step: f64,
28 color: ColorName,
29 focus: FocusHandle,
30 disabled: bool,
31}
32
33impl EventEmitter<SliderEvent> for Slider {}
34
35impl Slider {
36 pub fn new(cx: &mut Context<Self>) -> Self {
37 Slider {
38 value: 0.0,
39 min: 0.0,
40 max: 100.0,
41 step: 1.0,
42 color: ColorName::Blue,
43 focus: cx.focus_handle(),
44 disabled: false,
45 }
46 }
47
48 pub fn value(mut self, value: f64) -> Self {
49 self.value = value;
50 self
51 }
52
53 pub fn min(mut self, min: f64) -> Self {
54 self.min = min;
55 self
56 }
57
58 pub fn max(mut self, max: f64) -> Self {
59 self.max = max;
60 self
61 }
62
63 pub fn step(mut self, step: f64) -> Self {
64 self.step = step.max(f64::EPSILON);
65 self
66 }
67
68 pub fn color(mut self, color: ColorName) -> Self {
69 self.color = color;
70 self
71 }
72
73 pub fn disabled(mut self, disabled: bool) -> Self {
74 self.disabled = disabled;
75 self
76 }
77
78 pub fn value_f64(&self) -> f64 {
79 self.value
80 }
81
82 pub fn bind(entity: &Entity<Slider>, signal: &Signal<f64>, cx: &mut App) {
88 let initial = signal.get(cx);
89 entity.update(cx, |this, cx| this.sync_value(initial, cx));
90 let sink = signal.clone();
91 cx.subscribe(entity, move |_slider, event: &SliderEvent, cx| {
92 sink.set_if_changed(cx, event.0);
93 })
94 .detach();
95 let slider = entity.downgrade();
96 cx.observe(signal.entity(), move |observed, cx| {
97 let value = *observed.read(cx);
98 slider
99 .update(cx, |this, cx| this.sync_value(value, cx))
100 .ok();
101 })
102 .detach();
103 }
104
105 fn sync_value(&mut self, raw: f64, cx: &mut Context<Self>) {
107 let next = self.snap(raw);
108 if next != self.value {
109 self.value = next;
110 cx.notify();
111 }
112 }
113
114 fn fraction(&self) -> f32 {
115 if self.max <= self.min {
116 0.0
117 } else {
118 (((self.value - self.min) / (self.max - self.min)) as f32).clamp(0.0, 1.0)
119 }
120 }
121
122 fn snap(&self, raw: f64) -> f64 {
123 let stepped = (raw / self.step).round() * self.step;
124 stepped.clamp(self.min, self.max)
125 }
126
127 fn set_value(&mut self, raw: f64, cx: &mut Context<Self>) {
128 if self.disabled {
129 return;
130 }
131 let next = self.snap(raw);
132 if next != self.value {
133 self.value = next;
134 cx.emit(SliderEvent(next));
135 cx.notify();
136 }
137 }
138
139 fn segment_count(&self) -> usize {
140 (((self.max - self.min) / self.step).round() as usize).clamp(1, 200)
141 }
142
143 fn on_key(&mut self, event: &KeyDownEvent, _window: &mut Window, cx: &mut Context<Self>) {
144 match event.keystroke.key.as_str() {
145 "left" | "down" => self.set_value(self.value - self.step, cx),
146 "right" | "up" => self.set_value(self.value + self.step, cx),
147 "home" => self.set_value(self.min, cx),
148 "end" => self.set_value(self.max, cx),
149 _ => return,
150 }
151 cx.stop_propagation();
152 }
153}
154
155impl Render for Slider {
156 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
157 let t = theme(cx);
158 let accent = t.color(self.color, t.primary_shade()).hsla();
159 let track_color = if t.scheme.is_dark() {
160 t.color(ColorName::Dark, 4)
161 } else {
162 t.color(ColorName::Gray, 2)
163 }
164 .hsla();
165 let knob_bg = t.surface().hsla();
166 let frac = self.fraction();
167
168 let knob = div()
169 .w(px(16.0))
170 .h(px(16.0))
171 .rounded(px(8.0))
172 .bg(knob_bg)
173 .border_2()
174 .border_color(accent);
175
176 let fill = div()
177 .h_full()
178 .w(relative(frac))
179 .rounded(px(3.0))
180 .bg(accent)
181 .flex()
182 .items_center()
183 .justify_end()
184 .child(knob);
185
186 let track = div()
187 .relative()
188 .w_full()
189 .h(px(6.0))
190 .rounded(px(3.0))
191 .bg(track_color)
192 .flex()
193 .items_center()
194 .child(fill);
195
196 let count = self.segment_count();
197 let min = self.min;
198 let span = self.max - self.min;
199 let mut overlay = div()
200 .absolute()
201 .top(px(0.0))
202 .left(px(0.0))
203 .right(px(0.0))
204 .bottom(px(0.0))
205 .flex()
206 .flex_row()
207 .items_center();
208 for i in 0..count {
209 let raw = min + (i as f64) / ((count - 1).max(1) as f64) * span;
210 overlay = overlay.child(
211 div()
212 .id(("guise-slider-seg", i))
213 .flex_grow()
214 .flex_basis(relative(0.0))
215 .h_full()
216 .on_click(cx.listener(move |this, _ev, _window, cx| this.set_value(raw, cx))),
217 );
218 }
219
220 let slider = div()
221 .id("guise-slider")
222 .track_focus(&self.focus)
223 .on_key_down(cx.listener(Self::on_key))
224 .relative()
225 .w_full()
226 .h(px(20.0))
227 .flex()
228 .items_center()
229 .child(track)
230 .child(overlay);
231
232 let value_label = div()
234 .text_size(px(t.font_size(Size::Xs)))
235 .text_color(t.dimmed().hsla())
236 .child(SharedString::from(format!("{}", self.value)));
237
238 let column = div()
239 .flex()
240 .flex_col()
241 .gap(px(4.0))
242 .child(slider)
243 .child(value_label);
244
245 if self.disabled {
246 column.opacity(0.5)
247 } else {
248 column
249 }
250 }
251}