number_field.rs

  1use std::{
  2    fmt::Display,
  3    num::{NonZero, NonZeroU32, NonZeroU64},
  4    rc::Rc,
  5    str::FromStr,
  6};
  7
  8use editor::{Editor, EditorStyle};
  9use gpui::{ClickEvent, Entity, FocusHandle, Focusable, FontWeight, Modifiers};
 10
 11use settings::{CenteredPaddingSettings, CodeFade, DelayMs, InactiveOpacity, MinimumContrast};
 12use ui::prelude::*;
 13
 14#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
 15pub enum NumberFieldMode {
 16    #[default]
 17    Read,
 18    Edit,
 19}
 20
 21pub trait NumberFieldType: Display + Copy + Clone + Sized + PartialOrd + FromStr + 'static {
 22    fn default_format(value: &Self) -> String {
 23        format!("{}", value)
 24    }
 25    fn default_step() -> Self;
 26    fn large_step() -> Self;
 27    fn small_step() -> Self;
 28    fn min_value() -> Self;
 29    fn max_value() -> Self;
 30    fn saturating_add(self, rhs: Self) -> Self;
 31    fn saturating_sub(self, rhs: Self) -> Self;
 32}
 33
 34macro_rules! impl_newtype_numeric_stepper_float {
 35    ($type:ident, $default:expr, $large:expr, $small:expr, $min:expr, $max:expr) => {
 36        impl NumberFieldType for $type {
 37            fn default_step() -> Self {
 38                $default.into()
 39            }
 40
 41            fn large_step() -> Self {
 42                $large.into()
 43            }
 44
 45            fn small_step() -> Self {
 46                $small.into()
 47            }
 48
 49            fn min_value() -> Self {
 50                $min.into()
 51            }
 52
 53            fn max_value() -> Self {
 54                $max.into()
 55            }
 56
 57            fn saturating_add(self, rhs: Self) -> Self {
 58                $type((self.0 + rhs.0).min(Self::max_value().0))
 59            }
 60
 61            fn saturating_sub(self, rhs: Self) -> Self {
 62                $type((self.0 - rhs.0).max(Self::min_value().0))
 63            }
 64        }
 65    };
 66}
 67
 68macro_rules! impl_newtype_numeric_stepper_int {
 69    ($type:ident, $default:expr, $large:expr, $small:expr, $min:expr, $max:expr) => {
 70        impl NumberFieldType for $type {
 71            fn default_step() -> Self {
 72                $default.into()
 73            }
 74
 75            fn large_step() -> Self {
 76                $large.into()
 77            }
 78
 79            fn small_step() -> Self {
 80                $small.into()
 81            }
 82
 83            fn min_value() -> Self {
 84                $min.into()
 85            }
 86
 87            fn max_value() -> Self {
 88                $max.into()
 89            }
 90
 91            fn saturating_add(self, rhs: Self) -> Self {
 92                $type(self.0.saturating_add(rhs.0).min(Self::max_value().0))
 93            }
 94
 95            fn saturating_sub(self, rhs: Self) -> Self {
 96                $type(self.0.saturating_sub(rhs.0).max(Self::min_value().0))
 97            }
 98        }
 99    };
100}
101
102#[rustfmt::skip]
103impl_newtype_numeric_stepper_float!(FontWeight, 50., 100., 10., FontWeight::THIN, FontWeight::BLACK);
104impl_newtype_numeric_stepper_float!(CodeFade, 0.1, 0.2, 0.05, 0.0, 0.9);
105impl_newtype_numeric_stepper_float!(InactiveOpacity, 0.1, 0.2, 0.05, 0.0, 1.0);
106impl_newtype_numeric_stepper_float!(MinimumContrast, 1., 10., 0.5, 0.0, 106.0);
107impl_newtype_numeric_stepper_int!(DelayMs, 100, 500, 10, 0, 2000);
108impl_newtype_numeric_stepper_float!(
109    CenteredPaddingSettings,
110    0.05,
111    0.2,
112    0.1,
113    CenteredPaddingSettings::MIN_PADDING,
114    CenteredPaddingSettings::MAX_PADDING
115);
116
117macro_rules! impl_numeric_stepper_int {
118    ($type:ident) => {
119        impl NumberFieldType for $type {
120            fn default_step() -> Self {
121                1
122            }
123
124            fn large_step() -> Self {
125                10
126            }
127
128            fn small_step() -> Self {
129                1
130            }
131
132            fn min_value() -> Self {
133                <$type>::MIN
134            }
135
136            fn max_value() -> Self {
137                <$type>::MAX
138            }
139
140            fn saturating_add(self, rhs: Self) -> Self {
141                self.saturating_add(rhs)
142            }
143
144            fn saturating_sub(self, rhs: Self) -> Self {
145                self.saturating_sub(rhs)
146            }
147        }
148    };
149}
150
151macro_rules! impl_numeric_stepper_nonzero_int {
152    ($nonzero:ty, $inner:ty) => {
153        impl NumberFieldType for $nonzero {
154            fn default_step() -> Self {
155                <$nonzero>::new(1).unwrap()
156            }
157
158            fn large_step() -> Self {
159                <$nonzero>::new(10).unwrap()
160            }
161
162            fn small_step() -> Self {
163                <$nonzero>::new(1).unwrap()
164            }
165
166            fn min_value() -> Self {
167                <$nonzero>::MIN
168            }
169
170            fn max_value() -> Self {
171                <$nonzero>::MAX
172            }
173
174            fn saturating_add(self, rhs: Self) -> Self {
175                let result = self.get().saturating_add(rhs.get());
176                <$nonzero>::new(result.max(1)).unwrap()
177            }
178
179            fn saturating_sub(self, rhs: Self) -> Self {
180                let result = self.get().saturating_sub(rhs.get()).max(1);
181                <$nonzero>::new(result).unwrap()
182            }
183        }
184    };
185}
186
187macro_rules! impl_numeric_stepper_float {
188    ($type:ident) => {
189        impl NumberFieldType for $type {
190            fn default_format(value: &Self) -> String {
191                format!("{:.2}", value)
192            }
193
194            fn default_step() -> Self {
195                1.0
196            }
197
198            fn large_step() -> Self {
199                10.0
200            }
201
202            fn small_step() -> Self {
203                0.1
204            }
205
206            fn min_value() -> Self {
207                <$type>::MIN
208            }
209
210            fn max_value() -> Self {
211                <$type>::MAX
212            }
213
214            fn saturating_add(self, rhs: Self) -> Self {
215                (self + rhs).clamp(Self::min_value(), Self::max_value())
216            }
217
218            fn saturating_sub(self, rhs: Self) -> Self {
219                (self - rhs).clamp(Self::min_value(), Self::max_value())
220            }
221        }
222    };
223}
224
225impl_numeric_stepper_float!(f32);
226impl_numeric_stepper_float!(f64);
227impl_numeric_stepper_int!(isize);
228impl_numeric_stepper_int!(usize);
229impl_numeric_stepper_int!(i32);
230impl_numeric_stepper_int!(u32);
231impl_numeric_stepper_int!(i64);
232impl_numeric_stepper_int!(u64);
233
234impl_numeric_stepper_nonzero_int!(NonZeroU32, u32);
235impl_numeric_stepper_nonzero_int!(NonZeroU64, u64);
236impl_numeric_stepper_nonzero_int!(NonZero<usize>, usize);
237
238#[derive(RegisterComponent)]
239pub struct NumberField<T = usize> {
240    id: ElementId,
241    value: T,
242    focus_handle: FocusHandle,
243    mode: Entity<NumberFieldMode>,
244    format: Box<dyn FnOnce(&T) -> String>,
245    large_step: T,
246    small_step: T,
247    step: T,
248    min_value: T,
249    max_value: T,
250    on_reset: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
251    on_change: Rc<dyn Fn(&T, &mut Window, &mut App) + 'static>,
252    tab_index: Option<isize>,
253}
254
255impl<T: NumberFieldType> NumberField<T> {
256    pub fn new(id: impl Into<ElementId>, value: T, window: &mut Window, cx: &mut App) -> Self {
257        let id = id.into();
258
259        let (mode, focus_handle) = window.with_id(id.clone(), |window| {
260            let mode = window.use_state(cx, |_, _| NumberFieldMode::default());
261            let focus_handle = window.use_state(cx, |_, cx| cx.focus_handle());
262            (mode, focus_handle)
263        });
264
265        Self {
266            id,
267            mode,
268            value,
269            focus_handle: focus_handle.read(cx).clone(),
270            format: Box::new(T::default_format),
271            large_step: T::large_step(),
272            step: T::default_step(),
273            small_step: T::small_step(),
274            min_value: T::min_value(),
275            max_value: T::max_value(),
276            on_reset: None,
277            on_change: Rc::new(|_, _, _| {}),
278            tab_index: None,
279        }
280    }
281
282    pub fn format(mut self, format: impl FnOnce(&T) -> String + 'static) -> Self {
283        self.format = Box::new(format);
284        self
285    }
286
287    pub fn small_step(mut self, step: T) -> Self {
288        self.small_step = step;
289        self
290    }
291
292    pub fn normal_step(mut self, step: T) -> Self {
293        self.step = step;
294        self
295    }
296
297    pub fn large_step(mut self, step: T) -> Self {
298        self.large_step = step;
299        self
300    }
301
302    pub fn min(mut self, min: T) -> Self {
303        self.min_value = min;
304        self
305    }
306
307    pub fn max(mut self, max: T) -> Self {
308        self.max_value = max;
309        self
310    }
311
312    pub fn on_reset(
313        mut self,
314        on_reset: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
315    ) -> Self {
316        self.on_reset = Some(Box::new(on_reset));
317        self
318    }
319
320    pub fn tab_index(mut self, tab_index: isize) -> Self {
321        self.tab_index = Some(tab_index);
322        self
323    }
324
325    pub fn on_change(mut self, on_change: impl Fn(&T, &mut Window, &mut App) + 'static) -> Self {
326        self.on_change = Rc::new(on_change);
327        self
328    }
329}
330
331impl<T: NumberFieldType> IntoElement for NumberField<T> {
332    type Element = gpui::Component<Self>;
333
334    fn into_element(self) -> Self::Element {
335        gpui::Component::new(self)
336    }
337}
338
339impl<T: NumberFieldType> RenderOnce for NumberField<T> {
340    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
341        let mut tab_index = self.tab_index;
342
343        let get_step = {
344            let large_step = self.large_step;
345            let step = self.step;
346            let small_step = self.small_step;
347            move |modifiers: Modifiers| -> T {
348                if modifiers.shift {
349                    large_step
350                } else if modifiers.alt {
351                    small_step
352                } else {
353                    step
354                }
355            }
356        };
357
358        let bg_color = cx.theme().colors().surface_background;
359        let hover_bg_color = cx.theme().colors().element_hover;
360
361        let border_color = cx.theme().colors().border_variant;
362        let focus_border_color = cx.theme().colors().border_focused;
363
364        let base_button = |icon: IconName| {
365            h_flex()
366                .cursor_pointer()
367                .p_1p5()
368                .size_full()
369                .justify_center()
370                .overflow_hidden()
371                .border_1()
372                .border_color(border_color)
373                .bg(bg_color)
374                .hover(|s| s.bg(hover_bg_color))
375                .focus_visible(|s| s.border_color(focus_border_color).bg(hover_bg_color))
376                .child(Icon::new(icon).size(IconSize::Small))
377        };
378
379        h_flex()
380            .id(self.id.clone())
381            .track_focus(&self.focus_handle)
382            .gap_1()
383            .when_some(self.on_reset, |this, on_reset| {
384                this.child(
385                    IconButton::new("reset", IconName::RotateCcw)
386                        .icon_size(IconSize::Small)
387                        .when_some(tab_index.as_mut(), |this, tab_index| {
388                            *tab_index += 1;
389                            this.tab_index(*tab_index - 1)
390                        })
391                        .on_click(on_reset),
392                )
393            })
394            .child(
395                h_flex()
396                    .map(|decrement| {
397                        let decrement_handler = {
398                            let value = self.value;
399                            let on_change = self.on_change.clone();
400                            let min = self.min_value;
401                            move |click: &ClickEvent, window: &mut Window, cx: &mut App| {
402                                let step = get_step(click.modifiers());
403                                let new_value = value.saturating_sub(step);
404                                let new_value = if new_value < min { min } else { new_value };
405                                on_change(&new_value, window, cx);
406                            }
407                        };
408
409                        decrement.child(
410                            base_button(IconName::Dash)
411                                .id("decrement_button")
412                                .rounded_tl_sm()
413                                .rounded_bl_sm()
414                                .tab_index(
415                                    tab_index
416                                        .as_mut()
417                                        .map(|tab_index| {
418                                            *tab_index += 1;
419                                            *tab_index - 1
420                                        })
421                                        .unwrap_or(0),
422                                )
423                                .on_click(decrement_handler),
424                        )
425                    })
426                    .child(
427                        h_flex()
428                            .min_w_16()
429                            .size_full()
430                            .border_y_1()
431                            .border_color(border_color)
432                            .bg(bg_color)
433                            .in_focus(|this| this.border_color(focus_border_color))
434                            .child(match *self.mode.read(cx) {
435                                NumberFieldMode::Read => h_flex()
436                                    .px_1()
437                                    .flex_1()
438                                    .justify_center()
439                                    .child(Label::new((self.format)(&self.value)))
440                                    .into_any_element(),
441                                // Edit mode is disabled until we implement center text alignment for editor
442                                // mode.write(cx, NumberFieldMode::Edit);
443                                //
444                                // When we get to making Edit mode work, we shouldn't even focus the decrement/increment buttons.
445                                // Focus should go instead straight to the editor, avoiding any double-step focus.
446                                // In this world, the buttons become a mouse-only interaction, given users should be able
447                                // to do everything they'd do with the buttons straight in the editor anyway.
448                                NumberFieldMode::Edit => h_flex()
449                                    .flex_1()
450                                    .child(window.use_state(cx, {
451                                        |window, cx| {
452                                            let previous_focus_handle = window.focused(cx);
453                                            let mut editor = Editor::single_line(window, cx);
454                                            let mut style = EditorStyle::default();
455                                            style.text.text_align = gpui::TextAlign::Right;
456                                            editor.set_style(style, window, cx);
457
458                                            editor.set_text(format!("{}", self.value), window, cx);
459                                            cx.on_focus_out(&editor.focus_handle(cx), window, {
460                                                let mode = self.mode.clone();
461                                                let min = self.min_value;
462                                                let max = self.max_value;
463                                                let on_change = self.on_change.clone();
464                                                move |this, _, window, cx| {
465                                                    if let Ok(new_value) =
466                                                        this.text(cx).parse::<T>()
467                                                    {
468                                                        let new_value = if new_value < min {
469                                                            min
470                                                        } else if new_value > max {
471                                                            max
472                                                        } else {
473                                                            new_value
474                                                        };
475
476                                                        if let Some(previous) =
477                                                            previous_focus_handle.as_ref()
478                                                        {
479                                                            window.focus(previous);
480                                                        }
481                                                        on_change(&new_value, window, cx);
482                                                    };
483                                                    mode.write(cx, NumberFieldMode::Read);
484                                                }
485                                            })
486                                            .detach();
487
488                                            window.focus(&editor.focus_handle(cx));
489
490                                            editor
491                                        }
492                                    }))
493                                    .on_action::<menu::Confirm>({
494                                        move |_, window, _| {
495                                            window.blur();
496                                        }
497                                    })
498                                    .into_any_element(),
499                            }),
500                    )
501                    .map(|increment| {
502                        let increment_handler = {
503                            let value = self.value;
504                            let on_change = self.on_change.clone();
505                            let max = self.max_value;
506                            move |click: &ClickEvent, window: &mut Window, cx: &mut App| {
507                                let step = get_step(click.modifiers());
508                                let new_value = value.saturating_add(step);
509                                let new_value = if new_value > max { max } else { new_value };
510                                on_change(&new_value, window, cx);
511                            }
512                        };
513
514                        increment.child(
515                            base_button(IconName::Plus)
516                                .id("increment_button")
517                                .rounded_tr_sm()
518                                .rounded_br_sm()
519                                .tab_index(
520                                    tab_index
521                                        .as_mut()
522                                        .map(|tab_index| {
523                                            *tab_index += 1;
524                                            *tab_index - 1
525                                        })
526                                        .unwrap_or(0),
527                                )
528                                .on_click(increment_handler),
529                        )
530                    }),
531            )
532    }
533}
534
535impl Component for NumberField<usize> {
536    fn scope() -> ComponentScope {
537        ComponentScope::Input
538    }
539
540    fn name() -> &'static str {
541        "Number Field"
542    }
543
544    fn sort_name() -> &'static str {
545        Self::name()
546    }
547
548    fn description() -> Option<&'static str> {
549        Some("A numeric input element with increment and decrement buttons.")
550    }
551
552    fn preview(window: &mut Window, cx: &mut App) -> Option<AnyElement> {
553        let stepper_example = window.use_state(cx, |_, _| 100.0);
554
555        Some(
556            v_flex()
557                .gap_6()
558                .children(vec![single_example(
559                    "Default Numeric Stepper",
560                    NumberField::new(
561                        "numeric-stepper-component-preview",
562                        *stepper_example.read(cx),
563                        window,
564                        cx,
565                    )
566                    .on_change({
567                        let stepper_example = stepper_example.clone();
568                        move |value, _, cx| stepper_example.write(cx, *value)
569                    })
570                    .min(1.0)
571                    .max(100.0)
572                    .into_any_element(),
573                )])
574                .into_any_element(),
575        )
576    }
577}