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