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, 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}