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