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
This commit is contained in:
Danilo Leal
2025-12-20 11:15:46 -03:00
committed by GitHub
parent 3e8c25f5a9
commit a5540a08fb

View File

@@ -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(),