Skip to main content

sequoia_elements/
drag_value.rs

1use std::cell::RefCell;
2use std::rc::Rc;
3
4use sequoia_core::{Color, Edges, Length, Point, Size};
5use sequoia_input::{
6    CursorIcon, Event, EventPhase, Key, KeyEvent, Modifiers, PointerButton, PointerEvent,
7};
8use sequoia_layout::{Axis, LayoutCtx, LayoutStyle};
9use sequoia_reactive::Signal;
10use sequoia_style::{DragValueStyle, Theme, VisualState};
11use sequoia_view::{AnyView, EventCtx, FontWeight, PaintCtx, Unit, View};
12
13use crate::underline;
14
15const HEIGHT: f32 = 24.0;
16
17const KEY_FRACTION: f32 = 20.0;
18
19const DRAG_PX_FULL_RANGE: f32 = 200.0;
20
21const MAX_PRECISION: usize = 9;
22
23const CLICK_SLOP: f32 = 3.0;
24
25const CARET: char = '|';
26
27#[derive(Clone, Copy)]
28struct DragState {
29    origin: Point,
30    value: f32,
31}
32
33#[derive(Clone, Default)]
34struct Gesture {
35    drag: Option<DragState>,
36    edit: Option<String>,
37
38    fresh: bool,
39
40    announced: bool,
41}
42
43pub struct DragValue {
44    value: Signal<f32>,
45    reset: Option<f32>,
46    min: f32,
47    max: f32,
48    sensitivity: Option<f32>,
49    step: Option<f32>,
50    precision: usize,
51    orientation: Axis,
52    text_color: Option<Color>,
53    line_color: Option<Color>,
54    line_width: Option<f32>,
55    line_full: bool,
56    trim_zeros: bool,
57    font_size: Option<f32>,
58    weight: Option<FontWeight>,
59    appearance: Option<DragValueStyle>,
60    style: LayoutStyle,
61    margin: Edges,
62
63    revision: Signal<u32>,
64
65    gesture: Rc<RefCell<Gesture>>,
66
67    on_change_start: Option<Rc<dyn Fn()>>,
68
69    on_commit: Option<Rc<dyn Fn(f32)>>,
70}
71
72impl DragValue {
73    pub fn new(value: Signal<f32>) -> Self {
74        Self {
75            value,
76            reset: None,
77            min: 0.0,
78            max: 1.0,
79            sensitivity: None,
80            step: None,
81            precision: 2,
82            orientation: Axis::Horizontal,
83            text_color: None,
84            line_color: None,
85            line_width: None,
86            line_full: false,
87            trim_zeros: false,
88            font_size: None,
89            weight: None,
90            appearance: None,
91            style: LayoutStyle {
92                width: Length::Fit,
93                height: Length::Px(HEIGHT),
94                ..LayoutStyle::default()
95            },
96            margin: Edges::ZERO,
97            revision: Signal::new(0),
98            gesture: Rc::new(RefCell::new(Gesture::default())),
99            on_change_start: None,
100            on_commit: None,
101        }
102    }
103
104    pub fn reset_to(mut self, value: f32) -> Self {
105        self.reset = Some(value);
106        self
107    }
108
109    pub fn min(mut self, min: f32) -> Self {
110        if min.is_finite() {
111            self.min = min;
112        }
113        self
114    }
115
116    pub fn max(mut self, max: f32) -> Self {
117        if max.is_finite() {
118            self.max = max;
119        }
120        self
121    }
122
123    pub fn sensitivity(mut self, units_per_px: f32) -> Self {
124        self.sensitivity = Some(units_per_px.max(0.0));
125        self
126    }
127
128    pub fn step(mut self, step: f32) -> Self {
129        self.step = Some(step.max(0.0));
130        self
131    }
132
133    pub fn precision(mut self, precision: usize) -> Self {
134        self.precision = precision.min(MAX_PRECISION);
135        self
136    }
137
138    pub fn orientation(mut self, axis: Axis) -> Self {
139        self.orientation = axis;
140        self
141    }
142
143    pub fn appearance(mut self, style: DragValueStyle) -> Self {
144        self.appearance = Some(style);
145        self
146    }
147
148    pub fn color(mut self, color: Color) -> Self {
149        self.text_color = Some(color);
150        self
151    }
152
153    pub fn line_color(mut self, color: Color) -> Self {
154        self.line_color = Some(color);
155        self
156    }
157
158    pub fn line_width(mut self, width: f32) -> Self {
159        self.line_width = Some(width.max(0.0));
160        self
161    }
162
163    pub fn line_full(mut self, full: bool) -> Self {
164        self.line_full = full;
165        self
166    }
167
168    pub fn trim_zeros(mut self, trim: bool) -> Self {
169        self.trim_zeros = trim;
170        self
171    }
172
173    pub fn font_size(mut self, size: f32) -> Self {
174        self.font_size = Some(size);
175        self
176    }
177
178    pub fn weight(mut self, weight: FontWeight) -> Self {
179        self.weight = Some(weight);
180        self
181    }
182
183    pub fn bold(mut self, bold: bool) -> Self {
184        self.weight = Some(if bold {
185            FontWeight::BOLD
186        } else {
187            FontWeight::NORMAL
188        });
189        self
190    }
191
192    pub fn width(mut self, width: impl Into<Length>) -> Self {
193        self.style = self.style.width(width.into());
194        self
195    }
196
197    pub fn height(mut self, height: impl Into<Length>) -> Self {
198        self.style = self.style.height(height.into());
199        self
200    }
201
202    pub fn min_width(mut self, v: f32) -> Self {
203        self.style = self.style.min_width(v);
204        self
205    }
206
207    pub fn max_width(mut self, v: f32) -> Self {
208        self.style = self.style.max_width(v);
209        self
210    }
211
212    pub fn min_height(mut self, v: f32) -> Self {
213        self.style = self.style.min_height(v);
214        self
215    }
216
217    pub fn max_height(mut self, v: f32) -> Self {
218        self.style = self.style.max_height(v);
219        self
220    }
221
222    pub fn flex(mut self, weight: f32) -> Self {
223        self.style = self.style.flex(weight);
224        self
225    }
226
227    pub fn expand(self) -> Self {
228        self.flex(1.0)
229    }
230
231    pub fn margin(mut self, margin: impl Into<Edges>) -> Self {
232        let m = margin.into();
233        self.margin = Edges::only(
234            sanitize_margin(m.left),
235            sanitize_margin(m.top),
236            sanitize_margin(m.right),
237            sanitize_margin(m.bottom),
238        );
239        self
240    }
241
242    pub fn margin_xy(self, x: f32, y: f32) -> Self {
243        self.margin(Edges::xy(x, y))
244    }
245
246    pub fn margin_x(mut self, x: f32) -> Self {
247        let x = sanitize_margin(x);
248        self.margin.left = x;
249        self.margin.right = x;
250        self
251    }
252
253    pub fn margin_y(mut self, y: f32) -> Self {
254        let y = sanitize_margin(y);
255        self.margin.top = y;
256        self.margin.bottom = y;
257        self
258    }
259
260    pub fn margin_left(mut self, v: f32) -> Self {
261        self.margin.left = sanitize_margin(v);
262        self
263    }
264
265    pub fn margin_right(mut self, v: f32) -> Self {
266        self.margin.right = sanitize_margin(v);
267        self
268    }
269
270    pub fn margin_top(mut self, v: f32) -> Self {
271        self.margin.top = sanitize_margin(v);
272        self
273    }
274
275    pub fn margin_bottom(mut self, v: f32) -> Self {
276        self.margin.bottom = sanitize_margin(v);
277        self
278    }
279
280    pub fn on_change_start(mut self, f: impl Fn() + 'static) -> Self {
281        self.on_change_start = Some(Rc::new(f));
282        self
283    }
284
285    pub fn on_commit(mut self, f: impl Fn(f32) + 'static) -> Self {
286        self.on_commit = Some(Rc::new(f));
287        self
288    }
289
290    fn announce(&self) {
291        let need = self.with_gesture(|g| {
292            if g.announced {
293                false
294            } else {
295                g.announced = true;
296                true
297            }
298        });
299        if need && let Some(f) = &self.on_change_start {
300            f();
301        }
302    }
303
304    fn commit_if_changed(&self, before: f32) {
305        let after = self.value.get_untracked();
306        if after != before
307            && let Some(f) = &self.on_commit
308        {
309            f(after);
310        }
311    }
312
313    fn with_gesture<R>(&self, f: impl FnOnce(&mut Gesture) -> R) -> R {
314        f(&mut self.gesture.borrow_mut())
315    }
316
317    fn anchor(&self) -> Option<DragState> {
318        self.with_gesture(|g| g.drag)
319    }
320
321    fn is_editing(&self) -> bool {
322        self.with_gesture(|g| g.edit.is_some())
323    }
324
325    fn edit_buffer(&self) -> Option<String> {
326        self.with_gesture(|g| g.edit.clone())
327    }
328
329    fn start_edit(&self, buffer: String, fresh: bool) {
330        self.with_gesture(|g| {
331            g.edit = Some(buffer);
332            g.fresh = fresh;
333        });
334        self.bump();
335    }
336
337    fn stop_edit(&self) {
338        self.with_gesture(|g| {
339            g.edit = None;
340            g.fresh = false;
341        });
342        self.bump();
343    }
344
345    fn push_char(&self, c: char) {
346        self.with_gesture(|g| {
347            if g.edit.is_none() {
348                return;
349            }
350            if g.fresh {
351                g.edit = Some(String::new());
352                g.fresh = false;
353            }
354            if let Some(buffer) = g.edit.as_mut() {
355                buffer.push(c);
356            }
357        });
358        self.bump();
359    }
360
361    fn pop_char(&self) {
362        self.with_gesture(|g| {
363            g.fresh = false;
364            if let Some(buffer) = g.edit.as_mut() {
365                buffer.pop();
366            }
367        });
368        self.bump();
369    }
370
371    fn commit_edit(&self) {
372        let Some(buffer) = self.edit_buffer() else {
373            return;
374        };
375        let before = self.value.get_untracked();
376        self.stop_edit();
377        if let Ok(parsed) = buffer.trim().replace(',', ".").parse::<f32>() {
378            self.set_value(parsed);
379        }
380        self.commit_if_changed(before);
381    }
382
383    fn bump(&self) {
384        self.revision
385            .set(self.revision.get_untracked().wrapping_add(1));
386    }
387
388    fn span(&self) -> f32 {
389        (self.max - self.min).abs().max(f32::EPSILON)
390    }
391
392    fn units_per_px(&self) -> f32 {
393        self.sensitivity.unwrap_or(self.span() / DRAG_PX_FULL_RANGE)
394    }
395
396    fn key_step(&self) -> f32 {
397        self.step.unwrap_or(self.span() / KEY_FRACTION)
398    }
399
400    fn clamped(&self, v: f32) -> f32 {
401        let lo = self.min.min(self.max);
402        let hi = self.min.max(self.max);
403        let scale = 10f32.powi(self.precision as i32);
404        let q = ((v.clamp(lo, hi) * scale).round() / scale).clamp(lo, hi);
405        if q == 0.0 { 0.0 } else { q }
406    }
407
408    fn set_value(&self, v: f32) {
409        let next = self.clamped(v);
410
411        if !next.is_finite() {
412            return;
413        }
414        if next != self.value.get_untracked() {
415            self.announce();
416            self.value.set(next);
417        }
418    }
419
420    fn text(&self, value: f32) -> String {
421        let s = format!("{value:.prec$}", prec = self.precision);
422        if self.trim_zeros && self.precision > 0 && (s.contains('.') || s.contains(',')) {
423            let t = s.trim_end_matches('0');
424            t.strip_suffix('.')
425                .or_else(|| t.strip_suffix(','))
426                .unwrap_or(t)
427                .to_owned()
428        } else {
429            s
430        }
431    }
432
433    fn display(&self, value: f32) -> String {
434        match self.edit_buffer() {
435            Some(buffer) => format!("{buffer}{CARET}"),
436            None => self.text(value),
437        }
438    }
439
440    fn font_weight(&self) -> FontWeight {
441        self.weight
442            .or_else(|| {
443                self.appearance
444                    .and_then(|s| s.font_weight)
445                    .map(FontWeight::new)
446            })
447            .unwrap_or_default()
448    }
449
450    fn resolved_font_size(&self, theme: &Theme) -> f32 {
451        self.font_size
452            .or_else(|| self.appearance.and_then(|s| s.font_size))
453            .unwrap_or(theme.font_size)
454    }
455
456    fn resolved_line_width(&self) -> f32 {
457        if let Some(w) = self
458            .line_width
459            .or_else(|| self.appearance.and_then(|s| s.line_width))
460        {
461            return w;
462        }
463        let has_line_color =
464            self.line_color.is_some() || self.appearance.and_then(|s| s.line).is_some();
465        if has_line_color {
466            return underline::LINE_W;
467        }
468        0.0
469    }
470
471    fn resolved_text_color(&self) -> Option<Color> {
472        self.text_color
473            .or_else(|| self.appearance.and_then(|s| s.text))
474    }
475
476    fn resolved_line_color(&self) -> Option<Color> {
477        self.line_color
478            .or_else(|| self.appearance.and_then(|s| s.line))
479    }
480}
481
482fn is_value_char(c: char) -> bool {
483    c.is_ascii_digit() || c == '.' || c == ',' || c == '-'
484}
485
486fn normalize_char(c: char) -> char {
487    if c == ',' { '.' } else { c }
488}
489
490impl View for DragValue {
491    fn body(&self) -> impl View + 'static {
492        Unit
493    }
494
495    fn is_leaf(&self) -> bool {
496        true
497    }
498
499    fn focusable(&self) -> bool {
500        true
501    }
502
503    fn captures_pointer(&self) -> bool {
504        true
505    }
506
507    fn has_visual_state(&self) -> bool {
508        true
509    }
510
511    fn keyed_children(&self) -> Option<Vec<(u64, AnyView)>> {
512        let _ = self.value.get();
513        let _ = self.revision.get();
514        Some(Vec::new())
515    }
516
517    fn cursor_icon(&self, state: VisualState) -> CursorIcon {
518        if state.pressed {
519            match self.orientation {
520                Axis::Horizontal => CursorIcon::ColResize,
521                Axis::Vertical => CursorIcon::RowResize,
522            }
523        } else {
524            CursorIcon::Grab
525        }
526    }
527
528    fn layout_style(&self) -> LayoutStyle {
529        let mut s = self.style;
530        s.margin = self.margin;
531        s
532    }
533
534    fn intrinsic_size(&self, ctx: &mut LayoutCtx) -> Option<Size> {
535        let appearance = self.appearance.unwrap_or_default();
536        let size = self.font_size.or(appearance.font_size).unwrap_or(14.0);
537        let text = self.display(self.clamped(self.value.get_untracked()));
538        let w = ctx.text_width_weighted(&text, size, self.font_weight());
539        let line_width = self.resolved_line_width();
540        let h = if line_width > 0.0 {
541            underline::content_height(size, line_width)
542        } else {
543            LayoutCtx::text_line_height(size)
544        };
545        Some(Size::new(w + underline::PAD_X * 2.0, h))
546    }
547
548    fn reuse_state(&self, old: &Self) {
549        if !Rc::ptr_eq(&self.gesture, &old.gesture) {
550            *self.gesture.borrow_mut() = old.gesture.borrow().clone();
551        }
552        self.revision.set(old.revision.get_untracked());
553    }
554
555    fn on_event(&self, event: &Event, ctx: &mut EventCtx) {
556        if ctx.phase != EventPhase::Target {
557            return;
558        }
559        match event {
560            Event::Pointer(PointerEvent::Down {
561                button: PointerButton::Left,
562                pos,
563                modifiers,
564                ..
565            }) => {
566                if modifiers.contains(Modifiers::CONTROL) {
567                    if let Some(reset) = self.reset {
568                        let before = self.value.get_untracked();
569                        self.stop_edit();
570                        self.with_gesture(|g| g.drag = None);
571                        self.set_value(reset);
572                        self.commit_if_changed(before);
573                        ctx.handled();
574                    }
575                    return;
576                }
577
578                if self.is_editing() {
579                    self.commit_edit();
580                }
581                self.with_gesture(|g| {
582                    g.drag = Some(DragState {
583                        origin: *pos,
584                        value: self.value.get_untracked(),
585                    });
586                    g.announced = false;
587                });
588                ctx.capture_pointer();
589                ctx.handled();
590            }
591            Event::Pointer(PointerEvent::Move { pos, .. }) => {
592                let Some(drag) = self.anchor() else { return };
593
594                if !ctx.state.pressed {
595                    self.with_gesture(|g| g.drag = None);
596                    self.commit_if_changed(drag.value);
597                    return;
598                }
599                let delta = match self.orientation {
600                    Axis::Horizontal => pos.x - drag.origin.x,
601
602                    Axis::Vertical => drag.origin.y - pos.y,
603                };
604                self.set_value(drag.value + delta * self.units_per_px());
605                ctx.handled();
606            }
607            Event::Pointer(PointerEvent::Up { pos, .. }) => {
608                let Some(drag) = self.anchor() else { return };
609                self.with_gesture(|g| g.drag = None);
610
611                let moved = (pos.x - drag.origin.x)
612                    .abs()
613                    .max((pos.y - drag.origin.y).abs());
614                if moved <= CLICK_SLOP {
615                    self.start_edit(self.text(self.value.get_untracked()), true);
616                } else {
617                    self.bump();
618                    self.commit_if_changed(drag.value);
619                }
620                ctx.handled();
621            }
622            Event::Key(key) => {
623                if self.is_editing() {
624                    self.on_edit_key(key);
625                    ctx.handled();
626                    return;
627                }
628
629                if let Some(c) = key.key.char().filter(|c| is_value_char(*c)) {
630                    self.start_edit(normalize_char(c).to_string(), false);
631                    ctx.request_focus();
632                    ctx.handled();
633                    return;
634                }
635                let delta = match key.key {
636                    Key::ARROW_LEFT | Key::ARROW_DOWN => -self.key_step(),
637                    Key::ARROW_RIGHT | Key::ARROW_UP => self.key_step(),
638                    _ => return,
639                };
640                let before = self.value.get_untracked();
641                self.set_value(self.value.get_untracked() + delta);
642                self.commit_if_changed(before);
643                ctx.handled();
644            }
645
646            Event::Pointer(PointerEvent::Wheel {
647                delta, modifiers, ..
648            }) => {
649                if self.is_editing() {
650                    return;
651                }
652                let mult = if modifiers.contains(Modifiers::SHIFT) {
653                    0.1
654                } else if modifiers.contains(Modifiers::CONTROL)
655                    || modifiers.contains(Modifiers::SUPER)
656                {
657                    10.0
658                } else {
659                    1.0
660                };
661                let dir = if delta.y < 0.0 { 1.0 } else { -1.0 };
662                let before = self.value.get_untracked();
663                self.set_value(before + self.span() / 100.0 * mult * dir);
664                self.commit_if_changed(before);
665                ctx.handled();
666            }
667            _ => {}
668        }
669    }
670
671    fn paint_self(&self, ctx: &mut PaintCtx) {
672        if !ctx.state.focused && self.is_editing() {
673            self.commit_edit();
674        }
675
676        let rect = ctx.rect;
677        if rect.size.width <= 0.0 || rect.size.height <= 0.0 {
678            return;
679        }
680        let theme = ctx.theme();
681        let size = self.resolved_font_size(&theme);
682        let weight = self.font_weight();
683        let line_width = self.resolved_line_width();
684        let editing = self.is_editing();
685        let colors = underline::field_colors(
686            &theme,
687            ctx.state.hovered || ctx.state.pressed || editing,
688            ctx.state.pressed || editing,
689            self.resolved_text_color(),
690            self.resolved_line_color(),
691        );
692        let text = self.display(self.clamped(self.value.get_untracked()));
693        let text_width = ctx.text_width_weighted(&text, size, weight);
694
695        if line_width > 0.0 && colors.line.a > 0.0 {
696            let w = if self.line_full {
697                (rect.size.width - underline::PAD_X * 2.0).max(0.0)
698            } else {
699                text_width
700            };
701            let line = underline::line_rect(rect, size, w, line_width);
702            ctx.quad(line, colors.line, line_width * 0.5);
703        }
704
705        let x = rect.left() + underline::PAD_X;
706        let y = underline::baseline(rect, size);
707        ctx.draw_text_weighted(&text, (x, y), size, weight, colors.text);
708    }
709}
710
711impl DragValue {
712    fn on_edit_key(&self, key: &KeyEvent) {
713        match key.key {
714            Key::ENTER => self.commit_edit(),
715            Key::ESCAPE => self.stop_edit(),
716            Key::BACKSPACE => self.pop_char(),
717            other => {
718                if let Some(c) = other.char().filter(|c| is_value_char(*c)) {
719                    self.push_char(normalize_char(c));
720                }
721            }
722        }
723    }
724}
725
726fn sanitize_margin(v: f32) -> f32 {
727    if v.is_finite() { v.max(0.0) } else { 0.0 }
728}