Skip to main content

sequoia_elements/
slider.rs

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}