From a5540a08fb7ea3f6a94934e599eb843a8496d38d Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Sat, 20 Dec 2025 11:15:46 -0300 Subject: [PATCH] ui: Make the NumberField in edit mode work (#45447) - 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 --- crates/ui_input/src/number_field.rs | 255 ++++++++++++++++++++-------- 1 file changed, 187 insertions(+), 68 deletions(-) diff --git a/crates/ui_input/src/number_field.rs b/crates/ui_input/src/number_field.rs index 389f61c748615da162c04182e855459f62d3d226..b07cc5d9dc17dc6c761d02222807101d19cecc33 100644 --- a/crates/ui_input/src/number_field.rs +++ b/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); -#[derive(RegisterComponent)] -pub struct NumberField { +#[derive(IntoElement, RegisterComponent)] +pub struct NumberField { id: ElementId, value: T, focus_handle: FocusHandle, mode: Entity, + /// Stores a weak reference to the editor when in edit mode, so buttons can update its text + edit_editor: Entity>>, format: Box String>, large_step: T, small_step: T, @@ -259,15 +261,17 @@ impl NumberField { pub fn new(id: impl Into, 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 NumberField { } } -impl IntoElement for NumberField { - type Element = gpui::Component; - - fn into_element(self) -> Self::Element { - gpui::Component::new(self) - } +#[derive(Clone, Copy)] +enum ValueChangeDirection { + Increment, + Decrement, } impl RenderOnce for NumberField { 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 RenderOnce for NumberField { } }; + 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::().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 RenderOnce for NumberField { 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 RenderOnce for NumberField { .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 RenderOnce for NumberField { }); 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::({ + 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::() + { + 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::({ + 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::() + { + 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::() { - 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 RenderOnce for NumberField { ) .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 { "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 { - 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(),