number_field.rs

  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}