number_field.rs

  1use std::{
  2    fmt::Display,
  3    num::{NonZero, NonZeroU32, NonZeroU64},
  4    rc::Rc,
  5    str::FromStr,
  6};
  7
  8use editor::{Editor, actions::MoveDown, actions::MoveUp};
  9use gpui::{
 10    ClickEvent, Entity, FocusHandle, Focusable, FontWeight, Modifiers, TextAlign,
 11    TextStyleRefinement, WeakEntity,
 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(IntoElement, RegisterComponent)]
242pub struct NumberField<T: NumberFieldType = usize> {
243    id: ElementId,
244    value: T,
245    focus_handle: FocusHandle,
246    mode: Entity<NumberFieldMode>,
247    /// Stores a weak reference to the editor when in edit mode, so buttons can update its text
248    edit_editor: Entity<Option<WeakEntity<Editor>>>,
249    format: Box<dyn FnOnce(&T) -> String>,
250    large_step: T,
251    small_step: T,
252    step: T,
253    min_value: T,
254    max_value: T,
255    on_reset: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
256    on_change: Rc<dyn Fn(&T, &mut Window, &mut App) + 'static>,
257    tab_index: Option<isize>,
258}
259
260impl<T: NumberFieldType> NumberField<T> {
261    pub fn new(id: impl Into<ElementId>, value: T, window: &mut Window, cx: &mut App) -> Self {
262        let id = id.into();
263
264        let (mode, focus_handle, edit_editor) = window.with_id(id.clone(), |window| {
265            let mode = window.use_state(cx, |_, _| NumberFieldMode::default());
266            let focus_handle = window.use_state(cx, |_, cx| cx.focus_handle());
267            let edit_editor = window.use_state(cx, |_, _| None);
268            (mode, focus_handle, edit_editor)
269        });
270
271        Self {
272            id,
273            mode,
274            edit_editor,
275            value,
276            focus_handle: focus_handle.read(cx).clone(),
277            format: Box::new(T::default_format),
278            large_step: T::large_step(),
279            step: T::default_step(),
280            small_step: T::small_step(),
281            min_value: T::min_value(),
282            max_value: T::max_value(),
283            on_reset: None,
284            on_change: Rc::new(|_, _, _| {}),
285            tab_index: None,
286        }
287    }
288
289    pub fn format(mut self, format: impl FnOnce(&T) -> String + 'static) -> Self {
290        self.format = Box::new(format);
291        self
292    }
293
294    pub fn small_step(mut self, step: T) -> Self {
295        self.small_step = step;
296        self
297    }
298
299    pub fn normal_step(mut self, step: T) -> Self {
300        self.step = step;
301        self
302    }
303
304    pub fn large_step(mut self, step: T) -> Self {
305        self.large_step = step;
306        self
307    }
308
309    pub fn min(mut self, min: T) -> Self {
310        self.min_value = min;
311        self
312    }
313
314    pub fn max(mut self, max: T) -> Self {
315        self.max_value = max;
316        self
317    }
318
319    pub fn mode(self, mode: NumberFieldMode, cx: &mut App) -> Self {
320        self.mode.write(cx, mode);
321        self
322    }
323
324    pub fn on_reset(
325        mut self,
326        on_reset: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
327    ) -> Self {
328        self.on_reset = Some(Box::new(on_reset));
329        self
330    }
331
332    pub fn tab_index(mut self, tab_index: isize) -> Self {
333        self.tab_index = Some(tab_index);
334        self
335    }
336
337    pub fn on_change(mut self, on_change: impl Fn(&T, &mut Window, &mut App) + 'static) -> Self {
338        self.on_change = Rc::new(on_change);
339        self
340    }
341}
342
343#[derive(Clone, Copy)]
344enum ValueChangeDirection {
345    Increment,
346    Decrement,
347}
348
349impl<T: NumberFieldType> RenderOnce for NumberField<T> {
350    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
351        let mut tab_index = self.tab_index;
352        let is_edit_mode = matches!(*self.mode.read(cx), NumberFieldMode::Edit);
353
354        let get_step = {
355            let large_step = self.large_step;
356            let step = self.step;
357            let small_step = self.small_step;
358            move |modifiers: Modifiers| -> T {
359                if modifiers.shift {
360                    large_step
361                } else if modifiers.alt {
362                    small_step
363                } else {
364                    step
365                }
366            }
367        };
368
369        let clamp_value = {
370            let min = self.min_value;
371            let max = self.max_value;
372            move |value: T| -> T {
373                if value < min {
374                    min
375                } else if value > max {
376                    max
377                } else {
378                    value
379                }
380            }
381        };
382
383        let change_value = {
384            move |current: T, step: T, direction: ValueChangeDirection| -> T {
385                let new_value = match direction {
386                    ValueChangeDirection::Increment => current.saturating_add(step),
387                    ValueChangeDirection::Decrement => current.saturating_sub(step),
388                };
389                clamp_value(new_value)
390            }
391        };
392
393        let get_current_value = {
394            let value = self.value;
395            let edit_editor = self.edit_editor.clone();
396
397            Rc::new(move |cx: &App| -> T {
398                if !is_edit_mode {
399                    return value;
400                }
401                edit_editor
402                    .read(cx)
403                    .as_ref()
404                    .and_then(|weak| weak.upgrade())
405                    .and_then(|editor| editor.read(cx).text(cx).parse::<T>().ok())
406                    .unwrap_or(value)
407            })
408        };
409
410        let update_editor_text = {
411            let edit_editor = self.edit_editor.clone();
412
413            Rc::new(move |new_value: T, window: &mut Window, cx: &mut App| {
414                if !is_edit_mode {
415                    return;
416                }
417                let Some(editor) = edit_editor
418                    .read(cx)
419                    .as_ref()
420                    .and_then(|weak| weak.upgrade())
421                else {
422                    return;
423                };
424                editor.update(cx, |editor, cx| {
425                    editor.set_text(format!("{}", new_value), window, cx);
426                });
427            })
428        };
429
430        let bg_color = cx.theme().colors().surface_background;
431        let hover_bg_color = cx.theme().colors().element_hover;
432
433        let border_color = cx.theme().colors().border_variant;
434        let focus_border_color = cx.theme().colors().border_focused;
435
436        let base_button = |icon: IconName| {
437            h_flex()
438                .cursor_pointer()
439                .p_1p5()
440                .size_full()
441                .justify_center()
442                .overflow_hidden()
443                .border_1()
444                .border_color(border_color)
445                .bg(bg_color)
446                .hover(|s| s.bg(hover_bg_color))
447                .focus_visible(|s| s.border_color(focus_border_color).bg(hover_bg_color))
448                .child(Icon::new(icon).size(IconSize::Small))
449        };
450
451        h_flex()
452            .id(self.id.clone())
453            .track_focus(&self.focus_handle)
454            .gap_1()
455            .when_some(self.on_reset, |this, on_reset| {
456                this.child(
457                    IconButton::new("reset", IconName::RotateCcw)
458                        .icon_size(IconSize::Small)
459                        .when_some(tab_index.as_mut(), |this, tab_index| {
460                            *tab_index += 1;
461                            this.tab_index(*tab_index - 1)
462                        })
463                        .on_click(on_reset),
464                )
465            })
466            .child(
467                h_flex()
468                    .map(|decrement| {
469                        let decrement_handler = {
470                            let on_change = self.on_change.clone();
471                            let get_current_value = get_current_value.clone();
472                            let update_editor_text = update_editor_text.clone();
473
474                            move |click: &ClickEvent, window: &mut Window, cx: &mut App| {
475                                let current_value = get_current_value(cx);
476                                let step = get_step(click.modifiers());
477                                let new_value = change_value(
478                                    current_value,
479                                    step,
480                                    ValueChangeDirection::Decrement,
481                                );
482
483                                update_editor_text(new_value, window, cx);
484                                on_change(&new_value, window, cx);
485                            }
486                        };
487
488                        decrement.child(
489                            base_button(IconName::Dash)
490                                .id("decrement_button")
491                                .rounded_tl_sm()
492                                .rounded_bl_sm()
493                                .tab_index(
494                                    tab_index
495                                        .as_mut()
496                                        .map(|tab_index| {
497                                            *tab_index += 1;
498                                            *tab_index - 1
499                                        })
500                                        .unwrap_or(0),
501                                )
502                                .on_click(decrement_handler),
503                        )
504                    })
505                    .child(
506                        h_flex()
507                            .min_w_16()
508                            .size_full()
509                            .border_y_1()
510                            .border_color(border_color)
511                            .bg(bg_color)
512                            .in_focus(|this| this.border_color(focus_border_color))
513                            .child(match *self.mode.read(cx) {
514                                NumberFieldMode::Read => h_flex()
515                                    .px_1()
516                                    .flex_1()
517                                    .justify_center()
518                                    .child(Label::new((self.format)(&self.value)))
519                                    .into_any_element(),
520                                NumberFieldMode::Edit => h_flex()
521                                    .flex_1()
522                                    .child(window.use_state(cx, {
523                                        |window, cx| {
524                                            let mut editor = Editor::single_line(window, cx);
525
526                                            editor.set_text_style_refinement(TextStyleRefinement {
527                                                text_align: Some(TextAlign::Center),
528                                                ..Default::default()
529                                            });
530
531                                            editor.set_text(format!("{}", self.value), window, cx);
532
533                                            let editor_weak = cx.entity().downgrade();
534
535                                            self.edit_editor.update(cx, |state, _| {
536                                                *state = Some(editor_weak);
537                                            });
538
539                                            editor
540                                                .register_action::<MoveUp>({
541                                                    let on_change = self.on_change.clone();
542                                                    let editor_handle = cx.entity().downgrade();
543                                                    move |_, window, cx| {
544                                                        let Some(editor) = editor_handle.upgrade()
545                                                        else {
546                                                            return;
547                                                        };
548                                                        editor.update(cx, |editor, cx| {
549                                                            if let Ok(current_value) =
550                                                                editor.text(cx).parse::<T>()
551                                                            {
552                                                                let step =
553                                                                    get_step(window.modifiers());
554                                                                let new_value = change_value(
555                                                                    current_value,
556                                                                    step,
557                                                                    ValueChangeDirection::Increment,
558                                                                );
559                                                                editor.set_text(
560                                                                    format!("{}", new_value),
561                                                                    window,
562                                                                    cx,
563                                                                );
564                                                                on_change(&new_value, window, cx);
565                                                            }
566                                                        });
567                                                    }
568                                                })
569                                                .detach();
570
571                                            editor
572                                                .register_action::<MoveDown>({
573                                                    let on_change = self.on_change.clone();
574                                                    let editor_handle = cx.entity().downgrade();
575                                                    move |_, window, cx| {
576                                                        let Some(editor) = editor_handle.upgrade()
577                                                        else {
578                                                            return;
579                                                        };
580                                                        editor.update(cx, |editor, cx| {
581                                                            if let Ok(current_value) =
582                                                                editor.text(cx).parse::<T>()
583                                                            {
584                                                                let step =
585                                                                    get_step(window.modifiers());
586                                                                let new_value = change_value(
587                                                                    current_value,
588                                                                    step,
589                                                                    ValueChangeDirection::Decrement,
590                                                                );
591                                                                editor.set_text(
592                                                                    format!("{}", new_value),
593                                                                    window,
594                                                                    cx,
595                                                                );
596                                                                on_change(&new_value, window, cx);
597                                                            }
598                                                        });
599                                                    }
600                                                })
601                                                .detach();
602
603                                            cx.on_focus_out(&editor.focus_handle(cx), window, {
604                                                let mode = self.mode.clone();
605                                                let on_change = self.on_change.clone();
606                                                move |this, _, window, cx| {
607                                                    if let Ok(parsed_value) =
608                                                        this.text(cx).parse::<T>()
609                                                    {
610                                                        let new_value = clamp_value(parsed_value);
611                                                        on_change(&new_value, window, cx);
612                                                    };
613                                                    mode.write(cx, NumberFieldMode::Read);
614                                                }
615                                            })
616                                            .detach();
617
618                                            window.focus(&editor.focus_handle(cx), cx);
619
620                                            editor
621                                        }
622                                    }))
623                                    .on_action::<menu::Confirm>({
624                                        move |_, window, _| {
625                                            window.blur();
626                                        }
627                                    })
628                                    .into_any_element(),
629                            }),
630                    )
631                    .map(|increment| {
632                        let increment_handler = {
633                            let on_change = self.on_change.clone();
634                            let get_current_value = get_current_value.clone();
635                            let update_editor_text = update_editor_text.clone();
636
637                            move |click: &ClickEvent, window: &mut Window, cx: &mut App| {
638                                let current_value = get_current_value(cx);
639                                let step = get_step(click.modifiers());
640                                let new_value = change_value(
641                                    current_value,
642                                    step,
643                                    ValueChangeDirection::Increment,
644                                );
645
646                                update_editor_text(new_value, window, cx);
647                                on_change(&new_value, window, cx);
648                            }
649                        };
650
651                        increment.child(
652                            base_button(IconName::Plus)
653                                .id("increment_button")
654                                .rounded_tr_sm()
655                                .rounded_br_sm()
656                                .tab_index(
657                                    tab_index
658                                        .as_mut()
659                                        .map(|tab_index| {
660                                            *tab_index += 1;
661                                            *tab_index - 1
662                                        })
663                                        .unwrap_or(0),
664                                )
665                                .on_click(increment_handler),
666                        )
667                    }),
668            )
669    }
670}
671
672impl Component for NumberField<usize> {
673    fn scope() -> ComponentScope {
674        ComponentScope::Input
675    }
676
677    fn name() -> &'static str {
678        "Number Field"
679    }
680
681    fn description() -> Option<&'static str> {
682        Some("A numeric input element with increment and decrement buttons.")
683    }
684
685    fn preview(window: &mut Window, cx: &mut App) -> Option<AnyElement> {
686        let default_ex = window.use_state(cx, |_, _| 100.0);
687        let edit_ex = window.use_state(cx, |_, _| 500.0);
688
689        Some(
690            v_flex()
691                .gap_6()
692                .children(vec![
693                    single_example(
694                        "Button-Only Number Field",
695                        NumberField::new("number-field", *default_ex.read(cx), window, cx)
696                            .on_change({
697                                let default_ex = default_ex.clone();
698                                move |value, _, cx| default_ex.write(cx, *value)
699                            })
700                            .min(1.0)
701                            .max(100.0)
702                            .into_any_element(),
703                    ),
704                    single_example(
705                        "Editable Number Field",
706                        NumberField::new("editable-number-field", *edit_ex.read(cx), window, cx)
707                            .on_change({
708                                let edit_ex = edit_ex.clone();
709                                move |value, _, cx| edit_ex.write(cx, *value)
710                            })
711                            .min(100.0)
712                            .max(500.0)
713                            .mode(NumberFieldMode::Edit, cx)
714                            .into_any_element(),
715                    ),
716                ])
717                .into_any_element(),
718        )
719    }
720}