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