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, 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        h_flex()
348            .id(self.id.clone())
349            .track_focus(&self.focus_handle)
350            .gap_1()
351            .when_some(self.on_reset, |this, on_reset| {
352                this.child(
353                    IconButton::new("reset", IconName::RotateCcw)
354                        .icon_size(IconSize::Small)
355                        .when_some(tab_index.as_mut(), |this, tab_index| {
356                            *tab_index += 1;
357                            this.tab_index(*tab_index - 1)
358                        })
359                        .on_click(on_reset),
360                )
361            })
362            .child(
363                h_flex()
364                    .map(|decrement| {
365                        let decrement_handler = {
366                            let value = self.value;
367                            let on_change = self.on_change.clone();
368                            let min = self.min_value;
369                            move |click: &ClickEvent, window: &mut Window, cx: &mut App| {
370                                let step = get_step(click.modifiers());
371                                let new_value = value.saturating_sub(step);
372                                let new_value = if new_value < min { min } else { new_value };
373                                on_change(&new_value, window, cx);
374                                window.focus_prev();
375                            }
376                        };
377
378                        decrement.child(
379                            h_flex()
380                                .id("decrement_button")
381                                .cursor(gpui::CursorStyle::PointingHand)
382                                .p_1p5()
383                                .size_full()
384                                .justify_center()
385                                .overflow_hidden()
386                                .rounded_tl_sm()
387                                .rounded_bl_sm()
388                                .border_1()
389                                .border_color(cx.theme().colors().border_variant)
390                                .bg(cx.theme().colors().surface_background)
391                                .hover(|s| s.bg(cx.theme().colors().element_hover))
392                                .child(Icon::new(IconName::Dash).size(IconSize::Small))
393                                .when_some(tab_index.as_mut(), |this, tab_index| {
394                                    *tab_index += 1;
395                                    this.tab_index(*tab_index - 1).focus(|style| {
396                                        style
397                                            .border_color(cx.theme().colors().border_focused)
398                                            .bg(cx.theme().colors().element_hover)
399                                    })
400                                })
401                                .on_click(decrement_handler),
402                        )
403                    })
404                    .child(
405                        h_flex()
406                            .min_w_16()
407                            .size_full()
408                            .border_y_1()
409                            .border_color(cx.theme().colors().border_variant)
410                            .bg(cx.theme().colors().surface_background)
411                            .in_focus(|this| this.border_color(cx.theme().colors().border_focused))
412                            .child(match *self.mode.read(cx) {
413                                NumberFieldMode::Read => h_flex()
414                                    .id("numeric_stepper_label")
415                                    .px_1()
416                                    .flex_1()
417                                    .justify_center()
418                                    .child(Label::new((self.format)(&self.value)))
419                                    .when_some(tab_index.as_mut(), |this, tab_index| {
420                                        *tab_index += 1;
421                                        this.tab_index(*tab_index - 1).focus(|style| {
422                                            style
423                                                .border_color(cx.theme().colors().border_focused)
424                                                .bg(cx.theme().colors().element_hover)
425                                        })
426                                    })
427                                    .on_click({
428                                        let _mode = self.mode.clone();
429                                        move |click, _, _cx| {
430                                            if click.click_count() == 2 || click.is_keyboard() {
431                                                // Edit mode is disabled until we implement center text alignment for editor
432                                                // mode.write(cx, NumberFieldMode::Edit);
433                                            }
434                                        }
435                                    })
436                                    .into_any_element(),
437                                NumberFieldMode::Edit => h_flex()
438                                    .flex_1()
439                                    .child(window.use_state(cx, {
440                                        |window, cx| {
441                                            let previous_focus_handle = window.focused(cx);
442                                            let mut editor = Editor::single_line(window, cx);
443                                            let mut style = EditorStyle::default();
444                                            style.text.text_align = gpui::TextAlign::Right;
445                                            editor.set_style(style, window, cx);
446
447                                            editor.set_text(format!("{}", self.value), window, cx);
448                                            cx.on_focus_out(&editor.focus_handle(cx), window, {
449                                                let mode = self.mode.clone();
450                                                let min = self.min_value;
451                                                let max = self.max_value;
452                                                let on_change = self.on_change.clone();
453                                                move |this, _, window, cx| {
454                                                    if let Ok(new_value) =
455                                                        this.text(cx).parse::<T>()
456                                                    {
457                                                        let new_value = if new_value < min {
458                                                            min
459                                                        } else if new_value > max {
460                                                            max
461                                                        } else {
462                                                            new_value
463                                                        };
464
465                                                        if let Some(previous) =
466                                                            previous_focus_handle.as_ref()
467                                                        {
468                                                            window.focus(previous);
469                                                        }
470                                                        on_change(&new_value, window, cx);
471                                                    };
472                                                    mode.write(cx, NumberFieldMode::Read);
473                                                }
474                                            })
475                                            .detach();
476
477                                            window.focus(&editor.focus_handle(cx));
478
479                                            editor
480                                        }
481                                    }))
482                                    .on_action::<menu::Confirm>({
483                                        move |_, window, _| {
484                                            window.blur();
485                                        }
486                                    })
487                                    .into_any_element(),
488                            }),
489                    )
490                    .map(|increment| {
491                        let increment_handler = {
492                            let value = self.value;
493                            let on_change = self.on_change.clone();
494                            let max = self.max_value;
495                            move |click: &ClickEvent, window: &mut Window, cx: &mut App| {
496                                let step = get_step(click.modifiers());
497                                let new_value = value.saturating_add(step);
498                                let new_value = if new_value > max { max } else { new_value };
499                                on_change(&new_value, window, cx);
500                            }
501                        };
502
503                        increment.child(
504                            h_flex()
505                                .id("increment_button")
506                                .cursor(gpui::CursorStyle::PointingHand)
507                                .p_1p5()
508                                .size_full()
509                                .justify_center()
510                                .overflow_hidden()
511                                .rounded_tr_sm()
512                                .rounded_br_sm()
513                                .border_1()
514                                .border_color(cx.theme().colors().border_variant)
515                                .bg(cx.theme().colors().surface_background)
516                                .hover(|s| s.bg(cx.theme().colors().element_hover))
517                                .child(Icon::new(IconName::Plus).size(IconSize::Small))
518                                .when_some(tab_index.as_mut(), |this, tab_index| {
519                                    *tab_index += 1;
520                                    this.tab_index(*tab_index - 1).focus(|style| {
521                                        style
522                                            .border_color(cx.theme().colors().border_focused)
523                                            .bg(cx.theme().colors().element_hover)
524                                    })
525                                })
526                                .on_click(increment_handler),
527                        )
528                    }),
529            )
530    }
531}
532
533impl Component for NumberField<usize> {
534    fn scope() -> ComponentScope {
535        ComponentScope::Input
536    }
537
538    fn name() -> &'static str {
539        "Number Field"
540    }
541
542    fn sort_name() -> &'static str {
543        Self::name()
544    }
545
546    fn description() -> Option<&'static str> {
547        Some("A numeric input element with increment and decrement buttons.")
548    }
549
550    fn preview(window: &mut Window, cx: &mut App) -> Option<AnyElement> {
551        let stepper_example = window.use_state(cx, |_, _| 100.0);
552
553        Some(
554            v_flex()
555                .gap_6()
556                .children(vec![single_example(
557                    "Default Numeric Stepper",
558                    NumberField::new(
559                        "numeric-stepper-component-preview",
560                        *stepper_example.read(cx),
561                        window,
562                        cx,
563                    )
564                    .on_change({
565                        let stepper_example = stepper_example.clone();
566                        move |value, _, cx| stepper_example.write(cx, *value)
567                    })
568                    .min(1.0)
569                    .max(100.0)
570                    .into_any_element(),
571                )])
572                .into_any_element(),
573        )
574    }
575}