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