ui: Make the NumberField in edit mode work (#45447)

Danilo Leal created

- Make the buttons capable of changing the editor's content
(incrementing or decrementing the value)
- Make arrow key up and down increment and decrement the editor value
- Tried to apply a bit of DRY here by creating some functions that can
be reused across the buttons and editor given they all essentially do
the same thing (change the value)
- Fixed an issue where the editor would not allow focus to move
elsewhere, making it impossible to open a dropdown, for example, if your
focus was on the number field's editor

Release Notes:

- N/A

Change summary

crates/ui_input/src/number_field.rs | 255 ++++++++++++++++++++++--------
1 file changed, 187 insertions(+), 68 deletions(-)

Detailed changes

crates/ui_input/src/number_field.rs 🔗

@@ -5,10 +5,10 @@ use std::{
     str::FromStr,
 };
 
-use editor::Editor;
+use editor::{Editor, actions::MoveDown, actions::MoveUp};
 use gpui::{
     ClickEvent, Entity, FocusHandle, Focusable, FontWeight, Modifiers, TextAlign,
-    TextStyleRefinement,
+    TextStyleRefinement, WeakEntity,
 };
 
 use settings::{CenteredPaddingSettings, CodeFade, DelayMs, InactiveOpacity, MinimumContrast};
@@ -238,12 +238,14 @@ impl_numeric_stepper_nonzero_int!(NonZeroU32, u32);
 impl_numeric_stepper_nonzero_int!(NonZeroU64, u64);
 impl_numeric_stepper_nonzero_int!(NonZero<usize>, usize);
 
-#[derive(RegisterComponent)]
-pub struct NumberField<T = usize> {
+#[derive(IntoElement, RegisterComponent)]
+pub struct NumberField<T: NumberFieldType = usize> {
     id: ElementId,
     value: T,
     focus_handle: FocusHandle,
     mode: Entity<NumberFieldMode>,
+    /// Stores a weak reference to the editor when in edit mode, so buttons can update its text
+    edit_editor: Entity<Option<WeakEntity<Editor>>>,
     format: Box<dyn FnOnce(&T) -> String>,
     large_step: T,
     small_step: T,
@@ -259,15 +261,17 @@ impl<T: NumberFieldType> NumberField<T> {
     pub fn new(id: impl Into<ElementId>, value: T, window: &mut Window, cx: &mut App) -> Self {
         let id = id.into();
 
-        let (mode, focus_handle) = window.with_id(id.clone(), |window| {
+        let (mode, focus_handle, edit_editor) = window.with_id(id.clone(), |window| {
             let mode = window.use_state(cx, |_, _| NumberFieldMode::default());
             let focus_handle = window.use_state(cx, |_, cx| cx.focus_handle());
-            (mode, focus_handle)
+            let edit_editor = window.use_state(cx, |_, _| None);
+            (mode, focus_handle, edit_editor)
         });
 
         Self {
             id,
             mode,
+            edit_editor,
             value,
             focus_handle: focus_handle.read(cx).clone(),
             format: Box::new(T::default_format),
@@ -336,17 +340,16 @@ impl<T: NumberFieldType> NumberField<T> {
     }
 }
 
-impl<T: NumberFieldType> IntoElement for NumberField<T> {
-    type Element = gpui::Component<Self>;
-
-    fn into_element(self) -> Self::Element {
-        gpui::Component::new(self)
-    }
+#[derive(Clone, Copy)]
+enum ValueChangeDirection {
+    Increment,
+    Decrement,
 }
 
 impl<T: NumberFieldType> RenderOnce for NumberField<T> {
     fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
         let mut tab_index = self.tab_index;
+        let is_edit_mode = matches!(*self.mode.read(cx), NumberFieldMode::Edit);
 
         let get_step = {
             let large_step = self.large_step;
@@ -363,6 +366,67 @@ impl<T: NumberFieldType> RenderOnce for NumberField<T> {
             }
         };
 
+        let clamp_value = {
+            let min = self.min_value;
+            let max = self.max_value;
+            move |value: T| -> T {
+                if value < min {
+                    min
+                } else if value > max {
+                    max
+                } else {
+                    value
+                }
+            }
+        };
+
+        let change_value = {
+            move |current: T, step: T, direction: ValueChangeDirection| -> T {
+                let new_value = match direction {
+                    ValueChangeDirection::Increment => current.saturating_add(step),
+                    ValueChangeDirection::Decrement => current.saturating_sub(step),
+                };
+                clamp_value(new_value)
+            }
+        };
+
+        let get_current_value = {
+            let value = self.value;
+            let edit_editor = self.edit_editor.clone();
+
+            Rc::new(move |cx: &App| -> T {
+                if !is_edit_mode {
+                    return value;
+                }
+                edit_editor
+                    .read(cx)
+                    .as_ref()
+                    .and_then(|weak| weak.upgrade())
+                    .and_then(|editor| editor.read(cx).text(cx).parse::<T>().ok())
+                    .unwrap_or(value)
+            })
+        };
+
+        let update_editor_text = {
+            let edit_editor = self.edit_editor.clone();
+
+            Rc::new(move |new_value: T, window: &mut Window, cx: &mut App| {
+                if !is_edit_mode {
+                    return;
+                }
+                let Some(editor) = edit_editor
+                    .read(cx)
+                    .as_ref()
+                    .and_then(|weak| weak.upgrade())
+                else {
+                    return;
+                };
+                editor.update(cx, |editor, cx| {
+                    editor.set_text(format!("{}", new_value), window, cx);
+                });
+            })
+        };
+
         let bg_color = cx.theme().colors().surface_background;
         let hover_bg_color = cx.theme().colors().element_hover;
 
@@ -403,13 +467,20 @@ impl<T: NumberFieldType> RenderOnce for NumberField<T> {
                 h_flex()
                     .map(|decrement| {
                         let decrement_handler = {
-                            let value = self.value;
                             let on_change = self.on_change.clone();
-                            let min = self.min_value;
+                            let get_current_value = get_current_value.clone();
+                            let update_editor_text = update_editor_text.clone();
+
                             move |click: &ClickEvent, window: &mut Window, cx: &mut App| {
+                                let current_value = get_current_value(cx);
                                 let step = get_step(click.modifiers());
-                                let new_value = value.saturating_sub(step);
-                                let new_value = if new_value < min { min } else { new_value };
+                                let new_value = change_value(
+                                    current_value,
+                                    step,
+                                    ValueChangeDirection::Decrement,
+                                );
+
+                                update_editor_text(new_value, window, cx);
                                 on_change(&new_value, window, cx);
                             }
                         };
@@ -446,18 +517,10 @@ impl<T: NumberFieldType> RenderOnce for NumberField<T> {
                                     .justify_center()
                                     .child(Label::new((self.format)(&self.value)))
                                     .into_any_element(),
-                                // Edit mode is disabled until we implement center text alignment for editor
-                                // mode.write(cx, NumberFieldMode::Edit);
-                                //
-                                // When we get to making Edit mode work, we shouldn't even focus the decrement/increment buttons.
-                                // Focus should go instead straight to the editor, avoiding any double-step focus.
-                                // In this world, the buttons become a mouse-only interaction, given users should be able
-                                // to do everything they'd do with the buttons straight in the editor anyway.
                                 NumberFieldMode::Edit => h_flex()
                                     .flex_1()
                                     .child(window.use_state(cx, {
                                         |window, cx| {
-                                            let previous_focus_handle = window.focused(cx);
                                             let mut editor = Editor::single_line(window, cx);
 
                                             editor.set_text_style_refinement(TextStyleRefinement {
@@ -466,28 +529,85 @@ impl<T: NumberFieldType> RenderOnce for NumberField<T> {
                                             });
 
                                             editor.set_text(format!("{}", self.value), window, cx);
+
+                                            let editor_weak = cx.entity().downgrade();
+
+                                            self.edit_editor.update(cx, |state, _| {
+                                                *state = Some(editor_weak);
+                                            });
+
+                                            editor
+                                                .register_action::<MoveUp>({
+                                                    let on_change = self.on_change.clone();
+                                                    let editor_handle = cx.entity().downgrade();
+                                                    move |_, window, cx| {
+                                                        let Some(editor) = editor_handle.upgrade()
+                                                        else {
+                                                            return;
+                                                        };
+                                                        editor.update(cx, |editor, cx| {
+                                                            if let Ok(current_value) =
+                                                                editor.text(cx).parse::<T>()
+                                                            {
+                                                                let step =
+                                                                    get_step(window.modifiers());
+                                                                let new_value = change_value(
+                                                                    current_value,
+                                                                    step,
+                                                                    ValueChangeDirection::Increment,
+                                                                );
+                                                                editor.set_text(
+                                                                    format!("{}", new_value),
+                                                                    window,
+                                                                    cx,
+                                                                );
+                                                                on_change(&new_value, window, cx);
+                                                            }
+                                                        });
+                                                    }
+                                                })
+                                                .detach();
+
+                                            editor
+                                                .register_action::<MoveDown>({
+                                                    let on_change = self.on_change.clone();
+                                                    let editor_handle = cx.entity().downgrade();
+                                                    move |_, window, cx| {
+                                                        let Some(editor) = editor_handle.upgrade()
+                                                        else {
+                                                            return;
+                                                        };
+                                                        editor.update(cx, |editor, cx| {
+                                                            if let Ok(current_value) =
+                                                                editor.text(cx).parse::<T>()
+                                                            {
+                                                                let step =
+                                                                    get_step(window.modifiers());
+                                                                let new_value = change_value(
+                                                                    current_value,
+                                                                    step,
+                                                                    ValueChangeDirection::Decrement,
+                                                                );
+                                                                editor.set_text(
+                                                                    format!("{}", new_value),
+                                                                    window,
+                                                                    cx,
+                                                                );
+                                                                on_change(&new_value, window, cx);
+                                                            }
+                                                        });
+                                                    }
+                                                })
+                                                .detach();
+
                                             cx.on_focus_out(&editor.focus_handle(cx), window, {
                                                 let mode = self.mode.clone();
-                                                let min = self.min_value;
-                                                let max = self.max_value;
                                                 let on_change = self.on_change.clone();
                                                 move |this, _, window, cx| {
-                                                    if let Ok(new_value) =
+                                                    if let Ok(parsed_value) =
                                                         this.text(cx).parse::<T>()
                                                     {
-                                                        let new_value = if new_value < min {
-                                                            min
-                                                        } else if new_value > max {
-                                                            max
-                                                        } else {
-                                                            new_value
-                                                        };
-
-                                                        if let Some(previous) =
-                                                            previous_focus_handle.as_ref()
-                                                        {
-                                                            window.focus(previous, cx);
-                                                        }
+                                                        let new_value = clamp_value(parsed_value);
                                                         on_change(&new_value, window, cx);
                                                     };
                                                     mode.write(cx, NumberFieldMode::Read);
@@ -510,13 +630,20 @@ impl<T: NumberFieldType> RenderOnce for NumberField<T> {
                     )
                     .map(|increment| {
                         let increment_handler = {
-                            let value = self.value;
                             let on_change = self.on_change.clone();
-                            let max = self.max_value;
+                            let get_current_value = get_current_value.clone();
+                            let update_editor_text = update_editor_text.clone();
+
                             move |click: &ClickEvent, window: &mut Window, cx: &mut App| {
+                                let current_value = get_current_value(cx);
                                 let step = get_step(click.modifiers());
-                                let new_value = value.saturating_add(step);
-                                let new_value = if new_value > max { max } else { new_value };
+                                let new_value = change_value(
+                                    current_value,
+                                    step,
+                                    ValueChangeDirection::Increment,
+                                );
+
+                                update_editor_text(new_value, window, cx);
                                 on_change(&new_value, window, cx);
                             }
                         };
@@ -551,48 +678,40 @@ impl Component for NumberField<usize> {
         "Number Field"
     }
 
-    fn sort_name() -> &'static str {
-        Self::name()
-    }
-
     fn description() -> Option<&'static str> {
         Some("A numeric input element with increment and decrement buttons.")
     }
 
     fn preview(window: &mut Window, cx: &mut App) -> Option<AnyElement> {
-        let stepper_example = window.use_state(cx, |_, _| 100.0);
+        let default_ex = window.use_state(cx, |_, _| 100.0);
+        let edit_ex = window.use_state(cx, |_, _| 500.0);
 
         Some(
             v_flex()
                 .gap_6()
                 .children(vec![
                     single_example(
-                        "Default Number Field",
-                        NumberField::new("number-field", *stepper_example.read(cx), window, cx)
+                        "Button-Only Number Field",
+                        NumberField::new("number-field", *default_ex.read(cx), window, cx)
                             .on_change({
-                                let stepper_example = stepper_example.clone();
-                                move |value, _, cx| stepper_example.write(cx, *value)
+                                let default_ex = default_ex.clone();
+                                move |value, _, cx| default_ex.write(cx, *value)
                             })
                             .min(1.0)
                             .max(100.0)
                             .into_any_element(),
                     ),
                     single_example(
-                        "Read-Only Number Field",
-                        NumberField::new(
-                            "editable-number-field",
-                            *stepper_example.read(cx),
-                            window,
-                            cx,
-                        )
-                        .on_change({
-                            let stepper_example = stepper_example.clone();
-                            move |value, _, cx| stepper_example.write(cx, *value)
-                        })
-                        .min(1.0)
-                        .max(100.0)
-                        .mode(NumberFieldMode::Edit, cx)
-                        .into_any_element(),
+                        "Editable Number Field",
+                        NumberField::new("editable-number-field", *edit_ex.read(cx), window, cx)
+                            .on_change({
+                                let edit_ex = edit_ex.clone();
+                                move |value, _, cx| edit_ex.write(cx, *value)
+                            })
+                            .min(100.0)
+                            .max(500.0)
+                            .mode(NumberFieldMode::Edit, cx)
+                            .into_any_element(),
                     ),
                 ])
                 .into_any_element(),