1use std::rc::Rc;
4
5use gpui::{
6 App, InteractiveElement, IntoElement, MouseButton, ParentElement, RenderOnce, SharedString,
7 Styled, Window, div, prelude::FluentBuilder, px,
8};
9use gpui_kit_semantics::{NodeSpec, Role, Semantic};
10use gpui_kit_theme::{ActiveTheme, ControlSize, Space, TypeScale};
11
12use crate::foundation::{
13 Disableable, FocusRing, Ident, Sizable, StyledExt, text as foundation_text,
14};
15use crate::layout::measure;
16use crate::motion::{self, keyed};
17
18#[derive(Default)]
25struct PointerDriven(bool);
26
27type ChangeHandler = Rc<dyn Fn(f32, &mut Window, &mut App)>;
28
29#[derive(IntoElement)]
35pub struct Slider {
36 ident: Ident,
37 label: Option<SharedString>,
38 min: f32,
39 max: f32,
40 value: f32,
41 step: Option<f32>,
42 size: ControlSize,
43 disabled: bool,
44 display: Option<SharedString>,
46 on_change: Option<ChangeHandler>,
47}
48
49impl std::fmt::Debug for Slider {
50 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51 formatter
52 .debug_struct("Slider")
53 .field("ident", &self.ident)
54 .field("range", &(self.min, self.max))
55 .field("value", &self.value)
56 .field("disabled", &self.disabled)
57 .field("has_handler", &self.on_change.is_some())
58 .finish()
59 }
60}
61
62impl Slider {
63 pub fn new(ident: impl Into<Ident>) -> Self {
64 Self {
65 ident: ident.into(),
66 label: None,
67 min: 0.0,
68 max: 1.0,
69 value: 0.0,
70 step: None,
71 size: ControlSize::Md,
72 disabled: false,
73 display: None,
74 on_change: None,
75 }
76 }
77
78 pub fn label(mut self, label: impl Into<SharedString>) -> Self {
79 self.label = Some(label.into());
80 self
81 }
82
83 pub fn range(mut self, min: f32, max: f32) -> Self {
86 let (min, max) = if min <= max { (min, max) } else { (max, min) };
87 self.min = min;
88 self.max = if (max - min).abs() < f32::EPSILON {
89 min + 1.0
90 } else {
91 max
92 };
93 self
94 }
95
96 pub fn value(mut self, value: f32) -> Self {
97 self.value = value;
98 self
99 }
100
101 pub fn step(mut self, step: f32) -> Self {
103 self.step = (step > 0.0).then_some(step);
104 self
105 }
106
107 pub fn display(mut self, display: impl Into<SharedString>) -> Self {
109 self.display = Some(display.into());
110 self
111 }
112
113 pub fn on_change(mut self, handler: impl Fn(f32, &mut Window, &mut App) + 'static) -> Self {
114 self.on_change = Some(Rc::new(handler));
115 self
116 }
117
118 fn clamped(&self) -> f32 {
119 self.value.clamp(self.min, self.max)
120 }
121
122 fn fraction(&self) -> f32 {
123 (self.clamped() - self.min) / (self.max - self.min)
124 }
125}
126
127impl Disableable for Slider {
128 fn disabled(mut self, disabled: bool) -> Self {
129 self.disabled = disabled;
130 self
131 }
132}
133
134impl Sizable for Slider {
135 fn control_size(mut self, size: ControlSize) -> Self {
136 self.size = size;
137 self
138 }
139}
140
141impl RenderOnce for Slider {
142 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
143 let theme = cx.theme().clone();
144 let metrics = theme.control.get(self.size);
145 let actionable = !self.disabled && self.on_change.is_some();
146 let dragging = keyed::slot::<PointerDriven>(&self.ident.semantic_id(), cx);
147 let snap = std::mem::take(&mut dragging.borrow_mut().0);
148 let fraction = motion::tracked_or_snap(
149 &self.ident.semantic_id(),
150 self.fraction(),
151 motion::tracking(&theme),
152 snap,
153 window,
154 cx,
155 );
156 let track_height = px(4.0);
157 let knob = px(metrics.icon_size);
160
161 let track_id = self.ident.clone();
164 let measured = measure::cell(&track_id.semantic_id(), cx);
167 let mut track = div()
168 .id(track_id.element_id())
169 .relative()
170 .w_full()
171 .h(knob)
172 .flex()
173 .items_center()
174 .when(actionable, |element| element.cursor_pointer())
175 .child(
176 div()
177 .absolute()
178 .left_0()
179 .right_0()
180 .h(track_height)
181 .rounded_full()
182 .bg(theme.colors.hairline_strong),
183 )
184 .child(
185 div()
186 .absolute()
187 .left_0()
188 .w(gpui::relative(fraction))
189 .h(track_height)
190 .rounded_full()
191 .bg(theme.colors.accent),
192 )
193 .child(
194 div()
195 .absolute()
196 .left(gpui::relative(fraction))
200 .ml(-(knob / 2.0))
201 .size(knob)
202 .rounded_full()
203 .bg(theme.colors.text)
204 .border(px(theme.borders.hairline))
205 .border_color(theme.colors.hairline_strong),
206 );
207
208 if actionable && let Some(handler) = self.on_change.clone() {
209 let (min, max, step) = (self.min, self.max, self.step);
210 let down = Rc::clone(&handler);
211 let down_bounds = Rc::clone(&measured);
212 let down_dragging = Rc::clone(&dragging);
213 track = track.on_mouse_down(MouseButton::Left, move |event, window, cx| {
214 let bounds = down_bounds.get();
215 let width = f32::from(bounds.size.width);
216 if width <= 0.0 {
217 return;
218 }
219 down_dragging.borrow_mut().0 = true;
220 let fraction =
221 (f32::from(event.position.x - bounds.left()) / width).clamp(0.0, 1.0);
222 down(
223 quantize(min + fraction * (max - min), min, max, step),
224 window,
225 cx,
226 );
227 });
228
229 let drag = Rc::clone(&handler);
230 let move_bounds = Rc::clone(&measured);
234 let move_dragging = Rc::clone(&dragging);
235 track = track.on_mouse_move(move |event, window, cx| {
236 if event.pressed_button != Some(MouseButton::Left) {
237 return;
238 }
239 let bounds = move_bounds.get();
240 let width = f32::from(bounds.size.width);
241 if width <= 0.0 {
242 return;
243 }
244 move_dragging.borrow_mut().0 = true;
245 let fraction =
246 (f32::from(event.position.x - bounds.left()) / width).clamp(0.0, 1.0);
247 drag(
248 quantize(min + fraction * (max - min), min, max, step),
249 window,
250 cx,
251 );
252 });
253 }
254
255 let value = self.clamped();
256 let keyboard_step = self.step.unwrap_or((self.max - self.min) / 20.0);
257 let mut spec = NodeSpec::new(self.ident.semantic_id(), Role::Slider)
258 .disabled(self.disabled)
259 .range(self.min, self.max, value);
260 if let Some(label) = self.label.clone() {
261 spec = spec.text(label);
262 }
263 if let Some(display) = self.display.clone() {
264 spec = spec.value(display);
265 }
266
267 let mut frame = div()
268 .id(self.ident.child("frame").element_id())
269 .flex()
270 .flex_col()
271 .gap(px(theme.space(Space::Xs)))
272 .w_full()
273 .when(self.disabled, |element| {
274 element.opacity(theme.opacity.disabled)
275 })
276 .when(actionable, |element| {
277 element.tab_index(0).focus_ring(&theme)
278 })
279 .when_some(self.label.clone(), |element, label| {
280 element.child(
281 div()
282 .flex()
283 .flex_row()
284 .justify_between()
285 .child(
286 foundation_text(&theme, TypeScale::Label, label)
287 .text_size(px(metrics.font_size))
288 .text_tone(&theme, gpui_kit_theme::TextTone::Muted),
289 )
290 .when_some(self.display.clone(), |element, display| {
291 element.child(
292 foundation_text(&theme, TypeScale::Label, display)
293 .text_size(px(metrics.font_size)),
294 )
295 }),
296 )
297 })
298 .child(
299 div()
300 .w_full()
301 .on_children_prepainted({
302 let measured = Rc::clone(&measured);
303 move |bounds, window, _| {
304 if let Some(first) = bounds.first() {
305 measure::record(&measured, *first, window);
306 }
307 }
308 })
309 .child(track)
310 .semantic_in(cx, spec),
311 );
312
313 if actionable && let Some(handler) = self.on_change.clone() {
314 let (min, max) = (self.min, self.max);
315 frame.interactivity().on_key_down(move |event, window, cx| {
316 let next = match event.keystroke.key.as_str() {
317 "left" | "down" => value - keyboard_step,
318 "right" | "up" => value + keyboard_step,
319 "home" => min,
320 "end" => max,
321 _ => return,
322 };
323 handler(next.clamp(min, max), window, cx);
324 cx.stop_propagation();
325 });
326 }
327
328 frame
329 }
330}
331
332fn quantize(value: f32, min: f32, max: f32, step: Option<f32>) -> f32 {
335 let value = value.clamp(min, max);
336 match step {
337 Some(step) => {
338 let steps = ((value - min) / step).round();
339 (min + steps * step).clamp(min, max)
340 }
341 None => value,
342 }
343}