1use std::cell::Cell;
42use std::rc::Rc;
43
44use gpui::prelude::*;
45use gpui::*;
46
47use crate::theme::{get_theme_or, Theme};
48use crate::utils::format_display_value;
49use super::focus_navigation::{handle_tab_navigation, with_focus_actions, EnabledCursorExt};
50use super::text_input::{TextInput, TextInputEvent};
51
52#[derive(Clone, Debug)]
54pub enum NumberStepperEvent {
55 Change(f64),
57}
58
59#[doc(hidden)]
61#[derive(Clone)]
62struct NumberDragState;
63
64#[doc(hidden)]
66struct EmptyDragView;
67
68impl Render for EmptyDragView {
69 fn render(&mut self, _window: &mut Window, _cx: &mut Context<'_, Self>) -> impl IntoElement {
70 div().size_0()
71 }
72}
73
74pub struct NumberStepper {
76 value: f64,
77 min: Option<f64>,
78 max: Option<f64>,
79 step: Option<f64>,
80 resolution: Option<f64>,
82 display_precision: Option<usize>,
84 focus_handle: FocusHandle,
85 custom_theme: Option<Theme>,
86 enabled: bool,
88
89 value_per_pixel_normal: f64,
92 value_per_pixel_fast: f64,
94 value_per_pixel_slow: f64,
96 auto_scale_drag: bool,
98 value_display_width: Rc<Cell<f32>>,
100
101 editing: bool,
104 edit_input: Entity<TextInput>,
106 original_value: f64,
108 pending_refocus: bool,
110
111 dragging: bool,
114 drag_start_x: f32,
116 drag_start_value: f64,
118
119 step_small_multiplier: f64,
122 step_large_multiplier: f64,
124}
125
126impl EventEmitter<NumberStepperEvent> for NumberStepper {}
127
128impl Focusable for NumberStepper {
129 fn focus_handle(&self, _cx: &App) -> FocusHandle {
130 self.focus_handle.clone()
131 }
132}
133
134impl NumberStepper {
135 pub fn new(cx: &mut Context<Self>) -> Self {
137 let edit_input = cx.new(|cx| {
139 TextInput::new(cx)
140 .borderless(true)
141 .select_on_focus(true)
142 .input_filter(|c| c.is_ascii_digit() || c == '.' || c == '-')
143 .emit_tab_events(true) });
145
146 cx.subscribe(&edit_input, |this: &mut Self, _input, event: &TextInputEvent, cx| {
148 match event {
149 TextInputEvent::Enter => this.commit_edit(cx),
150 TextInputEvent::Escape => this.cancel_edit(cx),
151 TextInputEvent::Blur => {
152 if this.editing {
153 this.commit_edit(cx);
154 }
155 }
156 TextInputEvent::Tab | TextInputEvent::ShiftTab => {
157 this.commit_edit(cx);
159 }
160 _ => {}
161 }
162 }).detach();
163
164 Self {
165 value: 0.0,
166 min: None,
167 max: None,
168 step: None,
169 resolution: None,
170 display_precision: None,
171 focus_handle: cx.focus_handle().tab_stop(true),
172 custom_theme: None,
173 enabled: true,
174 value_per_pixel_normal: 0.5,
175 value_per_pixel_fast: 2.5, value_per_pixel_slow: 0.05, auto_scale_drag: true,
178 value_display_width: Rc::new(Cell::new(0.0)),
179 editing: false,
180 edit_input,
181 original_value: 0.0,
182 pending_refocus: false,
183 dragging: false,
184 drag_start_x: 0.0,
185 drag_start_value: 0.0,
186 step_small_multiplier: 0.1, step_large_multiplier: 10.0, }
189 }
190
191 #[must_use]
193 pub fn with_value(mut self, value: f64) -> Self {
194 self.value = value;
195 self
196 }
197
198 #[must_use]
200 pub fn min(mut self, min: f64) -> Self {
201 self.min = Some(min);
202 self
203 }
204
205 #[must_use]
207 pub fn max(mut self, max: f64) -> Self {
208 self.max = Some(max);
209 self
210 }
211
212 #[must_use]
214 pub fn step(mut self, step: f64) -> Self {
215 self.step = Some(step);
216 self
217 }
218
219 #[must_use]
224 pub fn resolution(mut self, resolution: f64) -> Self {
225 self.resolution = Some(resolution);
226 self
227 }
228
229 #[must_use]
234 pub fn display_precision(mut self, precision: usize) -> Self {
235 self.display_precision = Some(precision);
236 self
237 }
238
239 #[must_use]
241 pub fn theme(mut self, theme: Theme) -> Self {
242 self.custom_theme = Some(theme);
243 self
244 }
245
246 #[must_use]
248 pub fn with_enabled(mut self, enabled: bool) -> Self {
249 self.enabled = enabled;
250 self
251 }
252
253 #[must_use]
270 pub fn drag_sensitivities(mut self, normal: f64, fast: f64, slow: f64) -> Self {
271 self.value_per_pixel_normal = normal;
272 self.value_per_pixel_fast = fast;
273 self.value_per_pixel_slow = slow;
274 self
275 }
276
277 #[must_use]
279 pub fn drag_sensitivity(mut self, value_per_pixel: f64) -> Self {
280 self.value_per_pixel_normal = value_per_pixel;
281 self.value_per_pixel_fast = value_per_pixel * 5.0;
282 self.value_per_pixel_slow = value_per_pixel * 0.1;
283 self
284 }
285
286 #[must_use]
292 pub fn manual_drag_sensitivity(mut self) -> Self {
293 self.auto_scale_drag = false;
294 self
295 }
296
297 #[must_use]
307 pub fn step_multipliers(mut self, small: f64, large: f64) -> Self {
308 self.step_small_multiplier = small;
309 self.step_large_multiplier = large;
310 self
311 }
312
313 #[must_use]
315 pub fn step_small(mut self, multiplier: f64) -> Self {
316 self.step_small_multiplier = multiplier;
317 self
318 }
319
320 #[must_use]
322 pub fn step_large(mut self, multiplier: f64) -> Self {
323 self.step_large_multiplier = multiplier;
324 self
325 }
326
327 pub fn focus_handle(&self) -> &FocusHandle {
329 &self.focus_handle
330 }
331
332 pub fn is_enabled(&self) -> bool {
334 self.enabled
335 }
336
337 pub fn set_enabled(&mut self, enabled: bool, cx: &mut Context<Self>) {
339 if self.enabled != enabled {
340 self.enabled = enabled;
341 cx.notify();
342 }
343 }
344
345 pub fn value(&self) -> f64 {
347 self.value
348 }
349
350 pub fn get_min(&self) -> Option<f64> {
352 self.min
353 }
354
355 pub fn get_max(&self) -> Option<f64> {
357 self.max
358 }
359
360 pub fn get_step(&self) -> Option<f64> {
362 self.step
363 }
364
365 pub fn get_resolution(&self) -> Option<f64> {
367 self.resolution
368 }
369
370 pub fn get_display_precision(&self) -> Option<usize> {
372 self.display_precision
373 }
374
375 pub fn set_value(&mut self, value: f64, cx: &mut Context<Self>) {
377 let normalized = self.normalize_value(value);
378 if (self.value - normalized).abs() > f64::EPSILON {
379 self.value = normalized;
380 cx.emit(NumberStepperEvent::Change(self.value));
381 cx.notify();
382 }
383 }
384
385 fn format_value(&self) -> String {
387 format_display_value(self.value, self.display_precision)
388 }
389
390 fn normalize_value(&self, value: f64) -> f64 {
392 let min = self.min.unwrap_or(f64::NEG_INFINITY);
393 let max = self.max.unwrap_or(f64::INFINITY);
394
395 let snapped = if let Some(resolution) = self.resolution {
397 if resolution > 0.0 {
398 let offset = value - min;
400 let n = (offset / resolution).round();
401 min + n * resolution
402 } else {
403 value
404 }
405 } else {
406 value
407 };
408
409 snapped.clamp(min, max)
411 }
412
413 fn adjust_value(&mut self, direction: f64, multiplier: f64, cx: &mut Context<Self>) {
414 let step = self.step.unwrap_or(1.0) * multiplier * direction;
415 let new_value = self.normalize_value(self.value + step);
416 if (self.value - new_value).abs() > f64::EPSILON {
417 self.value = new_value;
418 cx.emit(NumberStepperEvent::Change(self.value));
419 cx.notify();
420 }
421 }
422
423 fn increment(&mut self, multiplier: f64, cx: &mut Context<Self>) {
424 self.adjust_value(1.0, multiplier, cx);
425 }
426
427 fn decrement(&mut self, multiplier: f64, cx: &mut Context<Self>) {
428 self.adjust_value(-1.0, multiplier, cx);
429 }
430
431 fn enter_edit_mode(&mut self, window: &mut Window, cx: &mut Context<Self>) {
435 self.editing = true;
436 self.original_value = self.value;
437
438 let formatted = self.format_value();
440 self.edit_input.update(cx, |input, cx| {
441 input.set_value(&formatted, cx);
442 });
443
444 self.edit_input.read(cx).focus_handle().focus(window);
446 cx.notify();
447 }
448
449 fn cancel_edit(&mut self, cx: &mut Context<Self>) {
451 self.editing = false;
452 self.pending_refocus = true;
453 cx.notify();
454 }
455
456 fn commit_edit(&mut self, cx: &mut Context<Self>) {
458 self.apply_edit_value(cx);
459 self.editing = false;
460 self.pending_refocus = true;
461 cx.notify();
462 }
463
464 fn apply_edit_value(&mut self, cx: &mut Context<Self>) {
466 let content = self.edit_input.read(cx).content().to_string();
468
469 if let Ok(parsed) = content.trim().parse::<f64>() {
470 let normalized = self.normalize_value(parsed);
471 if (self.value - normalized).abs() > f64::EPSILON {
472 self.value = normalized;
473 cx.emit(NumberStepperEvent::Change(self.value));
474 }
475 }
476 }
478
479 fn start_drag(&mut self, x: f32) {
483 self.dragging = true;
484 self.drag_start_x = x;
485 self.drag_start_value = self.value;
486 }
487
488 fn update_drag(&mut self, x: f32, modifiers: &Modifiers, cx: &mut Context<Self>) {
490 if !self.dragging {
491 return;
492 }
493
494 let auto_scale_range = if self.auto_scale_drag {
496 self.min.zip(self.max).map(|(min, max)| max - min)
497 } else {
498 None
499 };
500
501 let base_value_per_pixel = if let Some(range) = auto_scale_range {
502 let width = self.value_display_width.get();
503 if width > 0.0 {
504 range / width as f64
505 } else {
506 self.value_per_pixel_normal
507 }
508 } else {
509 self.value_per_pixel_normal
510 };
511
512 let value_per_pixel = if modifiers.shift {
514 if auto_scale_range.is_some() {
515 base_value_per_pixel * 5.0 } else {
517 self.value_per_pixel_fast
518 }
519 } else if modifiers.alt {
520 if auto_scale_range.is_some() {
521 base_value_per_pixel * 0.1 } else {
523 self.value_per_pixel_slow
524 }
525 } else {
526 base_value_per_pixel
527 };
528
529 let delta_pixels = (x - self.drag_start_x) as f64;
530 let new_value = self.normalize_value(self.drag_start_value + delta_pixels * value_per_pixel);
531 if (self.value - new_value).abs() > f64::EPSILON {
532 self.value = new_value;
533 cx.emit(NumberStepperEvent::Change(self.value));
534 cx.notify();
535 }
536 }
537
538 fn end_drag(&mut self) {
540 self.dragging = false;
541 }
542}
543
544impl Render for NumberStepper {
545 fn render(&mut self, window: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement {
546 let theme = get_theme_or(cx, self.custom_theme.as_ref());
547 let display_value = self.format_value();
548 let focus_handle = self.focus_handle.clone();
549 let editing = self.editing;
550 let enabled = self.enabled;
551
552 if self.pending_refocus {
554 self.pending_refocus = false;
555 self.focus_handle.focus(window);
556 }
557
558 let is_focused = self.focus_handle.is_focused(window);
560
561 let bg_color = if enabled { theme.bg_input } else { theme.disabled_bg };
563 let border_color = if !enabled {
564 theme.disabled_bg
565 } else if is_focused || editing {
566 theme.border_focus
567 } else {
568 theme.border_input
569 };
570 let separator_color = if enabled { theme.text_muted } else { theme.disabled_text };
571 let text_color = if enabled { theme.text_value } else { theme.disabled_text };
572 let button_text_color = if enabled { theme.text_value } else { theme.disabled_text };
573
574 let value_element = if editing && enabled {
576 div()
578 .id("ccf_number_value")
579 .px_2()
580 .py_1()
581 .flex_1()
582 .flex()
583 .items_center()
584 .overflow_hidden()
585 .child(self.edit_input.clone())
586 } else {
587 let width_cell = self.value_display_width.clone();
590
591 let mut value_div = div()
592 .id("ccf_number_value")
593 .relative()
594 .px_2()
595 .py_1()
596 .flex_1()
597 .flex()
598 .items_center()
599 .justify_center()
600 .text_sm()
601 .text_color(rgb(text_color))
602 .when(enabled, |d| d.cursor(CursorStyle::ResizeLeftRight))
603 .when(!enabled, |d| d.cursor_default());
604
605 if enabled {
607 value_div = value_div
608 .on_mouse_down(MouseButton::Left, cx.listener(|stepper, event: &MouseDownEvent, window, cx| {
610 if !stepper.enabled {
611 return;
612 }
613 stepper.focus_handle.focus(window);
614 if event.click_count == 2 {
615 stepper.enter_edit_mode(window, cx);
617 } else {
618 let x: f32 = event.position.x.into();
620 stepper.start_drag(x);
621 }
622 }))
623 .on_drag(NumberDragState, |_state, _position, _window, cx| {
625 cx.new(|_| EmptyDragView)
626 })
627 .on_drag_move(cx.listener(|stepper, event: &DragMoveEvent<NumberDragState>, _window, cx| {
629 if stepper.dragging {
630 let x: f32 = event.event.position.x.into();
631 stepper.update_drag(x, &event.event.modifiers, cx);
632 }
633 }))
634 .on_mouse_up(MouseButton::Left, cx.listener(|stepper, _event: &MouseUpEvent, _window, _cx| {
636 stepper.end_drag();
637 }))
638 .on_mouse_up_out(MouseButton::Left, cx.listener(|stepper, _event: &MouseUpEvent, _window, _cx| {
640 stepper.end_drag();
641 }));
642 }
643
644 value_div
645 .child(
647 canvas(
648 move |bounds, _window, _cx| {
649 width_cell.set(bounds.size.width.into());
650 bounds
651 },
652 |_, _, _, _| {},
653 )
654 .size_full()
655 .absolute()
656 )
657 .child(display_value)
658 };
659
660 let separator = || {
662 div()
663 .w(px(1.0))
664 .h_full()
665 .bg(rgb(separator_color))
666 };
667
668 let mut decrement_button = div()
670 .id("ccf_number_decrement")
671 .flex()
672 .items_center()
673 .justify_center()
674 .px_2()
675 .py_1()
676 .text_color(rgb(button_text_color))
677 .cursor_for_enabled(enabled)
678 .when(enabled, |d| d.hover(|h| h.bg(rgb(theme.bg_hover))))
679 .child("\u{2212}"); if enabled {
682 decrement_button = decrement_button.on_click(cx.listener(|stepper, event: &ClickEvent, window, cx| {
683 if !stepper.enabled {
684 return;
685 }
686 stepper.focus_handle.focus(window);
687 if stepper.editing {
688 stepper.editing = false;
690 }
691 let multiplier = if event.modifiers().shift {
693 stepper.step_large_multiplier
694 } else if event.modifiers().alt {
695 stepper.step_small_multiplier
696 } else {
697 1.0
698 };
699 stepper.decrement(multiplier, cx);
700 }));
701 }
702
703 let mut increment_button = div()
705 .id("ccf_number_increment")
706 .flex()
707 .items_center()
708 .justify_center()
709 .px_2()
710 .py_1()
711 .text_color(rgb(button_text_color))
712 .cursor_for_enabled(enabled)
713 .when(enabled, |d| d.hover(|h| h.bg(rgb(theme.bg_hover))))
714 .child("+");
715
716 if enabled {
717 increment_button = increment_button.on_click(cx.listener(|stepper, event: &ClickEvent, window, cx| {
718 if !stepper.enabled {
719 return;
720 }
721 stepper.focus_handle.focus(window);
722 if stepper.editing {
723 stepper.editing = false;
725 }
726 let multiplier = if event.modifiers().shift {
728 stepper.step_large_multiplier
729 } else if event.modifiers().alt {
730 stepper.step_small_multiplier
731 } else {
732 1.0
733 };
734 stepper.increment(multiplier, cx);
735 }));
736 }
737
738 with_focus_actions(
740 div()
741 .id("ccf_number_stepper")
742 .track_focus(&focus_handle)
743 .tab_stop(enabled),
744 cx,
745 )
746 .on_key_down(cx.listener(|stepper, event: &KeyDownEvent, window, cx| {
747 if !stepper.enabled || stepper.editing {
749 return;
750 }
751 if handle_tab_navigation(event, window) {
752 return;
753 }
754 let multiplier = if event.keystroke.modifiers.shift { 10.0 } else { 1.0 };
755 match event.keystroke.key.as_str() {
756 "enter" => stepper.enter_edit_mode(window, cx),
757 "up" => stepper.increment(multiplier, cx),
758 "down" => stepper.decrement(multiplier, cx),
759 _ => {}
760 }
761 }))
762 .flex()
764 .flex_row()
765 .items_center()
766 .h(px(28.0)) .bg(rgb(bg_color))
768 .border_1()
769 .border_color(rgb(border_color))
770 .rounded_md()
771 .overflow_hidden()
772 .child(decrement_button)
774 .child(separator())
776 .child(value_element)
778 .child(separator())
780 .child(increment_button)
782 }
783}