1use std::{
2 fmt::Display,
3 num::{NonZero, NonZeroU32, NonZeroU64},
4 rc::Rc,
5 str::FromStr,
6};
7
8use editor::{Editor, actions::MoveDown, actions::MoveUp};
9use gpui::{
10 ClickEvent, Entity, FocusHandle, Focusable, FontWeight, Modifiers, TextAlign,
11 TextStyleRefinement, WeakEntity,
12};
13
14use settings::{
15 CenteredPaddingSettings, CodeFade, DelayMs, FontSize, InactiveOpacity, MinimumContrast,
16};
17use ui::prelude::*;
18
19#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
20pub enum NumberFieldMode {
21 #[default]
22 Read,
23 Edit,
24}
25
26pub trait NumberFieldType: Display + Copy + Clone + Sized + PartialOrd + FromStr + 'static {
27 fn default_format(value: &Self) -> String {
28 format!("{}", value)
29 }
30 fn default_step() -> Self;
31 fn large_step() -> Self;
32 fn small_step() -> Self;
33 fn min_value() -> Self;
34 fn max_value() -> Self;
35 fn saturating_add(self, rhs: Self) -> Self;
36 fn saturating_sub(self, rhs: Self) -> Self;
37}
38
39macro_rules! impl_newtype_numeric_stepper_float {
40 ($type:ident, $default:expr, $large:expr, $small:expr, $min:expr, $max:expr) => {
41 impl NumberFieldType for $type {
42 fn default_step() -> Self {
43 $default.into()
44 }
45
46 fn large_step() -> Self {
47 $large.into()
48 }
49
50 fn small_step() -> Self {
51 $small.into()
52 }
53
54 fn min_value() -> Self {
55 $min.into()
56 }
57
58 fn max_value() -> Self {
59 $max.into()
60 }
61
62 fn saturating_add(self, rhs: Self) -> Self {
63 $type((self.0 + rhs.0).min(Self::max_value().0))
64 }
65
66 fn saturating_sub(self, rhs: Self) -> Self {
67 $type((self.0 - rhs.0).max(Self::min_value().0))
68 }
69 }
70 };
71}
72
73macro_rules! impl_newtype_numeric_stepper_int {
74 ($type:ident, $default:expr, $large:expr, $small:expr, $min:expr, $max:expr) => {
75 impl NumberFieldType for $type {
76 fn default_step() -> Self {
77 $default.into()
78 }
79
80 fn large_step() -> Self {
81 $large.into()
82 }
83
84 fn small_step() -> Self {
85 $small.into()
86 }
87
88 fn min_value() -> Self {
89 $min.into()
90 }
91
92 fn max_value() -> Self {
93 $max.into()
94 }
95
96 fn saturating_add(self, rhs: Self) -> Self {
97 $type(self.0.saturating_add(rhs.0).min(Self::max_value().0))
98 }
99
100 fn saturating_sub(self, rhs: Self) -> Self {
101 $type(self.0.saturating_sub(rhs.0).max(Self::min_value().0))
102 }
103 }
104 };
105}
106
107#[rustfmt::skip]
108impl_newtype_numeric_stepper_float!(FontWeight, 50., 100., 10., FontWeight::THIN, FontWeight::BLACK);
109impl_newtype_numeric_stepper_float!(CodeFade, 0.1, 0.2, 0.05, 0.0, 0.9);
110impl_newtype_numeric_stepper_float!(FontSize, 1.0, 4.0, 0.5, 6.0, 72.0);
111impl_newtype_numeric_stepper_float!(InactiveOpacity, 0.1, 0.2, 0.05, 0.0, 1.0);
112impl_newtype_numeric_stepper_float!(MinimumContrast, 1., 10., 0.5, 0.0, 106.0);
113impl_newtype_numeric_stepper_int!(DelayMs, 100, 500, 10, 0, 2000);
114impl_newtype_numeric_stepper_float!(
115 CenteredPaddingSettings,
116 0.05,
117 0.2,
118 0.1,
119 CenteredPaddingSettings::MIN_PADDING,
120 CenteredPaddingSettings::MAX_PADDING
121);
122
123macro_rules! impl_numeric_stepper_int {
124 ($type:ident) => {
125 impl NumberFieldType for $type {
126 fn default_step() -> Self {
127 1
128 }
129
130 fn large_step() -> Self {
131 10
132 }
133
134 fn small_step() -> Self {
135 1
136 }
137
138 fn min_value() -> Self {
139 <$type>::MIN
140 }
141
142 fn max_value() -> Self {
143 <$type>::MAX
144 }
145
146 fn saturating_add(self, rhs: Self) -> Self {
147 self.saturating_add(rhs)
148 }
149
150 fn saturating_sub(self, rhs: Self) -> Self {
151 self.saturating_sub(rhs)
152 }
153 }
154 };
155}
156
157macro_rules! impl_numeric_stepper_nonzero_int {
158 ($nonzero:ty, $inner:ty) => {
159 impl NumberFieldType for $nonzero {
160 fn default_step() -> Self {
161 <$nonzero>::new(1).unwrap()
162 }
163
164 fn large_step() -> Self {
165 <$nonzero>::new(10).unwrap()
166 }
167
168 fn small_step() -> Self {
169 <$nonzero>::new(1).unwrap()
170 }
171
172 fn min_value() -> Self {
173 <$nonzero>::MIN
174 }
175
176 fn max_value() -> Self {
177 <$nonzero>::MAX
178 }
179
180 fn saturating_add(self, rhs: Self) -> Self {
181 let result = self.get().saturating_add(rhs.get());
182 <$nonzero>::new(result.max(1)).unwrap()
183 }
184
185 fn saturating_sub(self, rhs: Self) -> Self {
186 let result = self.get().saturating_sub(rhs.get()).max(1);
187 <$nonzero>::new(result).unwrap()
188 }
189 }
190 };
191}
192
193macro_rules! impl_numeric_stepper_float {
194 ($type:ident) => {
195 impl NumberFieldType for $type {
196 fn default_format(value: &Self) -> String {
197 format!("{:.2}", value)
198 }
199
200 fn default_step() -> Self {
201 1.0
202 }
203
204 fn large_step() -> Self {
205 10.0
206 }
207
208 fn small_step() -> Self {
209 0.1
210 }
211
212 fn min_value() -> Self {
213 <$type>::MIN
214 }
215
216 fn max_value() -> Self {
217 <$type>::MAX
218 }
219
220 fn saturating_add(self, rhs: Self) -> Self {
221 (self + rhs).clamp(Self::min_value(), Self::max_value())
222 }
223
224 fn saturating_sub(self, rhs: Self) -> Self {
225 (self - rhs).clamp(Self::min_value(), Self::max_value())
226 }
227 }
228 };
229}
230
231impl_numeric_stepper_float!(f32);
232impl_numeric_stepper_float!(f64);
233impl_numeric_stepper_int!(isize);
234impl_numeric_stepper_int!(usize);
235impl_numeric_stepper_int!(i32);
236impl_numeric_stepper_int!(u32);
237impl_numeric_stepper_int!(i64);
238impl_numeric_stepper_int!(u64);
239
240impl_numeric_stepper_nonzero_int!(NonZeroU32, u32);
241impl_numeric_stepper_nonzero_int!(NonZeroU64, u64);
242impl_numeric_stepper_nonzero_int!(NonZero<usize>, usize);
243
244type OnChangeCallback<T> = Rc<dyn Fn(&T, &mut Window, &mut App) + 'static>;
245
246#[derive(IntoElement, RegisterComponent)]
247pub struct NumberField<T: NumberFieldType = usize> {
248 id: ElementId,
249 value: T,
250 focus_handle: FocusHandle,
251 mode: Entity<NumberFieldMode>,
252 /// Stores a weak reference to the editor when in edit mode, so buttons can update its text
253 edit_editor: Entity<Option<WeakEntity<Editor>>>,
254 /// Stores the on_change callback in Entity state so it's not stale in focus_out handlers
255 on_change_state: Entity<Option<OnChangeCallback<T>>>,
256 /// Tracks the last prop value we synced to, so we can detect external changes (like reset)
257 last_synced_value: Entity<Option<T>>,
258 format: Box<dyn FnOnce(&T) -> String>,
259 large_step: T,
260 small_step: T,
261 step: T,
262 min_value: T,
263 max_value: T,
264 on_reset: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
265 on_change: Rc<dyn Fn(&T, &mut Window, &mut App) + 'static>,
266 tab_index: Option<isize>,
267}
268
269impl<T: NumberFieldType> NumberField<T> {
270 pub fn new(id: impl Into<ElementId>, value: T, window: &mut Window, cx: &mut App) -> Self {
271 let id = id.into();
272
273 let (mode, focus_handle, edit_editor, on_change_state, last_synced_value) =
274 window.with_id(id.clone(), |window| {
275 let mode = window.use_state(cx, |_, _| NumberFieldMode::default());
276 let focus_handle = window.use_state(cx, |_, cx| cx.focus_handle());
277 let edit_editor = window.use_state(cx, |_, _| None);
278 let on_change_state: Entity<Option<OnChangeCallback<T>>> =
279 window.use_state(cx, |_, _| None);
280 let last_synced_value: Entity<Option<T>> = window.use_state(cx, |_, _| None);
281 (
282 mode,
283 focus_handle,
284 edit_editor,
285 on_change_state,
286 last_synced_value,
287 )
288 });
289
290 Self {
291 id,
292 mode,
293 edit_editor,
294 on_change_state,
295 last_synced_value,
296 value,
297 focus_handle: focus_handle.read(cx).clone(),
298 format: Box::new(T::default_format),
299 large_step: T::large_step(),
300 step: T::default_step(),
301 small_step: T::small_step(),
302 min_value: T::min_value(),
303 max_value: T::max_value(),
304 on_reset: None,
305 on_change: Rc::new(|_, _, _| {}),
306 tab_index: None,
307 }
308 }
309
310 pub fn format(mut self, format: impl FnOnce(&T) -> String + 'static) -> Self {
311 self.format = Box::new(format);
312 self
313 }
314
315 pub fn small_step(mut self, step: T) -> Self {
316 self.small_step = step;
317 self
318 }
319
320 pub fn normal_step(mut self, step: T) -> Self {
321 self.step = step;
322 self
323 }
324
325 pub fn large_step(mut self, step: T) -> Self {
326 self.large_step = step;
327 self
328 }
329
330 pub fn min(mut self, min: T) -> Self {
331 self.min_value = min;
332 self
333 }
334
335 pub fn max(mut self, max: T) -> Self {
336 self.max_value = max;
337 self
338 }
339
340 pub fn mode(self, mode: NumberFieldMode, cx: &mut App) -> Self {
341 self.mode.write(cx, mode);
342 self
343 }
344
345 pub fn on_reset(
346 mut self,
347 on_reset: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
348 ) -> Self {
349 self.on_reset = Some(Box::new(on_reset));
350 self
351 }
352
353 pub fn tab_index(mut self, tab_index: isize) -> Self {
354 self.tab_index = Some(tab_index);
355 self
356 }
357
358 pub fn on_change(mut self, on_change: impl Fn(&T, &mut Window, &mut App) + 'static) -> Self {
359 self.on_change = Rc::new(on_change);
360 self
361 }
362
363 fn sync_on_change_state(&self, cx: &mut App) {
364 self.on_change_state
365 .update(cx, |state, _| *state = Some(self.on_change.clone()));
366 }
367}
368
369#[derive(Clone, Copy)]
370enum ValueChangeDirection {
371 Increment,
372 Decrement,
373}
374
375impl<T: NumberFieldType> RenderOnce for NumberField<T> {
376 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
377 // Sync the on_change callback to Entity state so focus_out handlers can access it
378 self.sync_on_change_state(cx);
379
380 let is_edit_mode = matches!(*self.mode.read(cx), NumberFieldMode::Edit);
381
382 let get_step = {
383 let large_step = self.large_step;
384 let step = self.step;
385 let small_step = self.small_step;
386 move |modifiers: Modifiers| -> T {
387 if modifiers.shift {
388 large_step
389 } else if modifiers.alt {
390 small_step
391 } else {
392 step
393 }
394 }
395 };
396
397 let clamp_value = {
398 let min = self.min_value;
399 let max = self.max_value;
400 move |value: T| -> T {
401 if value < min {
402 min
403 } else if value > max {
404 max
405 } else {
406 value
407 }
408 }
409 };
410
411 let change_value = {
412 move |current: T, step: T, direction: ValueChangeDirection| -> T {
413 let new_value = match direction {
414 ValueChangeDirection::Increment => current.saturating_add(step),
415 ValueChangeDirection::Decrement => current.saturating_sub(step),
416 };
417 clamp_value(new_value)
418 }
419 };
420
421 let get_current_value = {
422 let value = self.value;
423 let edit_editor = self.edit_editor.clone();
424
425 Rc::new(move |cx: &App| -> T {
426 if !is_edit_mode {
427 return value;
428 }
429 edit_editor
430 .read(cx)
431 .as_ref()
432 .and_then(|weak| weak.upgrade())
433 .and_then(|editor| editor.read(cx).text(cx).parse::<T>().ok())
434 .unwrap_or(value)
435 })
436 };
437
438 let update_editor_text = {
439 let edit_editor = self.edit_editor.clone();
440
441 Rc::new(move |new_value: T, window: &mut Window, cx: &mut App| {
442 if !is_edit_mode {
443 return;
444 }
445 let Some(editor) = edit_editor
446 .read(cx)
447 .as_ref()
448 .and_then(|weak| weak.upgrade())
449 else {
450 return;
451 };
452 editor.update(cx, |editor, cx| {
453 editor.set_text(format!("{}", new_value), window, cx);
454 });
455 })
456 };
457
458 let bg_color = cx.theme().colors().surface_background;
459 let hover_bg_color = cx.theme().colors().element_hover;
460
461 let border_color = cx.theme().colors().border_variant;
462 let focus_border_color = cx.theme().colors().border_focused;
463
464 let base_button = |icon: IconName| {
465 h_flex()
466 .cursor_pointer()
467 .p_1p5()
468 .size_full()
469 .justify_center()
470 .overflow_hidden()
471 .border_1()
472 .border_color(border_color)
473 .bg(bg_color)
474 .hover(|s| s.bg(hover_bg_color))
475 .focus_visible(|s| s.border_color(focus_border_color).bg(hover_bg_color))
476 .child(Icon::new(icon).size(IconSize::Small))
477 };
478
479 h_flex()
480 .id(self.id.clone())
481 .track_focus(&self.focus_handle)
482 .gap_1()
483 .when_some(self.on_reset, |this, on_reset| {
484 this.child(
485 IconButton::new("reset", IconName::RotateCcw)
486 .icon_size(IconSize::Small)
487 .when_some(self.tab_index, |this, _| this.tab_index(0isize))
488 .on_click(on_reset),
489 )
490 })
491 .child({
492 let on_change_for_increment = self.on_change.clone();
493
494 h_flex()
495 .map(|decrement| {
496 let decrement_handler = {
497 let on_change = self.on_change.clone();
498 let get_current_value = get_current_value.clone();
499 let update_editor_text = update_editor_text.clone();
500
501 move |click: &ClickEvent, window: &mut Window, cx: &mut App| {
502 let current_value = get_current_value(cx);
503 let step = get_step(click.modifiers());
504 let new_value = change_value(
505 current_value,
506 step,
507 ValueChangeDirection::Decrement,
508 );
509
510 update_editor_text(new_value, window, cx);
511 on_change(&new_value, window, cx);
512 }
513 };
514
515 decrement.child(
516 base_button(IconName::Dash)
517 .id((self.id.clone(), "decrement_button"))
518 .rounded_tl_sm()
519 .rounded_bl_sm()
520 .when_some(self.tab_index, |this, _| this.tab_index(0isize))
521 .on_click(decrement_handler),
522 )
523 })
524 .child({
525 h_flex()
526 .min_w_16()
527 .size_full()
528 .border_y_1()
529 .border_color(border_color)
530 .bg(bg_color)
531 .in_focus(|this| this.border_color(focus_border_color))
532 .child(match *self.mode.read(cx) {
533 NumberFieldMode::Read => h_flex()
534 .px_1()
535 .flex_1()
536 .justify_center()
537 .child(
538 Label::new((self.format)(&self.value)).color(Color::Muted),
539 )
540 .into_any_element(),
541 NumberFieldMode::Edit => {
542 let expected_text = format!("{}", self.value);
543
544 let editor = window.use_state(cx, {
545 let expected_text = expected_text.clone();
546
547 move |window, cx| {
548 let mut editor = Editor::single_line(window, cx);
549
550 editor.set_text_style_refinement(TextStyleRefinement {
551 color: Some(cx.theme().colors().text),
552 text_align: Some(TextAlign::Center),
553 ..Default::default()
554 });
555
556 editor.set_text(expected_text, window, cx);
557
558 let editor_weak = cx.entity().downgrade();
559
560 self.edit_editor.update(cx, |state, _| {
561 *state = Some(editor_weak);
562 });
563
564 editor
565 .register_action::<MoveUp>({
566 let on_change = self.on_change.clone();
567 let editor_handle = cx.entity().downgrade();
568 move |_, window, cx| {
569 let Some(editor) = editor_handle.upgrade()
570 else {
571 return;
572 };
573 editor.update(cx, |editor, cx| {
574 if let Ok(current_value) =
575 editor.text(cx).parse::<T>()
576 {
577 let step =
578 get_step(window.modifiers());
579 let new_value = change_value(
580 current_value,
581 step,
582 ValueChangeDirection::Increment,
583 );
584 editor.set_text(
585 format!("{}", new_value),
586 window,
587 cx,
588 );
589 on_change(&new_value, window, cx);
590 }
591 });
592 }
593 })
594 .detach();
595
596 editor
597 .register_action::<MoveDown>({
598 let on_change = self.on_change.clone();
599 let editor_handle = cx.entity().downgrade();
600 move |_, window, cx| {
601 let Some(editor) = editor_handle.upgrade()
602 else {
603 return;
604 };
605 editor.update(cx, |editor, cx| {
606 if let Ok(current_value) =
607 editor.text(cx).parse::<T>()
608 {
609 let step =
610 get_step(window.modifiers());
611 let new_value = change_value(
612 current_value,
613 step,
614 ValueChangeDirection::Decrement,
615 );
616 editor.set_text(
617 format!("{}", new_value),
618 window,
619 cx,
620 );
621 on_change(&new_value, window, cx);
622 }
623 });
624 }
625 })
626 .detach();
627
628 cx.on_focus_out(&editor.focus_handle(cx), window, {
629 let on_change_state = self.on_change_state.clone();
630 move |this, _, window, cx| {
631 if let Ok(parsed_value) =
632 this.text(cx).parse::<T>()
633 {
634 let new_value = clamp_value(parsed_value);
635 let on_change =
636 on_change_state.read(cx).clone();
637
638 if let Some(on_change) = on_change.as_ref()
639 {
640 on_change(&new_value, window, cx);
641 }
642 };
643 }
644 })
645 .detach();
646
647 editor
648 }
649 });
650
651 let focus_handle = editor.focus_handle(cx);
652 let is_focused = focus_handle.is_focused(window);
653
654 if !is_focused {
655 let current_text = editor.read(cx).text(cx);
656 let last_synced = *self.last_synced_value.read(cx);
657
658 // Detect if the value changed externally (e.g., reset button)
659 let value_changed_externally = last_synced
660 .map(|last| last != self.value)
661 .unwrap_or(true);
662
663 let should_sync = if value_changed_externally {
664 true
665 } else {
666 match current_text.parse::<T>().ok() {
667 Some(parsed) => parsed == self.value,
668 None => true,
669 }
670 };
671
672 if should_sync && current_text != expected_text {
673 editor.update(cx, |editor, cx| {
674 editor.set_text(expected_text.clone(), window, cx);
675 });
676 }
677
678 self.last_synced_value
679 .update(cx, |state, _| *state = Some(self.value));
680 }
681
682 let focus_handle = if self.tab_index.is_some() {
683 focus_handle.tab_index(0isize).tab_stop(true)
684 } else {
685 focus_handle
686 };
687
688 h_flex()
689 .flex_1()
690 .h_full()
691 .track_focus(&focus_handle)
692 .when(is_focused, |this| {
693 this.border_1()
694 .border_color(cx.theme().colors().border_focused)
695 })
696 .child(editor)
697 .on_action::<menu::Confirm>({
698 move |_, window, _| {
699 window.blur();
700 }
701 })
702 .into_any_element()
703 }
704 })
705 })
706 .map(|increment| {
707 let increment_handler = {
708 let on_change = on_change_for_increment.clone();
709 let get_current_value = get_current_value.clone();
710 let update_editor_text = update_editor_text.clone();
711
712 move |click: &ClickEvent, window: &mut Window, cx: &mut App| {
713 let current_value = get_current_value(cx);
714 let step = get_step(click.modifiers());
715 let new_value = change_value(
716 current_value,
717 step,
718 ValueChangeDirection::Increment,
719 );
720
721 update_editor_text(new_value, window, cx);
722 on_change(&new_value, window, cx);
723 }
724 };
725
726 increment.child(
727 base_button(IconName::Plus)
728 .id((self.id.clone(), "increment_button"))
729 .rounded_tr_sm()
730 .rounded_br_sm()
731 .when_some(self.tab_index, |this, _| this.tab_index(0isize))
732 .on_click(increment_handler),
733 )
734 })
735 })
736 }
737}
738
739impl Component for NumberField<usize> {
740 fn scope() -> ComponentScope {
741 ComponentScope::Input
742 }
743
744 fn name() -> &'static str {
745 "Number Field"
746 }
747
748 fn description() -> Option<&'static str> {
749 Some("A numeric input element with increment and decrement buttons.")
750 }
751
752 fn preview(window: &mut Window, cx: &mut App) -> Option<AnyElement> {
753 let default_ex = window.use_state(cx, |_, _| 100.0);
754 let edit_ex = window.use_state(cx, |_, _| 500.0);
755
756 Some(
757 v_flex()
758 .gap_6()
759 .children(vec![
760 single_example(
761 "Button-Only Number Field",
762 NumberField::new("number-field", *default_ex.read(cx), window, cx)
763 .on_change({
764 let default_ex = default_ex.clone();
765 move |value, _, cx| default_ex.write(cx, *value)
766 })
767 .min(1.0)
768 .max(100.0)
769 .into_any_element(),
770 ),
771 single_example(
772 "Editable Number Field",
773 NumberField::new("editable-number-field", *edit_ex.read(cx), window, cx)
774 .on_change({
775 let edit_ex = edit_ex.clone();
776 move |value, _, cx| edit_ex.write(cx, *value)
777 })
778 .min(100.0)
779 .max(500.0)
780 .mode(NumberFieldMode::Edit, cx)
781 .into_any_element(),
782 ),
783 ])
784 .into_any_element(),
785 )
786 }
787}