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