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