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