1use std::{cell::Cell, rc::Rc};
2
3use gpui::{Bounds, Context, MouseButton, MouseDownEvent, MouseMoveEvent, Pixels, Render, canvas};
4
5use crate::prelude::*;
6
7const THUMB_SIZE: Pixels = px(16.);
8const TRACK_HEIGHT: Pixels = px(4.);
9
10pub struct Slider {
18 min: f32,
19 max: f32,
20 step: f32,
21 value: f32,
23 range_high: f32,
25 range_mode: bool,
26 disabled: bool,
27 dragging_thumb: Option<usize>,
28 track_bounds: Rc<Cell<Option<Bounds<Pixels>>>>,
29}
30
31impl Slider {
32 pub fn new(_cx: &mut Context<Self>) -> Self {
33 Self {
34 min: 0.,
35 max: 100.,
36 step: 1.,
37 value: 0.,
38 range_high: 100.,
39 range_mode: false,
40 disabled: false,
41 dragging_thumb: None,
42 track_bounds: Rc::new(Cell::new(None)),
43 }
44 }
45
46 pub fn min(mut self, min: f32) -> Self {
47 self.min = min;
48 self
49 }
50
51 pub fn max(mut self, max: f32) -> Self {
52 self.max = max;
53 self
54 }
55
56 pub fn step(mut self, step: f32) -> Self {
57 self.step = step.max(f32::MIN_POSITIVE);
58 self
59 }
60
61 pub fn value(mut self, value: f32) -> Self {
62 self.value = value;
63 self
64 }
65
66 pub fn range(mut self, low: f32, high: f32) -> Self {
68 self.range_mode = true;
69 self.value = low;
70 self.range_high = high;
71 self
72 }
73
74 pub fn disabled(mut self, disabled: bool) -> Self {
75 self.disabled = disabled;
76 self
77 }
78
79 pub fn values(&self) -> (f32, f32) {
80 if self.range_mode {
81 (
82 self.clamp_value(self.value),
83 self.clamp_value(self.range_high),
84 )
85 } else {
86 (self.clamp_value(self.value), self.clamp_value(self.value))
87 }
88 }
89
90 fn clamp_value(&self, raw: f32) -> f32 {
91 let (lo, hi) = (self.min.min(self.max), self.min.max(self.max));
95 let clamped = raw.clamp(lo, hi);
96 if self.step <= 0. {
97 return clamped;
98 }
99 let steps = ((clamped - lo) / self.step).round();
100 (lo + steps * self.step).clamp(lo, hi)
101 }
102
103 fn fraction_from_x(&self, x: Pixels, bounds: &Bounds<Pixels>) -> f32 {
104 if bounds.size.width <= Pixels::ZERO {
105 return 0.;
106 }
107 let offset = (x - bounds.origin.x).clamp(Pixels::ZERO, bounds.size.width);
108 (offset / bounds.size.width).clamp(0., 1.)
109 }
110
111 fn value_from_x(&self, x: Pixels, bounds: &Bounds<Pixels>) -> f32 {
112 let fraction = self.fraction_from_x(x, bounds);
113 self.clamp_value(self.min + fraction * (self.max - self.min))
114 }
115
116 fn nearest_thumb(&self, value: f32) -> usize {
117 if !self.range_mode {
118 return 0;
119 }
120 let low_dist = (value - self.value).abs();
121 let high_dist = (value - self.range_high).abs();
122 if high_dist < low_dist { 1 } else { 0 }
123 }
124
125 fn set_thumb_value(&mut self, thumb: usize, value: f32, cx: &mut Context<Self>) {
126 let value = self.clamp_value(value);
127 if self.range_mode {
128 if thumb == 0 {
129 self.value = value.min(self.range_high);
130 } else {
131 self.range_high = value.max(self.value);
132 }
133 } else {
134 self.value = value;
135 }
136 cx.notify();
137 }
138
139 fn update_from_position(&mut self, x: Pixels, thumb: Option<usize>, cx: &mut Context<Self>) {
140 let Some(bounds) = self.track_bounds.get() else {
141 return;
142 };
143 let value = self.value_from_x(x, &bounds);
144 let thumb = thumb.unwrap_or_else(|| self.nearest_thumb(value));
145 self.set_thumb_value(thumb, value, cx);
146 }
147}
148
149impl Render for Slider {
150 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
151 let (low, high) = self.values();
152 let span = (self.max - self.min).max(f32::MIN_POSITIVE);
153 let low_pct = (low - self.min) / span;
154 let high_pct = (high - self.min) / span;
155 let range_mode = self.range_mode;
156 let disabled = self.disabled;
157 let track_bounds = self.track_bounds.clone();
158 let dragging = self.dragging_thumb;
159
160 let thumb = |id: ElementId, pct: f32| {
161 div()
162 .id(id)
163 .absolute()
164 .top(px(-6.))
165 .left(relative(pct))
166 .ml(-THUMB_SIZE / 2.)
167 .size(THUMB_SIZE)
168 .rounded_full()
169 .bg(palette::primary(600))
170 .border_2()
171 .border_color(semantic::elevated_surface(cx))
172 .shadow_level(Shadow::Sm)
173 .when(!disabled, |this| this.cursor_grab())
174 };
175
176 let range_left = low_pct.min(high_pct);
177 let range_width = (high_pct - low_pct).abs();
178
179 div()
180 .id("slider")
181 .w_full()
182 .py_3()
183 .when(disabled, |this| this.opacity(0.5))
184 .child(
185 div()
186 .id("slider-track-container")
187 .relative()
188 .w_full()
189 .h(THUMB_SIZE)
190 .child(
191 div()
192 .absolute()
193 .top(px(6.))
194 .left_0()
195 .right_0()
196 .h(TRACK_HEIGHT)
197 .rounded_full()
198 .bg(semantic::border_muted(cx)),
199 )
200 .child(
201 div()
202 .absolute()
203 .top(px(6.))
204 .left(relative(range_left))
205 .w(relative(range_width.max(if range_mode {
206 0.
207 } else {
208 high_pct
209 })))
210 .h(TRACK_HEIGHT)
211 .rounded_full()
212 .bg(palette::primary(500)),
213 )
214 .child(thumb("slider-thumb-low".into(), low_pct))
215 .when(range_mode, |this| {
216 this.child(thumb("slider-thumb-high".into(), high_pct))
217 })
218 .child({
219 let track_bounds = track_bounds.clone();
220 canvas(
221 move |bounds, _window, _cx| track_bounds.set(Some(bounds)),
222 |_bounds, _state, _window, _cx| {},
223 )
224 .absolute()
225 .top_0()
226 .left_0()
227 .size_full()
228 })
229 .when(!disabled, |this| {
230 this.on_mouse_down(
231 MouseButton::Left,
232 cx.listener(|this, event: &MouseDownEvent, _window, cx| {
233 let value = this
234 .track_bounds
235 .get()
236 .map(|b| this.value_from_x(event.position.x, &b));
237 let thumb = value.map(|v| this.nearest_thumb(v));
238 this.dragging_thumb = thumb;
239 this.update_from_position(event.position.x, thumb, cx);
240 }),
241 )
242 .on_mouse_up(
243 MouseButton::Left,
244 cx.listener(|this, _, _, cx| {
245 this.dragging_thumb = None;
246 cx.notify();
247 }),
248 )
249 .on_mouse_move(cx.listener(
250 |this, event: &MouseMoveEvent, _, cx| {
251 if event.dragging() || this.dragging_thumb.is_some() {
252 this.update_from_position(
253 event.position.x,
254 this.dragging_thumb,
255 cx,
256 );
257 }
258 },
259 ))
260 }),
261 )
262 .child(
263 h_flex()
264 .mt_1()
265 .justify_between()
266 .child(
267 Label::new(format!("{low:.0}"))
268 .size(LabelSize::XSmall)
269 .color(Color::Muted),
270 )
271 .when(range_mode, |this| {
272 this.child(
273 Label::new(format!("{high:.0}"))
274 .size(LabelSize::XSmall)
275 .color(Color::Muted),
276 )
277 }),
278 )
279 .when(dragging.is_some(), |this| this.cursor_grabbing())
280 }
281}
282
283impl Component for Slider {
284 fn scope() -> ComponentScope {
285 ComponentScope::Input
286 }
287
288 fn description() -> Option<&'static str> {
289 Some("A slider for selecting a single value or a range via pointer drag.")
290 }
291
292 fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
293 Some(
294 v_flex()
295 .gap_6()
296 .w(px(280.))
297 .child(cx.new(|cx| Slider::new(cx).value(40.)))
298 .child(cx.new(|cx| Slider::new(cx).range(25., 75.)))
299 .into_any_element(),
300 )
301 }
302}