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}