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