1use std::cell::{Cell, RefCell};
2use std::rc::Rc;
3use std::time::Instant;
4
5use sequoia_core::{Color, Edges, Length, Rect, Size};
6use sequoia_input::{CursorIcon, Event, EventPhase, Key, PointerButton, PointerEvent};
7use sequoia_layout::{LayoutCtx, LayoutStyle};
8use sequoia_reactive::Signal;
9use sequoia_style::{SliderStyle, VisualState};
10use sequoia_view::{EventCtx, PaintCtx, Unit, View};
11
12pub const TRACK_COLOR: Color = Color::rgb8(0x3E, 0x3E, 0x3E);
13
14pub const FILL_COLOR: Color = Color::rgb8(0xC9, 0xC9, 0xC9);
15
16pub const THUMB_OUTLINE: Color = Color::rgb8(0xC9, 0xC9, 0xC9);
17
18pub const THUMB_CENTER: Color = Color::rgb8(0x12, 0x12, 0x12);
19
20const THUMB: f32 = 14.0;
21
22const THUMB_BORDER: f32 = 2.0;
23
24const THUMB_GROW: f32 = 1.5;
25
26const ANIM_SECS: f32 = 0.15;
27const ANIM_MIN_T: f32 = 0.08;
28
29const TRACK_H: f32 = 4.0;
30
31const HEIGHT: f32 = 20.0;
32
33const KEY_STEP: f32 = 0.05;
34
35#[derive(Clone, Copy)]
36struct Anim {
37 from: f32,
38 to: f32,
39 start: Option<Instant>,
40}
41
42thread_local! {
43 static TEST_NOW: RefCell<Option<Instant>> = const { RefCell::new(None) };
44}
45
46fn now() -> Instant {
47 let frozen: Option<Instant> = TEST_NOW.with(|c| *c.borrow());
48 frozen.unwrap_or_else(Instant::now)
49}
50
51#[doc(hidden)]
52pub fn __set_test_now(t: Option<Instant>) {
53 TEST_NOW.with(|c| *c.borrow_mut() = t);
54}
55
56pub struct Slider {
57 value: Signal<f32>,
58 min: f32,
59 max: f32,
60 track_height: Option<f32>,
61 track: Option<Color>,
62 fill: Option<Color>,
63 thumb_center: Option<Color>,
64 thumb_outline: Option<Color>,
65 thumb_size: Option<f32>,
66 appearance: Option<SliderStyle>,
67 style: LayoutStyle,
68 grow: Signal<f32>,
69 anim: Rc<Cell<Anim>>,
70 margin: Edges,
71
72 gesture_start: Rc<Cell<Option<f32>>>,
73
74 on_change_start: Option<Rc<dyn Fn()>>,
75
76 on_commit: Option<Rc<dyn Fn(f32)>>,
77}
78
79impl Slider {
80 pub fn new(value: Signal<f32>) -> Self {
81 Self {
82 value,
83 min: 0.0,
84 max: 1.0,
85 track_height: None,
86 track: None,
87 fill: None,
88 thumb_center: None,
89 thumb_outline: None,
90 thumb_size: None,
91 appearance: None,
92 style: LayoutStyle {
93 width: Length::Fill(1.0),
94 height: Length::Px(HEIGHT),
95 ..LayoutStyle::default()
96 },
97 grow: Signal::new(0.0),
98 anim: Rc::new(Cell::new(Anim {
99 from: 0.0,
100 to: 0.0,
101 start: None,
102 })),
103 gesture_start: Rc::new(Cell::new(None)),
104 on_change_start: None,
105 on_commit: None,
106 margin: Edges::ZERO,
107 }
108 }
109
110 pub fn min(mut self, min: f32) -> Self {
111 if min.is_finite() {
112 self.min = min;
113 }
114 self
115 }
116
117 pub fn max(mut self, max: f32) -> Self {
118 if max.is_finite() {
119 self.max = max;
120 }
121 self
122 }
123
124 pub fn appearance(mut self, style: SliderStyle) -> Self {
125 self.appearance = Some(style);
126 self
127 }
128
129 pub fn track_color(mut self, color: Color) -> Self {
130 self.track = Some(color);
131 self
132 }
133
134 pub fn fill_color(mut self, color: Color) -> Self {
135 self.fill = Some(color);
136 self
137 }
138
139 pub fn thumb_center(mut self, color: Color) -> Self {
140 self.thumb_center = Some(color);
141 self
142 }
143
144 pub fn thumb_outline(mut self, color: Color) -> Self {
145 self.thumb_outline = Some(color);
146 self
147 }
148
149 pub fn thumb_size(mut self, size: f32) -> Self {
150 self.thumb_size = Some(size.max(1.0));
151 self
152 }
153
154 pub fn track_height(mut self, height: f32) -> Self {
155 self.track_height = Some(height.max(1.0));
156 self
157 }
158
159 fn resolved_track_height(&self) -> f32 {
160 self.track_height
161 .or_else(|| self.appearance.and_then(|s| s.track_height))
162 .unwrap_or(TRACK_H)
163 }
164
165 fn resolved_thumb_size(&self) -> f32 {
166 self.thumb_size
167 .or_else(|| self.appearance.and_then(|s| s.thumb_size))
168 .unwrap_or(THUMB)
169 }
170
171 pub fn height(mut self, height: impl Into<Length>) -> Self {
172 self.style = self.style.height(height.into());
173 self
174 }
175
176 pub fn width(mut self, width: impl Into<Length>) -> Self {
177 self.style = self.style.width(width.into());
178 self
179 }
180
181 pub fn min_width(mut self, v: f32) -> Self {
182 self.style = self.style.min_width(v);
183 self
184 }
185
186 pub fn max_width(mut self, v: f32) -> Self {
187 self.style = self.style.max_width(v);
188 self
189 }
190
191 pub fn min_height(mut self, v: f32) -> Self {
192 self.style = self.style.min_height(v);
193 self
194 }
195
196 pub fn max_height(mut self, v: f32) -> Self {
197 self.style = self.style.max_height(v);
198 self
199 }
200
201 pub fn flex(mut self, weight: f32) -> Self {
202 self.style = self.style.flex(weight);
203 self
204 }
205
206 pub fn expand(self) -> Self {
207 self.flex(1.0)
208 }
209
210 pub fn margin(mut self, margin: impl Into<Edges>) -> Self {
211 let m = margin.into();
212 self.margin = Edges::only(
213 sanitize_margin(m.left),
214 sanitize_margin(m.top),
215 sanitize_margin(m.right),
216 sanitize_margin(m.bottom),
217 );
218 self
219 }
220
221 pub fn margin_xy(self, x: f32, y: f32) -> Self {
222 self.margin(Edges::xy(x, y))
223 }
224
225 pub fn margin_x(mut self, x: f32) -> Self {
226 let x = sanitize_margin(x);
227 self.margin.left = x;
228 self.margin.right = x;
229 self
230 }
231
232 pub fn margin_y(mut self, y: f32) -> Self {
233 let y = sanitize_margin(y);
234 self.margin.top = y;
235 self.margin.bottom = y;
236 self
237 }
238
239 pub fn margin_left(mut self, v: f32) -> Self {
240 self.margin.left = sanitize_margin(v);
241 self
242 }
243
244 pub fn margin_right(mut self, v: f32) -> Self {
245 self.margin.right = sanitize_margin(v);
246 self
247 }
248
249 pub fn margin_top(mut self, v: f32) -> Self {
250 self.margin.top = sanitize_margin(v);
251 self
252 }
253
254 pub fn margin_bottom(mut self, v: f32) -> Self {
255 self.margin.bottom = sanitize_margin(v);
256 self
257 }
258
259 pub fn on_change_start(mut self, f: impl Fn() + 'static) -> Self {
260 self.on_change_start = Some(Rc::new(f));
261 self
262 }
263
264 pub fn on_commit(mut self, f: impl Fn(f32) + 'static) -> Self {
265 self.on_commit = Some(Rc::new(f));
266 self
267 }
268
269 fn fire_start(&self) {
270 self.gesture_start.set(Some(self.value.get_untracked()));
271 if let Some(f) = &self.on_change_start {
272 f();
273 }
274 }
275
276 fn fire_commit(&self) {
277 if let Some(start) = self.gesture_start.take() {
278 let v = self.value.get_untracked();
279 if v != start
280 && let Some(f) = &self.on_commit
281 {
282 f(v);
283 }
284 }
285 }
286
287 fn span(&self) -> f32 {
288 (self.max - self.min).max(f32::EPSILON)
289 }
290
291 fn ratio(&self) -> f32 {
292 ((self.value.get_untracked() - self.min) / self.span()).clamp(0.0, 1.0)
293 }
294
295 fn set_ratio(&self, t: f32) {
296 let v = self.min + t.clamp(0.0, 1.0) * self.span();
297
298 if v.is_finite() {
299 self.value.set(v);
300 }
301 }
302
303 fn thumb_x(&self, rect: Rect) -> f32 {
304 let thumb = self.resolved_thumb_size();
305 rect.left() + thumb * 0.5 + self.ratio() * (rect.size.width - thumb).max(0.0)
306 }
307}
308
309impl View for Slider {
310 fn body(&self) -> impl View + 'static {
311 let _ = self.value.get();
312 let _ = self.grow.get();
313 Unit
314 }
315
316 fn is_leaf(&self) -> bool {
317 true
318 }
319
320 fn focusable(&self) -> bool {
321 true
322 }
323
324 fn captures_pointer(&self) -> bool {
325 true
326 }
327
328 fn has_visual_state(&self) -> bool {
329 true
330 }
331
332 fn cursor_icon(&self, state: VisualState) -> CursorIcon {
333 if state.pressed {
334 CursorIcon::Grabbing
335 } else {
336 CursorIcon::Grab
337 }
338 }
339
340 fn layout_style(&self) -> LayoutStyle {
341 let mut style = self.style;
342 style.margin = self.margin;
343 style
344 }
345
346 fn intrinsic_size(&self, _ctx: &mut LayoutCtx) -> Option<Size> {
347 None
348 }
349
350 fn reuse_state(&self, old: &Self) {
351 self.gesture_start.set(old.gesture_start.get());
352 self.grow.set(old.grow.get_untracked());
353 self.anim.set(old.anim.get());
354 }
355
356 fn on_event(&self, event: &Event, ctx: &mut EventCtx) {
357 if ctx.phase != EventPhase::Target {
358 return;
359 }
360 let thumb = self.resolved_thumb_size();
361 match event {
362 Event::Pointer(PointerEvent::Down {
363 button: PointerButton::Left,
364 pos,
365 ..
366 }) => {
367 self.fire_start();
368 let t = (pos.x - ctx.rect.left() - thumb * 0.5)
369 / (ctx.rect.size.width - thumb).max(1.0);
370 self.set_ratio(t);
371 ctx.capture_pointer();
372 ctx.handled();
373 }
374 Event::Pointer(PointerEvent::Move { pos, .. }) => {
375 if ctx.state.pressed {
376 let t = (pos.x - ctx.rect.left() - thumb * 0.5)
377 / (ctx.rect.size.width - thumb).max(1.0);
378 self.set_ratio(t);
379 ctx.handled();
380 }
381 }
382 Event::Pointer(PointerEvent::Up { .. }) => {
383 self.fire_commit();
384 }
385 Event::Key(key) => {
386 let step = match key.key {
387 Key::ARROW_LEFT | Key::ARROW_DOWN => -KEY_STEP,
388 Key::ARROW_RIGHT | Key::ARROW_UP => KEY_STEP,
389 _ => return,
390 };
391 self.fire_start();
392 self.set_ratio(self.ratio() + step);
393 self.fire_commit();
394 ctx.handled();
395 }
396 _ => {}
397 }
398 }
399
400 fn paint_self(&self, ctx: &mut PaintCtx) {
401 let rect = ctx.rect;
402 let cy = rect.top() + rect.size.height * 0.5;
403 let style = self.appearance.unwrap_or_default();
404 let track = self.track.or(style.track).unwrap_or(TRACK_COLOR);
405 let fill = self.fill.or(style.fill).unwrap_or(FILL_COLOR);
406 let thumb_outline = self
407 .thumb_outline
408 .or(style.thumb_outline)
409 .unwrap_or(THUMB_OUTLINE);
410 let thumb_center = self
411 .thumb_center
412 .or(style.thumb_center)
413 .unwrap_or(THUMB_CENTER);
414 let thumb = self.resolved_thumb_size();
415 let thumb_radius = thumb * 0.5;
416 let th = self.resolved_track_height().min(rect.size.height).max(0.0);
417 if th <= 0.0 || rect.size.width <= 0.0 {
418 return;
419 }
420 let tx = self.thumb_x(rect);
421
422 ctx.quad(
423 Rect::from_xywh(rect.left(), cy - th * 0.5, rect.size.width, th),
424 track,
425 th * 0.5,
426 );
427 let fill_w = (tx - rect.left()).max(0.0);
428 if fill_w > 0.0 {
429 ctx.quad(
430 Rect::from_xywh(rect.left(), cy - th * 0.5, fill_w, th),
431 fill,
432 th * 0.5,
433 );
434 }
435
436 let target = if ctx.state.hovered { 1.0 } else { 0.0 };
437 let mut st = self.anim.get();
438 if (st.to - target).abs() > f32::EPSILON {
439 st = Anim {
440 from: self.grow.get_untracked(),
441 to: target,
442 start: Some(now()),
443 };
444 self.anim.set(st);
445 }
446 if let Some(t0) = st.start {
447 let t = (now().saturating_duration_since(t0).as_secs_f32() / ANIM_SECS)
448 .clamp(ANIM_MIN_T, 1.0);
449 let eased = 1.0 - (1.0 - t).powi(3);
450 let v = st.from + (st.to - st.from) * eased;
451 if t >= 1.0 {
452 self.anim.set(Anim {
453 from: st.to,
454 to: st.to,
455 start: None,
456 });
457 }
458 self.grow.set(v);
459 }
460 let bw = THUMB_BORDER + self.grow.get_untracked() * THUMB_GROW;
461
462 ctx.quad(
463 Rect::from_xywh(tx - thumb_radius, cy - thumb_radius, thumb, thumb),
464 thumb_outline,
465 thumb_radius,
466 );
467 let inner = (thumb - 2.0 * bw).max(0.0);
468 if inner > 0.0 {
469 ctx.quad(
470 Rect::from_xywh(tx - inner * 0.5, cy - inner * 0.5, inner, inner),
471 thumb_center,
472 (thumb_radius - bw).max(0.0),
473 );
474 }
475 }
476}
477
478fn sanitize_margin(v: f32) -> f32 {
479 if v.is_finite() { v.max(0.0) } else { 0.0 }
480}