number_field.rs

  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}