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:
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user