From c5a258e0b8e65cb6696a5447f01f95e6098aa6bb Mon Sep 17 00:00:00 2001 From: Anthony Date: Tue, 19 Aug 2025 14:29:23 -0400 Subject: [PATCH 1/5] Add support for typing in the numeric stepper componenets --- Cargo.lock | 2 + crates/editor/src/editor_settings_controls.rs | 16 +-- crates/onboarding/Cargo.toml | 1 + crates/onboarding/src/editing_page.rs | 20 ++- .../src/appearance_settings_controls.rs | 12 +- crates/ui/src/components.rs | 2 - crates/ui_input/Cargo.toml | 1 + .../src}/numeric_stepper.rs | 131 ++++++++++++++++-- crates/ui_input/src/ui_input.rs | 2 + 9 files changed, 156 insertions(+), 31 deletions(-) rename crates/{ui/src/components => ui_input/src}/numeric_stepper.rs (62%) diff --git a/Cargo.lock b/Cargo.lock index dc9d074f01..6be6d3aea4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11095,6 +11095,7 @@ dependencies = [ "telemetry", "theme", "ui", + "ui_input", "util", "vim_mode_setting", "workspace", @@ -17514,6 +17515,7 @@ dependencies = [ "component", "editor", "gpui", + "menu", "settings", "theme", "ui", diff --git a/crates/editor/src/editor_settings_controls.rs b/crates/editor/src/editor_settings_controls.rs index dc5557b052..3ae08c7821 100644 --- a/crates/editor/src/editor_settings_controls.rs +++ b/crates/editor/src/editor_settings_controls.rs @@ -5,8 +5,7 @@ use project::project_settings::{InlineBlameSettings, ProjectSettings}; use settings::{EditableSettingControl, Settings}; use theme::{FontFamilyCache, FontFamilyName, ThemeSettings}; use ui::{ - CheckboxWithLabel, ContextMenu, DropdownMenu, NumericStepper, SettingsContainer, SettingsGroup, - prelude::*, + CheckboxWithLabel, ContextMenu, DropdownMenu, SettingsContainer, SettingsGroup, prelude::*, }; use crate::EditorSettings; @@ -139,21 +138,12 @@ impl EditableSettingControl for BufferFontSizeControl { impl RenderOnce for BufferFontSizeControl { fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - let value = Self::read(cx); + let _value = Self::read(cx); h_flex() .gap_2() .child(Icon::new(IconName::FontSize)) - .child(NumericStepper::new( - "buffer-font-size", - value.to_string(), - move |_, _, cx| { - Self::write(value - px(1.), cx); - }, - move |_, _, cx| { - Self::write(value + px(1.), cx); - }, - )) + .child(div()) // todo!(Numeric stepper was here) } } diff --git a/crates/onboarding/Cargo.toml b/crates/onboarding/Cargo.toml index 4157be3172..5f157a0ae9 100644 --- a/crates/onboarding/Cargo.toml +++ b/crates/onboarding/Cargo.toml @@ -39,6 +39,7 @@ settings.workspace = true telemetry.workspace = true theme.workspace = true ui.workspace = true +ui_input.workspace = true util.workspace = true vim_mode_setting.workspace = true workspace-hack.workspace = true diff --git a/crates/onboarding/src/editing_page.rs b/crates/onboarding/src/editing_page.rs index d941a0315a..9dfd9356cc 100644 --- a/crates/onboarding/src/editing_page.rs +++ b/crates/onboarding/src/editing_page.rs @@ -12,10 +12,10 @@ use project::project_settings::ProjectSettings; use settings::{Settings as _, update_settings_file}; use theme::{FontFamilyCache, FontFamilyName, ThemeSettings}; use ui::{ - ButtonLike, ListItem, ListItemSpacing, NumericStepper, PopoverMenu, SwitchField, - ToggleButtonGroup, ToggleButtonGroupStyle, ToggleButtonSimple, ToggleState, Tooltip, - prelude::*, + ButtonLike, ListItem, ListItemSpacing, PopoverMenu, SwitchField, ToggleButtonGroup, + ToggleButtonGroupStyle, ToggleButtonSimple, ToggleState, Tooltip, prelude::*, }; +use ui_input::NumericStepper; use crate::{ImportCursorSettings, ImportVsCodeSettings, SettingsImportState}; @@ -350,14 +350,19 @@ fn render_font_customization_section( NumericStepper::new( "ui-font-size", ui_font_size.to_string(), + move |size, cx| { + write_ui_font_size(Pixels::from(size), cx); + }, move |_, _, cx| { write_ui_font_size(ui_font_size - px(1.), cx); }, move |_, _, cx| { write_ui_font_size(ui_font_size + px(1.), cx); }, + window, + cx, ) - .style(ui::NumericStepperStyle::Outlined) + .style(ui_input::NumericStepperStyle::Outlined) .tab_index({ *tab_index += 2; *tab_index - 2 @@ -414,14 +419,19 @@ fn render_font_customization_section( NumericStepper::new( "buffer-font-size", buffer_font_size.to_string(), + move |size, cx| { + write_buffer_font_size(Pixels::from(size), cx); + }, move |_, _, cx| { write_buffer_font_size(buffer_font_size - px(1.), cx); }, move |_, _, cx| { write_buffer_font_size(buffer_font_size + px(1.), cx); }, + window, + cx, ) - .style(ui::NumericStepperStyle::Outlined) + .style(ui_input::NumericStepperStyle::Outlined) .tab_index({ *tab_index += 2; *tab_index - 2 diff --git a/crates/settings_ui/src/appearance_settings_controls.rs b/crates/settings_ui/src/appearance_settings_controls.rs index 141ae13182..e3c04bfa43 100644 --- a/crates/settings_ui/src/appearance_settings_controls.rs +++ b/crates/settings_ui/src/appearance_settings_controls.rs @@ -6,9 +6,10 @@ use theme::{ FontFamilyCache, FontFamilyName, SystemAppearance, ThemeMode, ThemeRegistry, ThemeSettings, }; use ui::{ - CheckboxWithLabel, ContextMenu, DropdownMenu, NumericStepper, SettingsContainer, SettingsGroup, - ToggleButton, prelude::*, + CheckboxWithLabel, ContextMenu, DropdownMenu, SettingsContainer, SettingsGroup, ToggleButton, + prelude::*, }; +use ui_input::NumericStepper; #[derive(IntoElement)] pub struct AppearanceSettingsControls {} @@ -254,7 +255,7 @@ impl EditableSettingControl for UiFontSizeControl { } impl RenderOnce for UiFontSizeControl { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { + fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { let value = Self::read(cx); h_flex() @@ -263,12 +264,17 @@ impl RenderOnce for UiFontSizeControl { .child(NumericStepper::new( "ui-font-size", value.to_string(), + move |size, cx| { + Self::write(Pixels::from(size), cx); + }, move |_, _, cx| { Self::write(value - px(1.), cx); }, move |_, _, cx| { Self::write(value + px(1.), cx); }, + window, + cx, )) } } diff --git a/crates/ui/src/components.rs b/crates/ui/src/components.rs index 486673e733..51a414cc32 100644 --- a/crates/ui/src/components.rs +++ b/crates/ui/src/components.rs @@ -22,7 +22,6 @@ mod list; mod modal; mod navigable; mod notification; -mod numeric_stepper; mod popover; mod popover_menu; mod progress; @@ -65,7 +64,6 @@ pub use list::*; pub use modal::*; pub use navigable::*; pub use notification::*; -pub use numeric_stepper::*; pub use popover::*; pub use popover_menu::*; pub use progress::*; diff --git a/crates/ui_input/Cargo.toml b/crates/ui_input/Cargo.toml index 0f337597f0..97f250c6ae 100644 --- a/crates/ui_input/Cargo.toml +++ b/crates/ui_input/Cargo.toml @@ -15,6 +15,7 @@ path = "src/ui_input.rs" component.workspace = true editor.workspace = true gpui.workspace = true +menu.workspace = true settings.workspace = true theme.workspace = true ui.workspace = true diff --git a/crates/ui/src/components/numeric_stepper.rs b/crates/ui_input/src/numeric_stepper.rs similarity index 62% rename from crates/ui/src/components/numeric_stepper.rs rename to crates/ui_input/src/numeric_stepper.rs index 2ddb86d9a0..9c7f1a7c67 100644 --- a/crates/ui/src/components/numeric_stepper.rs +++ b/crates/ui_input/src/numeric_stepper.rs @@ -1,6 +1,7 @@ -use gpui::ClickEvent; +use editor::Editor; +use gpui::{ClickEvent, Entity, Focusable}; -use crate::{IconButtonShape, prelude::*}; +use ui::{IconButtonShape, prelude::*}; #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] pub enum NumericStepperStyle { @@ -9,11 +10,21 @@ pub enum NumericStepperStyle { Ghost, } +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] +pub enum NumericStepperMode { + #[default] + Read, + Edit, +} + #[derive(IntoElement, RegisterComponent)] pub struct NumericStepper { id: ElementId, value: SharedString, style: NumericStepperStyle, + input_field: Entity, + mode: Entity, + set_value_to: Box, on_decrement: Box, on_increment: Box, /// Whether to reserve space for the reset button. @@ -26,15 +37,61 @@ impl NumericStepper { pub fn new( id: impl Into, value: impl Into, + set_value_to: impl Fn(usize, &mut App) + 'static, on_decrement: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, on_increment: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, + window: &mut Window, + cx: &mut App, ) -> Self { + let id = id.into(); + let value = value.into(); + + let (input_field, mode) = window.with_global_id(id.clone(), |global_id, window| { + // todo! Make sure that using this api is inline and appropriate with the codebase + window.with_element_state::<(Entity, Entity), _>( + global_id, + |mut editor, window| { + let state = editor + .get_or_insert_with(|| { + let mode = cx.new(|_| NumericStepperMode::default()); + let weak_mode = mode.downgrade(); + let editor = cx.new(|cx| { + let editor = Editor::single_line(window, cx); + + cx.on_focus_out( + &editor.focus_handle(cx), + window, + move |this, _, window, cx| { + this.clear(window, cx); + + weak_mode + .update(cx, |mode, _| *mode = NumericStepperMode::Read) + .ok(); + }, + ) + .detach(); + + editor + }); + + (editor, mode) + }) + .clone(); + + (state.clone(), state) + }, + ) + }); + Self { - id: id.into(), - value: value.into(), - style: NumericStepperStyle::default(), + id, + value, + input_field, + mode, + set_value_to: Box::new(set_value_to), on_decrement: Box::new(on_decrement), on_increment: Box::new(on_increment), + style: NumericStepperStyle::default(), reserve_space_for_reset: false, on_reset: None, tab_index: None, @@ -74,7 +131,7 @@ impl RenderOnce for NumericStepper { let mut tab_index = self.tab_index; h_flex() - .id(self.id) + .id(self.id.clone()) .gap_1() .map(|element| { if let Some(on_reset) = self.on_reset { @@ -146,7 +203,59 @@ impl RenderOnce for NumericStepper { ) } }) - .child(Label::new(self.value).mx_3()) + .child(if matches!(self.mode.read(cx), NumericStepperMode::Read) { + div() + .id(SharedString::new(format!( + "numeric_stepper_label{}", + &self.id, + ))) + .child(Label::new(self.value).mx_3()) + .on_click({ + let mode = self.mode.downgrade(); + let input_field_focus_handle = self.input_field.focus_handle(cx); + + move |click, window, cx| { + if click.click_count() == 2 { + mode.update(cx, |mode, _| { + *mode = NumericStepperMode::Edit; + }) + .ok(); + + window.focus(&input_field_focus_handle); + } + } + }) + .into_any_element() + } else { + div() + .child(self.input_field.clone()) + .child("todo!(This should be removed. It's only here to get input_field to render correctly)") + .on_action::({ + let input_field = self.input_field.downgrade(); + let mode = self.mode.downgrade(); + let set_value = self.set_value_to; + + move |_, _, cx| { + input_field + .update(cx, |input_field, cx| { + if let Some(number) = + input_field.text(cx).parse::().ok() + { + set_value(number, cx); + + mode.update(cx, |mode, _| { + *mode = NumericStepperMode::Read + }) + .ok(); + } + }) + .ok(); + } + }) + .w_full() + .mx_3() + .into_any_element() + }) .map(|increment| { if is_outlined { increment.child( @@ -201,7 +310,7 @@ impl Component for NumericStepper { Some("A button used to increment or decrement a numeric value.") } - fn preview(_window: &mut Window, _cx: &mut App) -> Option { + fn preview(window: &mut Window, cx: &mut App) -> Option { Some( v_flex() .gap_6() @@ -213,8 +322,11 @@ impl Component for NumericStepper { NumericStepper::new( "numeric-stepper-component-preview", "10", + move |_, _| {}, move |_, _, _| {}, move |_, _, _| {}, + window, + cx, ) .into_any_element(), ), @@ -223,8 +335,11 @@ impl Component for NumericStepper { NumericStepper::new( "numeric-stepper-with-border-component-preview", "10", + move |_, _| {}, move |_, _, _| {}, move |_, _, _| {}, + window, + cx, ) .style(NumericStepperStyle::Outlined) .into_any_element(), diff --git a/crates/ui_input/src/ui_input.rs b/crates/ui_input/src/ui_input.rs index 1a5bebaf1e..fdb8f54d62 100644 --- a/crates/ui_input/src/ui_input.rs +++ b/crates/ui_input/src/ui_input.rs @@ -4,10 +4,12 @@ //! //! It can't be located in the `ui` crate because it depends on `editor`. //! +mod numeric_stepper; use component::{example_group, single_example}; use editor::{Editor, EditorElement, EditorStyle}; use gpui::{App, Entity, FocusHandle, Focusable, FontStyle, Hsla, TextStyle}; +pub use numeric_stepper::*; use settings::Settings; use theme::ThemeSettings; use ui::prelude::*; From e6f06f14fdb3c2d7bc766fc58506e73bb1343764 Mon Sep 17 00:00:00 2001 From: Anthony Date: Tue, 19 Aug 2025 18:50:12 -0400 Subject: [PATCH 2/5] Refactor Numeric stepper to use generics Users now pass in an entity that can observes T for changes and can react to those changes Co-authored-by: Mikayla Maki --- crates/editor/src/editor_settings_controls.rs | 2 +- crates/gpui/src/window.rs | 4 +- crates/onboarding/src/editing_page.rs | 239 ++++++------ .../src/appearance_settings_controls.rs | 41 ++- crates/ui_input/src/numeric_stepper.rs | 340 ++++++++++-------- .../src/derive_register_component.rs | 1 + 6 files changed, 338 insertions(+), 289 deletions(-) diff --git a/crates/editor/src/editor_settings_controls.rs b/crates/editor/src/editor_settings_controls.rs index 3ae08c7821..2562a94d3d 100644 --- a/crates/editor/src/editor_settings_controls.rs +++ b/crates/editor/src/editor_settings_controls.rs @@ -143,7 +143,7 @@ impl RenderOnce for BufferFontSizeControl { h_flex() .gap_2() .child(Icon::new(IconName::FontSize)) - .child(div()) // todo!(Numeric stepper was here) + .child(div()) // TODO: Re-evaluate this whole crate once settings UI is complete } } diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 62aeb0df11..2597c001d2 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -2504,7 +2504,7 @@ impl Window { &mut self, key: impl Into, cx: &mut App, - init: impl FnOnce(&mut Self, &mut App) -> S, + init: impl FnOnce(&mut Self, &mut Context) -> S, ) -> Entity { let current_view = self.current_view(); self.with_global_id(key.into(), |global_id, window| { @@ -2537,7 +2537,7 @@ impl Window { pub fn use_state( &mut self, cx: &mut App, - init: impl FnOnce(&mut Self, &mut App) -> S, + init: impl FnOnce(&mut Self, &mut Context) -> S, ) -> Entity { self.use_keyed_state( ElementId::CodeLocation(*core::panic::Location::caller()), diff --git a/crates/onboarding/src/editing_page.rs b/crates/onboarding/src/editing_page.rs index 9dfd9356cc..06d20c83be 100644 --- a/crates/onboarding/src/editing_page.rs +++ b/crates/onboarding/src/editing_page.rs @@ -15,7 +15,6 @@ use ui::{ ButtonLike, ListItem, ListItemSpacing, PopoverMenu, SwitchField, ToggleButtonGroup, ToggleButtonGroupStyle, ToggleButtonSimple, ToggleState, Tooltip, prelude::*, }; -use ui_input::NumericStepper; use crate::{ImportCursorSettings, ImportVsCodeSettings, SettingsImportState}; @@ -110,7 +109,7 @@ fn write_ui_font_family(font: SharedString, cx: &mut App) { }); } -fn write_ui_font_size(size: Pixels, cx: &mut App) { +fn _write_ui_font_size(size: Pixels, cx: &mut App) { let fs = ::global(cx); update_settings_file::(fs, cx, move |theme_settings, _| { @@ -118,7 +117,7 @@ fn write_ui_font_size(size: Pixels, cx: &mut App) { }); } -fn write_buffer_font_size(size: Pixels, cx: &mut App) { +fn _write_buffer_font_size(size: Pixels, cx: &mut App) { let fs = ::global(cx); update_settings_file::(fs, cx, move |theme_settings, _| { @@ -278,10 +277,10 @@ fn render_font_customization_section( cx: &mut App, ) -> impl IntoElement { let theme_settings = ThemeSettings::get_global(cx); - let ui_font_size = theme_settings.ui_font_size(cx); + // let ui_font_size = theme_settings.ui_font_size(cx); let ui_font_family = theme_settings.ui_font.family.clone(); let buffer_font_family = theme_settings.buffer_font.family.clone(); - let buffer_font_size = theme_settings.buffer_font_size(cx); + // let buffer_font_size = theme_settings.buffer_font_size(cx); let ui_font_picker = cx.new(|cx| font_picker(ui_font_family.clone(), write_ui_font_family, window, cx)); @@ -307,67 +306,62 @@ fn render_font_customization_section( .gap_1() .child(Label::new("UI Font")) .child( - h_flex() - .w_full() - .justify_between() - .gap_2() - .child( - PopoverMenu::new("ui-font-picker") - .menu({ - let ui_font_picker = ui_font_picker.clone(); - move |_window, _cx| Some(ui_font_picker.clone()) - }) - .trigger( - ButtonLike::new("ui-font-family-button") - .style(ButtonStyle::Outlined) - .size(ButtonSize::Medium) - .full_width() - .tab_index({ - *tab_index += 1; - *tab_index - 1 - }) - .child( - h_flex() - .w_full() - .justify_between() - .child(Label::new(ui_font_family)) - .child( - Icon::new(IconName::ChevronUpDown) - .color(Color::Muted) - .size(IconSize::XSmall), - ), - ), - ) - .full_width(true) - .anchor(gpui::Corner::TopLeft) - .offset(gpui::Point { - x: px(0.0), - y: px(4.0), - }) - .with_handle(ui_font_handle), - ) - .child( - NumericStepper::new( - "ui-font-size", - ui_font_size.to_string(), - move |size, cx| { - write_ui_font_size(Pixels::from(size), cx); - }, - move |_, _, cx| { - write_ui_font_size(ui_font_size - px(1.), cx); - }, - move |_, _, cx| { - write_ui_font_size(ui_font_size + px(1.), cx); - }, - window, - cx, + h_flex().w_full().justify_between().gap_2().child( + PopoverMenu::new("ui-font-picker") + .menu({ + let ui_font_picker = ui_font_picker.clone(); + move |_window, _cx| Some(ui_font_picker.clone()) + }) + .trigger( + ButtonLike::new("ui-font-family-button") + .style(ButtonStyle::Outlined) + .size(ButtonSize::Medium) + .full_width() + .tab_index({ + *tab_index += 1; + *tab_index - 1 + }) + .child( + h_flex() + .w_full() + .justify_between() + .child(Label::new(ui_font_family)) + .child( + Icon::new(IconName::ChevronUpDown) + .color(Color::Muted) + .size(IconSize::XSmall), + ), + ), ) - .style(ui_input::NumericStepperStyle::Outlined) - .tab_index({ - *tab_index += 2; - *tab_index - 2 - }), - ), + .full_width(true) + .anchor(gpui::Corner::TopLeft) + .offset(gpui::Point { + x: px(0.0), + y: px(4.0), + }) + .with_handle(ui_font_handle), + ), // .child( + // NumericStepper::new( + // "ui-font-size", + // ui_font_size.to_string(), + // move |size, cx| { + // write_ui_font_size(Pixels::from(size), cx); + // }, + // move |_, _, cx| { + // write_ui_font_size(ui_font_size - px(1.), cx); + // }, + // move |_, _, cx| { + // write_ui_font_size(ui_font_size + px(1.), cx); + // }, + // window, + // cx, + // ) + // .style(ui_input::NumericStepperStyle::Outlined) + // .tab_index({ + // *tab_index += 2; + // *tab_index - 2 + // }), + // ), ), ) .child( @@ -376,67 +370,62 @@ fn render_font_customization_section( .gap_1() .child(Label::new("Editor Font")) .child( - h_flex() - .w_full() - .justify_between() - .gap_2() - .child( - PopoverMenu::new("buffer-font-picker") - .menu({ - let buffer_font_picker = buffer_font_picker.clone(); - move |_window, _cx| Some(buffer_font_picker.clone()) - }) - .trigger( - ButtonLike::new("buffer-font-family-button") - .style(ButtonStyle::Outlined) - .size(ButtonSize::Medium) - .full_width() - .tab_index({ - *tab_index += 1; - *tab_index - 1 - }) - .child( - h_flex() - .w_full() - .justify_between() - .child(Label::new(buffer_font_family)) - .child( - Icon::new(IconName::ChevronUpDown) - .color(Color::Muted) - .size(IconSize::XSmall), - ), - ), - ) - .full_width(true) - .anchor(gpui::Corner::TopLeft) - .offset(gpui::Point { - x: px(0.0), - y: px(4.0), - }) - .with_handle(buffer_font_handle), - ) - .child( - NumericStepper::new( - "buffer-font-size", - buffer_font_size.to_string(), - move |size, cx| { - write_buffer_font_size(Pixels::from(size), cx); - }, - move |_, _, cx| { - write_buffer_font_size(buffer_font_size - px(1.), cx); - }, - move |_, _, cx| { - write_buffer_font_size(buffer_font_size + px(1.), cx); - }, - window, - cx, + h_flex().w_full().justify_between().gap_2().child( + PopoverMenu::new("buffer-font-picker") + .menu({ + let buffer_font_picker = buffer_font_picker.clone(); + move |_window, _cx| Some(buffer_font_picker.clone()) + }) + .trigger( + ButtonLike::new("buffer-font-family-button") + .style(ButtonStyle::Outlined) + .size(ButtonSize::Medium) + .full_width() + .tab_index({ + *tab_index += 1; + *tab_index - 1 + }) + .child( + h_flex() + .w_full() + .justify_between() + .child(Label::new(buffer_font_family)) + .child( + Icon::new(IconName::ChevronUpDown) + .color(Color::Muted) + .size(IconSize::XSmall), + ), + ), ) - .style(ui_input::NumericStepperStyle::Outlined) - .tab_index({ - *tab_index += 2; - *tab_index - 2 - }), - ), + .full_width(true) + .anchor(gpui::Corner::TopLeft) + .offset(gpui::Point { + x: px(0.0), + y: px(4.0), + }) + .with_handle(buffer_font_handle), + ), // .child( todo! + // NumericStepper::new( + // "buffer-font-size", + // buffer_font_size.to_string(), + // move |size, cx| { + // write_buffer_font_size(Pixels::from(size), cx); + // }, + // move |_, _, cx| { + // write_buffer_font_size(buffer_font_size - px(1.), cx); + // }, + // move |_, _, cx| { + // write_buffer_font_size(buffer_font_size + px(1.), cx); + // }, + // window, + // cx, + // ) + // .style(ui_input::NumericStepperStyle::Outlined) + // .tab_index({ + // *tab_index += 2; + // *tab_index - 2 + // }), + // ), ), ) } diff --git a/crates/settings_ui/src/appearance_settings_controls.rs b/crates/settings_ui/src/appearance_settings_controls.rs index e3c04bfa43..f5b1bc5702 100644 --- a/crates/settings_ui/src/appearance_settings_controls.rs +++ b/crates/settings_ui/src/appearance_settings_controls.rs @@ -9,7 +9,7 @@ use ui::{ CheckboxWithLabel, ContextMenu, DropdownMenu, SettingsContainer, SettingsGroup, ToggleButton, prelude::*, }; -use ui_input::NumericStepper; +// use ui_input::NumericStepper; #[derive(IntoElement)] pub struct AppearanceSettingsControls {} @@ -255,27 +255,26 @@ impl EditableSettingControl for UiFontSizeControl { } impl RenderOnce for UiFontSizeControl { - fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { - let value = Self::read(cx); + fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { + // let value = Self::read(cx); - h_flex() - .gap_2() - .child(Icon::new(IconName::FontSize)) - .child(NumericStepper::new( - "ui-font-size", - value.to_string(), - move |size, cx| { - Self::write(Pixels::from(size), cx); - }, - move |_, _, cx| { - Self::write(value - px(1.), cx); - }, - move |_, _, cx| { - Self::write(value + px(1.), cx); - }, - window, - cx, - )) + h_flex().gap_2().child(Icon::new(IconName::FontSize)) + // TODO: Re-evaluate this whole crate once settings UI project starts + // .child(NumericStepper::new( + // "ui-font-size", + // value.to_string(), + // move |size, cx| { + // Self::write(Pixels::from(size), cx); + // }, + // move |_, _, cx| { + // Self::write(value - px(1.), cx); + // }, + // move |_, _, cx| { + // Self::write(value + px(1.), cx); + // }, + // window, + // cx, + // )) } } diff --git a/crates/ui_input/src/numeric_stepper.rs b/crates/ui_input/src/numeric_stepper.rs index 9c7f1a7c67..f04d1ed759 100644 --- a/crates/ui_input/src/numeric_stepper.rs +++ b/crates/ui_input/src/numeric_stepper.rs @@ -1,5 +1,11 @@ +use std::{ + fmt::Display, + ops::{Add, Sub}, + str::FromStr, +}; + use editor::Editor; -use gpui::{ClickEvent, Entity, Focusable}; +use gpui::{ClickEvent, Entity, FocusHandle, Focusable, Modifiers}; use ui::{IconButtonShape, prelude::*}; @@ -17,94 +23,128 @@ pub enum NumericStepperMode { Edit, } -#[derive(IntoElement, RegisterComponent)] -pub struct NumericStepper { +pub trait NumericStepperType: + Display + Add + Sub + Copy + Clone + Sized + FromStr + 'static +{ + fn default_format(value: &Self) -> String { + format!("{}", value) + } + fn default_step() -> Self; + fn large_step() -> Self; + fn small_step() -> Self; +} + +macro_rules! impl_numeric_stepper_int { + ($type:ident) => { + impl NumericStepperType for $type { + fn default_step() -> Self { + 1 + } + + fn large_step() -> Self { + 10 + } + + fn small_step() -> Self { + 1 + } + } + }; +} + +macro_rules! impl_numeric_stepper_float { + ($type:ident) => { + impl NumericStepperType for $type { + fn default_format(value: &Self) -> String { + format!("{:.2}", value) + } + + fn default_step() -> Self { + 1.0 + } + + fn large_step() -> Self { + 10.0 + } + + fn small_step() -> Self { + 0.1 + } + } + }; +} + +impl_numeric_stepper_float!(f32); +impl_numeric_stepper_float!(f64); +impl_numeric_stepper_int!(isize); +impl_numeric_stepper_int!(usize); +impl_numeric_stepper_int!(i32); +impl_numeric_stepper_int!(u32); +impl_numeric_stepper_int!(i64); +impl_numeric_stepper_int!(u64); + +// TODO: Add a new register component macro to support a specific type when using generics +pub struct NumericStepper { id: ElementId, - value: SharedString, + value: Entity, style: NumericStepperStyle, - input_field: Entity, + focus_handle: FocusHandle, mode: Entity, - set_value_to: Box, - on_decrement: Box, - on_increment: Box, - /// Whether to reserve space for the reset button. - reserve_space_for_reset: bool, + format: Box String>, + large_step: T, + small_step: T, + step: T, on_reset: Option>, tab_index: Option, } -impl NumericStepper { +impl NumericStepper { pub fn new( id: impl Into, - value: impl Into, - set_value_to: impl Fn(usize, &mut App) + 'static, - on_decrement: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, - on_increment: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, + value: Entity, window: &mut Window, cx: &mut App, ) -> Self { let id = id.into(); - let value = value.into(); - - let (input_field, mode) = window.with_global_id(id.clone(), |global_id, window| { - // todo! Make sure that using this api is inline and appropriate with the codebase - window.with_element_state::<(Entity, Entity), _>( - global_id, - |mut editor, window| { - let state = editor - .get_or_insert_with(|| { - let mode = cx.new(|_| NumericStepperMode::default()); - let weak_mode = mode.downgrade(); - let editor = cx.new(|cx| { - let editor = Editor::single_line(window, cx); - - cx.on_focus_out( - &editor.focus_handle(cx), - window, - move |this, _, window, cx| { - this.clear(window, cx); - - weak_mode - .update(cx, |mode, _| *mode = NumericStepperMode::Read) - .ok(); - }, - ) - .detach(); - - editor - }); - - (editor, mode) - }) - .clone(); - - (state.clone(), state) - }, - ) - }); + let mode = window.use_state(cx, |_, _| NumericStepperMode::default()); Self { id, - value, - input_field, + focus_handle: cx.focus_handle(), mode, - set_value_to: Box::new(set_value_to), - on_decrement: Box::new(on_decrement), - on_increment: Box::new(on_increment), + value, style: NumericStepperStyle::default(), - reserve_space_for_reset: false, + format: Box::new(T::default_format), + large_step: T::large_step(), + step: T::default_step(), + small_step: T::small_step(), on_reset: None, tab_index: None, } } - pub fn style(mut self, style: NumericStepperStyle) -> Self { - self.style = style; + pub fn format(mut self, format: impl FnOnce(&T) -> String + 'static) -> Self { + self.format = Box::new(format); self } - pub fn reserve_space_for_reset(mut self, reserve_space_for_reset: bool) -> Self { - self.reserve_space_for_reset = reserve_space_for_reset; + pub fn small_step(mut self, step: T) -> Self { + self.small_step = step; + self + } + + pub fn normal_step(mut self, step: T) -> Self { + self.step = step; + self + } + + pub fn large_step(mut self, step: T) -> Self { + self.large_step = step; + self + } + + pub fn style(mut self, style: NumericStepperStyle) -> Self { + self.style = style; self } @@ -122,7 +162,15 @@ impl NumericStepper { } } -impl RenderOnce for NumericStepper { +impl IntoElement for NumericStepper { + type Element = gpui::Component; + + fn into_element(self) -> Self::Element { + gpui::Component::new(self) + } +} + +impl RenderOnce for NumericStepper { fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { let shape = IconButtonShape::Square; let icon_size = IconSize::Small; @@ -130,31 +178,36 @@ impl RenderOnce for NumericStepper { let is_outlined = matches!(self.style, NumericStepperStyle::Outlined); let mut tab_index = self.tab_index; + let get_step = { + let large_step = self.large_step; + let step = self.step; + let small_step = self.small_step; + move |modifiers: Modifiers| -> T { + if modifiers.shift { + large_step + } else if modifiers.alt { + small_step + } else { + step + } + } + }; + h_flex() .id(self.id.clone()) + .track_focus(&self.focus_handle) .gap_1() - .map(|element| { - if let Some(on_reset) = self.on_reset { - element.child( - IconButton::new("reset", IconName::RotateCcw) - .shape(shape) - .icon_size(icon_size) - .when_some(tab_index.as_mut(), |this, tab_index| { - *tab_index += 1; - this.tab_index(*tab_index - 1) - }) - .on_click(on_reset), - ) - } else if self.reserve_space_for_reset { - element.child( - h_flex() - .size(icon_size.square(window, cx)) - .flex_none() - .into_any_element(), - ) - } else { - element - } + .when_some(self.on_reset, |this, on_reset| { + this.child( + IconButton::new("reset", IconName::RotateCcw) + .shape(shape) + .icon_size(icon_size) + .when_some(tab_index.as_mut(), |this, tab_index| { + *tab_index += 1; + this.tab_index(*tab_index - 1) + }) + .on_click(on_reset), + ) }) .child( h_flex() @@ -171,6 +224,15 @@ impl RenderOnce for NumericStepper { } }) .map(|decrement| { + let decrement_handler = { + let value = self.value.clone(); + move |click: &ClickEvent, _: &mut Window, cx: &mut App| { + let step = get_step(click.modifiers()); + let current_value = *value.read(cx); + value.write(cx, current_value - step); + } + }; + if is_outlined { decrement.child( h_flex() @@ -188,7 +250,7 @@ impl RenderOnce for NumericStepper { style.bg(cx.theme().colors().element_hover) }) }) - .on_click(self.on_decrement), + .on_click(decrement_handler), ) } else { decrement.child( @@ -199,64 +261,66 @@ impl RenderOnce for NumericStepper { *tab_index += 1; this.tab_index(*tab_index - 1) }) - .on_click(self.on_decrement), + .on_click(decrement_handler), ) } }) - .child(if matches!(self.mode.read(cx), NumericStepperMode::Read) { - div() - .id(SharedString::new(format!( - "numeric_stepper_label{}", - &self.id, - ))) - .child(Label::new(self.value).mx_3()) + .child(match *self.mode.read(cx) { + NumericStepperMode::Read => div() + .id("numeric_stepper_label") + .child(Label::new((self.format)(self.value.read(cx))).mx_3()) .on_click({ - let mode = self.mode.downgrade(); - let input_field_focus_handle = self.input_field.focus_handle(cx); + let mode = self.mode.clone(); - move |click, window, cx| { + move |click, _, cx| { if click.click_count() == 2 { - mode.update(cx, |mode, _| { - *mode = NumericStepperMode::Edit; - }) - .ok(); - - window.focus(&input_field_focus_handle); + mode.write(cx, NumericStepperMode::Edit); } } }) - .into_any_element() - } else { - div() - .child(self.input_field.clone()) - .child("todo!(This should be removed. It's only here to get input_field to render correctly)") + .into_any_element(), + NumericStepperMode::Edit => div() + .child(window.use_state(cx, { + |window, cx| { + let mut editor = Editor::single_line(window, cx); + editor.set_text(format!("{}", self.value.read(cx)), window, cx); + cx.on_focus_out(&editor.focus_handle(cx), window, { + let mode = self.mode.clone(); + let value = self.value.clone(); + move |this, _, _window, cx| { + if let Ok(new_value) = this.text(cx).parse::() { + value.write(cx, new_value); + }; + mode.write(cx, NumericStepperMode::Read); + } + }) + .detach(); + + window.focus(&editor.focus_handle(cx)); + + editor + } + })) .on_action::({ - let input_field = self.input_field.downgrade(); - let mode = self.mode.downgrade(); - let set_value = self.set_value_to; - - move |_, _, cx| { - input_field - .update(cx, |input_field, cx| { - if let Some(number) = - input_field.text(cx).parse::().ok() - { - set_value(number, cx); - - mode.update(cx, |mode, _| { - *mode = NumericStepperMode::Read - }) - .ok(); - } - }) - .ok(); + let focus = self.focus_handle.clone(); + move |_, window, _| { + window.focus(&focus); } }) .w_full() .mx_3() - .into_any_element() + .into_any_element(), }) .map(|increment| { + let increment_handler = { + let value = self.value.clone(); + move |click: &ClickEvent, _: &mut Window, cx: &mut App| { + let step = get_step(click.modifiers()); + let current_value = *value.read(cx); + value.write(cx, current_value + step); + } + }; + if is_outlined { increment.child( h_flex() @@ -274,7 +338,7 @@ impl RenderOnce for NumericStepper { style.bg(cx.theme().colors().element_hover) }) }) - .on_click(self.on_increment), + .on_click(increment_handler), ) } else { increment.child( @@ -285,7 +349,7 @@ impl RenderOnce for NumericStepper { *tab_index += 1; this.tab_index(*tab_index - 1) }) - .on_click(self.on_increment), + .on_click(increment_handler), ) } }), @@ -293,7 +357,7 @@ impl RenderOnce for NumericStepper { } } -impl Component for NumericStepper { +impl Component for NumericStepper { fn scope() -> ComponentScope { ComponentScope::Input } @@ -311,6 +375,8 @@ impl Component for NumericStepper { } fn preview(window: &mut Window, cx: &mut App) -> Option { + let first_stepper = window.use_state(cx, |_, _| 10usize); + let second_stepper = window.use_state(cx, |_, _| 10.0); Some( v_flex() .gap_6() @@ -321,10 +387,7 @@ impl Component for NumericStepper { "Default", NumericStepper::new( "numeric-stepper-component-preview", - "10", - move |_, _| {}, - move |_, _, _| {}, - move |_, _, _| {}, + first_stepper, window, cx, ) @@ -334,10 +397,7 @@ impl Component for NumericStepper { "Outlined", NumericStepper::new( "numeric-stepper-with-border-component-preview", - "10", - move |_, _| {}, - move |_, _, _| {}, - move |_, _, _| {}, + second_stepper, window, cx, ) diff --git a/crates/ui_macros/src/derive_register_component.rs b/crates/ui_macros/src/derive_register_component.rs index 27248e2aac..64ab132cc0 100644 --- a/crates/ui_macros/src/derive_register_component.rs +++ b/crates/ui_macros/src/derive_register_component.rs @@ -4,6 +4,7 @@ use syn::{DeriveInput, parse_macro_input}; pub fn derive_register_component(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); + let name = input.ident; let register_fn_name = syn::Ident::new( &format!("__component_registry_internal_register_{}", name), From b775a2e3cb3b6c5290984110320db6e6f111e775 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Tue, 19 Aug 2025 16:52:16 -0700 Subject: [PATCH 3/5] Add min and max values and fix rendering a bit --- .../src/appearance_settings_controls.rs | 2 +- crates/ui_input/src/numeric_stepper.rs | 197 +++++++++++++----- 2 files changed, 144 insertions(+), 55 deletions(-) diff --git a/crates/settings_ui/src/appearance_settings_controls.rs b/crates/settings_ui/src/appearance_settings_controls.rs index f5b1bc5702..c6ccacb65c 100644 --- a/crates/settings_ui/src/appearance_settings_controls.rs +++ b/crates/settings_ui/src/appearance_settings_controls.rs @@ -255,7 +255,7 @@ impl EditableSettingControl for UiFontSizeControl { } impl RenderOnce for UiFontSizeControl { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { + fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { // let value = Self::read(cx); h_flex().gap_2().child(Icon::new(IconName::FontSize)) diff --git a/crates/ui_input/src/numeric_stepper.rs b/crates/ui_input/src/numeric_stepper.rs index f04d1ed759..97c1102302 100644 --- a/crates/ui_input/src/numeric_stepper.rs +++ b/crates/ui_input/src/numeric_stepper.rs @@ -24,7 +24,15 @@ pub enum NumericStepperMode { } pub trait NumericStepperType: - Display + Add + Sub + Copy + Clone + Sized + FromStr + 'static + Display + + Add + + Sub + + Copy + + Clone + + Sized + + PartialOrd + + FromStr + + 'static { fn default_format(value: &Self) -> String { format!("{}", value) @@ -32,6 +40,8 @@ pub trait NumericStepperType: fn default_step() -> Self; fn large_step() -> Self; fn small_step() -> Self; + fn min_value() -> Self; + fn max_value() -> Self; } macro_rules! impl_numeric_stepper_int { @@ -48,6 +58,14 @@ macro_rules! impl_numeric_stepper_int { fn small_step() -> Self { 1 } + + fn min_value() -> Self { + <$type>::MIN + } + + fn max_value() -> Self { + <$type>::MAX + } } }; } @@ -56,7 +74,10 @@ macro_rules! impl_numeric_stepper_float { ($type:ident) => { impl NumericStepperType for $type { fn default_format(value: &Self) -> String { - format!("{:.2}", value) + format!("{:^4}", value) + .trim_end_matches('0') + .trim_end_matches('.') + .to_string() } fn default_step() -> Self { @@ -70,6 +91,14 @@ macro_rules! impl_numeric_stepper_float { fn small_step() -> Self { 0.1 } + + fn min_value() -> Self { + <$type>::MIN + } + + fn max_value() -> Self { + <$type>::MAX + } } }; } @@ -83,8 +112,8 @@ impl_numeric_stepper_int!(u32); impl_numeric_stepper_int!(i64); impl_numeric_stepper_int!(u64); -// TODO: Add a new register component macro to support a specific type when using generics -pub struct NumericStepper { +#[derive(RegisterComponent)] +pub struct NumericStepper { id: ElementId, value: Entity, style: NumericStepperStyle, @@ -94,6 +123,8 @@ pub struct NumericStepper { large_step: T, small_step: T, step: T, + min_value: T, + max_value: T, on_reset: Option>, tab_index: Option, } @@ -106,18 +137,25 @@ impl NumericStepper { cx: &mut App, ) -> Self { let id = id.into(); - let mode = window.use_state(cx, |_, _| NumericStepperMode::default()); + + let (mode, focus_handle) = window.with_id(id.clone(), |window| { + let mode = window.use_state(cx, |_, _| NumericStepperMode::default()); + let focus_handle = window.use_state(cx, |_, cx| cx.focus_handle()); + (mode, focus_handle) + }); Self { id, - focus_handle: cx.focus_handle(), mode, value, + focus_handle: focus_handle.read(cx).clone(), style: NumericStepperStyle::default(), format: Box::new(T::default_format), large_step: T::large_step(), step: T::default_step(), small_step: T::small_step(), + min_value: T::min_value(), + max_value: T::max_value(), on_reset: None, tab_index: None, } @@ -143,6 +181,16 @@ impl NumericStepper { self } + pub fn min(mut self, min: T) -> Self { + self.min_value = min; + self + } + + pub fn max(mut self, max: T) -> Self { + self.max_value = max; + self + } + pub fn style(mut self, style: NumericStepperStyle) -> Self { self.style = style; self @@ -226,10 +274,15 @@ impl RenderOnce for NumericStepper { .map(|decrement| { let decrement_handler = { let value = self.value.clone(); - move |click: &ClickEvent, _: &mut Window, cx: &mut App| { + let focus = self.focus_handle.clone(); + let min = self.min_value; + move |click: &ClickEvent, window: &mut Window, cx: &mut App| { let step = get_step(click.modifiers()); let current_value = *value.read(cx); - value.write(cx, current_value - step); + let new_value = current_value - step; + let new_value = if new_value < min { min } else { new_value }; + value.write(cx, new_value); + window.focus(&focus); } }; @@ -265,59 +318,93 @@ impl RenderOnce for NumericStepper { ) } }) - .child(match *self.mode.read(cx) { - NumericStepperMode::Read => div() - .id("numeric_stepper_label") - .child(Label::new((self.format)(self.value.read(cx))).mx_3()) - .on_click({ - let mode = self.mode.clone(); - - move |click, _, cx| { - if click.click_count() == 2 { - mode.write(cx, NumericStepperMode::Edit); - } - } + .child( + div() + .text_color(gpui::red()) + .in_focus(|this| { + this.border_1() + .border_color(cx.theme().colors().border_focused) }) - .into_any_element(), - NumericStepperMode::Edit => div() - .child(window.use_state(cx, { - |window, cx| { - let mut editor = Editor::single_line(window, cx); - editor.set_text(format!("{}", self.value.read(cx)), window, cx); - cx.on_focus_out(&editor.focus_handle(cx), window, { + .child(match *self.mode.read(cx) { + NumericStepperMode::Read => div() + .id("numeric_stepper_label") + .child(Label::new((self.format)(self.value.read(cx))).mx_3()) + .on_click({ let mode = self.mode.clone(); - let value = self.value.clone(); - move |this, _, _window, cx| { - if let Ok(new_value) = this.text(cx).parse::() { - value.write(cx, new_value); - }; - mode.write(cx, NumericStepperMode::Read); + + move |click, _, cx| { + if click.click_count() == 2 { + mode.write(cx, NumericStepperMode::Edit); + } } }) - .detach(); + .w(px(4.0 * 14.0)) // w_14 + .h_8() + .overflow_scroll() + .into_any_element(), + NumericStepperMode::Edit => div() + .child(window.use_state(cx, { + |window, cx| { + let mut editor = Editor::single_line(window, cx); - window.focus(&editor.focus_handle(cx)); + editor.set_text( + format!("{}", self.value.read(cx)), + window, + cx, + ); + cx.on_focus_out(&editor.focus_handle(cx), window, { + let mode = self.mode.clone(); + let value = self.value.clone(); + let min = self.min_value; + let max = self.max_value; + move |this, _, _window, cx| { + if let Ok(new_value) = + this.text(cx).parse::() + { + let new_value = if new_value < min { + min + } else if new_value > max { + max + } else { + new_value + }; + value.write(cx, new_value); + }; + mode.write(cx, NumericStepperMode::Read); + } + }) + .detach(); - editor - } - })) - .on_action::({ - let focus = self.focus_handle.clone(); - move |_, window, _| { - window.focus(&focus); - } - }) - .w_full() - .mx_3() - .into_any_element(), - }) + window.focus(&editor.focus_handle(cx)); + + editor + } + })) + .on_action::({ + let focus = self.focus_handle.clone(); + move |_, window, _| { + window.focus(&focus); + } + }) + .size_full() + .w(px(4.0 * 14.0)) // w_14 + .h_8() + .mx_3() + .into_any_element(), + }), + ) .map(|increment| { let increment_handler = { let value = self.value.clone(); - move |click: &ClickEvent, _: &mut Window, cx: &mut App| { + let focus = self.focus_handle.clone(); + let max = self.max_value; + move |click: &ClickEvent, window: &mut Window, cx: &mut App| { let step = get_step(click.modifiers()); let current_value = *value.read(cx); - value.write(cx, current_value + step); + let new_value = current_value + step; + let new_value = if new_value > max { max } else { new_value }; + value.write(cx, new_value); + window.focus(&focus); } }; @@ -342,7 +429,7 @@ impl RenderOnce for NumericStepper { ) } else { increment.child( - IconButton::new("increment", IconName::Dash) + IconButton::new("increment", IconName::Plus) .shape(shape) .icon_size(icon_size) .when_some(tab_index.as_mut(), |this, tab_index| { @@ -357,7 +444,7 @@ impl RenderOnce for NumericStepper { } } -impl Component for NumericStepper { +impl Component for NumericStepper { fn scope() -> ComponentScope { ComponentScope::Input } @@ -375,8 +462,8 @@ impl Component for NumericStepper { } fn preview(window: &mut Window, cx: &mut App) -> Option { - let first_stepper = window.use_state(cx, |_, _| 10usize); - let second_stepper = window.use_state(cx, |_, _| 10.0); + let first_stepper = window.use_state(cx, |_, _| 100usize); + let second_stepper = window.use_state(cx, |_, _| 100.0); Some( v_flex() .gap_6() @@ -401,6 +488,8 @@ impl Component for NumericStepper { window, cx, ) + .min(1.0) + .max(100.0) .style(NumericStepperStyle::Outlined) .into_any_element(), ), From e47b9d3b38d928b5472b6eb589efad36609bdf67 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Tue, 19 Aug 2025 21:04:57 -0700 Subject: [PATCH 4/5] Fine tune UX for new numeric stepper --- crates/gpui/src/app/context.rs | 14 ++ crates/gpui/src/window.rs | 6 + crates/onboarding/src/editing_page.rs | 257 ++++++++++++++----------- crates/ui_input/src/numeric_stepper.rs | 56 +++--- 4 files changed, 193 insertions(+), 140 deletions(-) diff --git a/crates/gpui/src/app/context.rs b/crates/gpui/src/app/context.rs index 1112878a66..516f51b8c7 100644 --- a/crates/gpui/src/app/context.rs +++ b/crates/gpui/src/app/context.rs @@ -69,6 +69,20 @@ impl<'a, T: 'static> Context<'a, T> { }) } + /// Observe changes to ourselves + pub fn observe_self( + &mut self, + mut on_event: impl FnMut(&mut T, &mut Context) + 'static, + ) -> Subscription + where + T: 'static, + { + let this = self.entity(); + self.app.observe(&this, move |this, cx| { + this.update(cx, |this, cx| on_event(this, cx)) + }) + } + /// Subscribe to an event type from another entity pub fn subscribe( &mut self, diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 2597c001d2..ec83fc6e30 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -4838,6 +4838,12 @@ impl> From<(ElementId, T)> for ElementId { } } +impl From> for ElementId { + fn from(value: core::panic::Location<'static>) -> Self { + Self::CodeLocation(value) + } +} + /// A rectangle to be rendered in the window at the given position and size. /// Passed as an argument [`Window::paint_quad`]. #[derive(Clone)] diff --git a/crates/onboarding/src/editing_page.rs b/crates/onboarding/src/editing_page.rs index 06d20c83be..ce509ea843 100644 --- a/crates/onboarding/src/editing_page.rs +++ b/crates/onboarding/src/editing_page.rs @@ -15,6 +15,7 @@ use ui::{ ButtonLike, ListItem, ListItemSpacing, PopoverMenu, SwitchField, ToggleButtonGroup, ToggleButtonGroupStyle, ToggleButtonSimple, ToggleState, Tooltip, prelude::*, }; +use ui_input::NumericStepper; use crate::{ImportCursorSettings, ImportVsCodeSettings, SettingsImportState}; @@ -109,7 +110,7 @@ fn write_ui_font_family(font: SharedString, cx: &mut App) { }); } -fn _write_ui_font_size(size: Pixels, cx: &mut App) { +fn write_ui_font_size(size: Pixels, cx: &mut App) { let fs = ::global(cx); update_settings_file::(fs, cx, move |theme_settings, _| { @@ -117,7 +118,7 @@ fn _write_ui_font_size(size: Pixels, cx: &mut App) { }); } -fn _write_buffer_font_size(size: Pixels, cx: &mut App) { +fn write_buffer_font_size(size: Pixels, cx: &mut App) { let fs = ::global(cx); update_settings_file::(fs, cx, move |theme_settings, _| { @@ -277,10 +278,10 @@ fn render_font_customization_section( cx: &mut App, ) -> impl IntoElement { let theme_settings = ThemeSettings::get_global(cx); - // let ui_font_size = theme_settings.ui_font_size(cx); + let ui_font_size = theme_settings.ui_font_size(cx); let ui_font_family = theme_settings.ui_font.family.clone(); let buffer_font_family = theme_settings.buffer_font.family.clone(); - // let buffer_font_size = theme_settings.buffer_font_size(cx); + let buffer_font_size = theme_settings.buffer_font_size(cx); let ui_font_picker = cx.new(|cx| font_picker(ui_font_family.clone(), write_ui_font_family, window, cx)); @@ -306,62 +307,53 @@ fn render_font_customization_section( .gap_1() .child(Label::new("UI Font")) .child( - h_flex().w_full().justify_between().gap_2().child( - PopoverMenu::new("ui-font-picker") - .menu({ - let ui_font_picker = ui_font_picker.clone(); - move |_window, _cx| Some(ui_font_picker.clone()) - }) - .trigger( - ButtonLike::new("ui-font-family-button") - .style(ButtonStyle::Outlined) - .size(ButtonSize::Medium) - .full_width() - .tab_index({ - *tab_index += 1; - *tab_index - 1 - }) - .child( - h_flex() - .w_full() - .justify_between() - .child(Label::new(ui_font_family)) - .child( - Icon::new(IconName::ChevronUpDown) - .color(Color::Muted) - .size(IconSize::XSmall), - ), - ), - ) - .full_width(true) - .anchor(gpui::Corner::TopLeft) - .offset(gpui::Point { - x: px(0.0), - y: px(4.0), - }) - .with_handle(ui_font_handle), - ), // .child( - // NumericStepper::new( - // "ui-font-size", - // ui_font_size.to_string(), - // move |size, cx| { - // write_ui_font_size(Pixels::from(size), cx); - // }, - // move |_, _, cx| { - // write_ui_font_size(ui_font_size - px(1.), cx); - // }, - // move |_, _, cx| { - // write_ui_font_size(ui_font_size + px(1.), cx); - // }, - // window, - // cx, - // ) - // .style(ui_input::NumericStepperStyle::Outlined) - // .tab_index({ - // *tab_index += 2; - // *tab_index - 2 - // }), - // ), + h_flex() + .w_full() + .justify_between() + .gap_2() + .child( + PopoverMenu::new("ui-font-picker") + .menu({ + let ui_font_picker = ui_font_picker.clone(); + move |_window, _cx| Some(ui_font_picker.clone()) + }) + .trigger( + ButtonLike::new("ui-font-family-button") + .style(ButtonStyle::Outlined) + .size(ButtonSize::Medium) + .full_width() + .tab_index({ + *tab_index += 1; + *tab_index - 1 + }) + .child( + h_flex() + .w_full() + .justify_between() + .child(Label::new(ui_font_family)) + .child( + Icon::new(IconName::ChevronUpDown) + .color(Color::Muted) + .size(IconSize::XSmall), + ), + ), + ) + .full_width(true) + .anchor(gpui::Corner::TopLeft) + .offset(gpui::Point { + x: px(0.0), + y: px(4.0), + }) + .with_handle(ui_font_handle), + ) + .child(font_picker_stepper( + "ui-font-size", + &ui_font_size, + tab_index, + write_ui_font_size, + window, + cx, + )), ), ) .child( @@ -370,66 +362,99 @@ fn render_font_customization_section( .gap_1() .child(Label::new("Editor Font")) .child( - h_flex().w_full().justify_between().gap_2().child( - PopoverMenu::new("buffer-font-picker") - .menu({ - let buffer_font_picker = buffer_font_picker.clone(); - move |_window, _cx| Some(buffer_font_picker.clone()) - }) - .trigger( - ButtonLike::new("buffer-font-family-button") - .style(ButtonStyle::Outlined) - .size(ButtonSize::Medium) - .full_width() - .tab_index({ - *tab_index += 1; - *tab_index - 1 - }) - .child( - h_flex() - .w_full() - .justify_between() - .child(Label::new(buffer_font_family)) - .child( - Icon::new(IconName::ChevronUpDown) - .color(Color::Muted) - .size(IconSize::XSmall), - ), - ), - ) - .full_width(true) - .anchor(gpui::Corner::TopLeft) - .offset(gpui::Point { - x: px(0.0), - y: px(4.0), - }) - .with_handle(buffer_font_handle), - ), // .child( todo! - // NumericStepper::new( - // "buffer-font-size", - // buffer_font_size.to_string(), - // move |size, cx| { - // write_buffer_font_size(Pixels::from(size), cx); - // }, - // move |_, _, cx| { - // write_buffer_font_size(buffer_font_size - px(1.), cx); - // }, - // move |_, _, cx| { - // write_buffer_font_size(buffer_font_size + px(1.), cx); - // }, - // window, - // cx, - // ) - // .style(ui_input::NumericStepperStyle::Outlined) - // .tab_index({ - // *tab_index += 2; - // *tab_index - 2 - // }), - // ), + h_flex() + .w_full() + .justify_between() + .gap_2() + .child( + PopoverMenu::new("buffer-font-picker") + .menu({ + let buffer_font_picker = buffer_font_picker.clone(); + move |_window, _cx| Some(buffer_font_picker.clone()) + }) + .trigger( + ButtonLike::new("buffer-font-family-button") + .style(ButtonStyle::Outlined) + .size(ButtonSize::Medium) + .full_width() + .tab_index({ + *tab_index += 1; + *tab_index - 1 + }) + .child( + h_flex() + .w_full() + .justify_between() + .child(Label::new(buffer_font_family)) + .child( + Icon::new(IconName::ChevronUpDown) + .color(Color::Muted) + .size(IconSize::XSmall), + ), + ), + ) + .full_width(true) + .anchor(gpui::Corner::TopLeft) + .offset(gpui::Point { + x: px(0.0), + y: px(4.0), + }) + .with_handle(buffer_font_handle), + ) + .child(font_picker_stepper( + "buffer-font-size", + &buffer_font_size, + tab_index, + write_buffer_font_size, + window, + cx, + )), ), ) } +fn font_picker_stepper( + id: &'static str, + font_size: &Pixels, + tab_index: &mut isize, + write_font_size: fn(Pixels, &mut App), + window: &mut Window, + cx: &mut App, +) -> NumericStepper { + window.with_id(id, |window| { + let optimistic_font_size: gpui::Entity> = window.use_state(cx, |_, _| None); + optimistic_font_size.update(cx, |optimistic_font_size, _| { + if let Some(optimistic_font_size_val) = optimistic_font_size { + if *optimistic_font_size_val == font_size.0 as u32 { + *optimistic_font_size = None; + } + } + }); + + let stepper_font_size = optimistic_font_size + .read(cx) + .unwrap_or_else(|| font_size.0 as u32); + + NumericStepper::new( + SharedString::new(format!("{}-stepper", id)), + stepper_font_size, + window, + cx, + ) + .on_change(move |new_value, _, cx| { + optimistic_font_size.write(cx, Some(*new_value)); + write_font_size(Pixels::from(*new_value), cx); + }) + .style(ui_input::NumericStepperStyle::Outlined) + .tab_index({ + *tab_index += 2; + *tab_index - 2 + }) + .min(6) + .max(32) + }) +} + type FontPicker = Picker; pub struct FontPickerDelegate { diff --git a/crates/ui_input/src/numeric_stepper.rs b/crates/ui_input/src/numeric_stepper.rs index 97c1102302..382c344fba 100644 --- a/crates/ui_input/src/numeric_stepper.rs +++ b/crates/ui_input/src/numeric_stepper.rs @@ -1,6 +1,7 @@ use std::{ fmt::Display, ops::{Add, Sub}, + rc::Rc, str::FromStr, }; @@ -115,7 +116,7 @@ impl_numeric_stepper_int!(u64); #[derive(RegisterComponent)] pub struct NumericStepper { id: ElementId, - value: Entity, + value: T, style: NumericStepperStyle, focus_handle: FocusHandle, mode: Entity, @@ -126,16 +127,12 @@ pub struct NumericStepper { min_value: T, max_value: T, on_reset: Option>, + on_change: Rc, tab_index: Option, } impl NumericStepper { - pub fn new( - id: impl Into, - value: Entity, - window: &mut Window, - cx: &mut App, - ) -> Self { + 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| { @@ -157,6 +154,7 @@ impl NumericStepper { min_value: T::min_value(), max_value: T::max_value(), on_reset: None, + on_change: Rc::new(|_, _, _| {}), tab_index: None, } } @@ -208,6 +206,11 @@ impl NumericStepper { self.tab_index = Some(tab_index); self } + + pub fn on_change(mut self, on_change: impl Fn(&T, &mut Window, &mut App) + 'static) -> Self { + self.on_change = Rc::new(on_change); + self + } } impl IntoElement for NumericStepper { @@ -274,14 +277,14 @@ impl RenderOnce for NumericStepper { .map(|decrement| { let decrement_handler = { let value = self.value.clone(); + let on_change = self.on_change.clone(); let focus = self.focus_handle.clone(); let min = self.min_value; move |click: &ClickEvent, window: &mut Window, cx: &mut App| { let step = get_step(click.modifiers()); - let current_value = *value.read(cx); - let new_value = current_value - step; + let new_value = value - step; let new_value = if new_value < min { min } else { new_value }; - value.write(cx, new_value); + on_change(&new_value, window, cx); window.focus(&focus); } }; @@ -328,7 +331,7 @@ impl RenderOnce for NumericStepper { .child(match *self.mode.read(cx) { NumericStepperMode::Read => div() .id("numeric_stepper_label") - .child(Label::new((self.format)(self.value.read(cx))).mx_3()) + .child(Label::new((self.format)(&self.value)).mx_3()) .on_click({ let mode = self.mode.clone(); @@ -347,17 +350,13 @@ impl RenderOnce for NumericStepper { |window, cx| { let mut editor = Editor::single_line(window, cx); - editor.set_text( - format!("{}", self.value.read(cx)), - window, - cx, - ); + editor.set_text(format!("{}", self.value), window, cx); cx.on_focus_out(&editor.focus_handle(cx), window, { let mode = self.mode.clone(); - let value = self.value.clone(); let min = self.min_value; let max = self.max_value; - move |this, _, _window, cx| { + let on_change = self.on_change.clone(); + move |this, _, window, cx| { if let Ok(new_value) = this.text(cx).parse::() { @@ -368,7 +367,8 @@ impl RenderOnce for NumericStepper { } else { new_value }; - value.write(cx, new_value); + + on_change(&new_value, window, cx); }; mode.write(cx, NumericStepperMode::Read); } @@ -397,13 +397,13 @@ impl RenderOnce for NumericStepper { let increment_handler = { let value = self.value.clone(); let focus = self.focus_handle.clone(); + let on_change = self.on_change.clone(); let max = self.max_value; move |click: &ClickEvent, window: &mut Window, cx: &mut App| { let step = get_step(click.modifiers()); - let current_value = *value.read(cx); - let new_value = current_value + step; + let new_value = value + step; let new_value = if new_value > max { max } else { new_value }; - value.write(cx, new_value); + on_change(&new_value, window, cx); window.focus(&focus); } }; @@ -474,20 +474,28 @@ impl Component for NumericStepper { "Default", NumericStepper::new( "numeric-stepper-component-preview", - first_stepper, + *first_stepper.read(cx), window, cx, ) + .on_change({ + let first_stepper = first_stepper.clone(); + move |value, _, cx| first_stepper.write(cx, *value) + }) .into_any_element(), ), single_example( "Outlined", NumericStepper::new( "numeric-stepper-with-border-component-preview", - second_stepper, + *second_stepper.read(cx), window, cx, ) + .on_change({ + let second_stepper = second_stepper.clone(); + move |value, _, cx| second_stepper.write(cx, *value) + }) .min(1.0) .max(100.0) .style(NumericStepperStyle::Outlined) From 2eeede1efb939fa9f5710c58df5462748d7f0b36 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Tue, 19 Aug 2025 21:21:01 -0700 Subject: [PATCH 5/5] Oh no, focus is hard --- crates/gpui/src/interactive.rs | 8 ++++++++ crates/onboarding/src/editing_page.rs | 1 + crates/ui_input/src/numeric_stepper.rs | 24 ++++++++++++++++-------- 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/crates/gpui/src/interactive.rs b/crates/gpui/src/interactive.rs index 218ae5fcdf..dafe623dfa 100644 --- a/crates/gpui/src/interactive.rs +++ b/crates/gpui/src/interactive.rs @@ -259,6 +259,14 @@ impl ClickEvent { ClickEvent::Mouse(event) => event.up.click_count, } } + + /// Returns whether the click event is generated by a keyboard event + pub fn is_keyboard(&self) -> bool { + match self { + ClickEvent::Mouse(_) => false, + ClickEvent::Keyboard(_) => true, + } + } } /// An enum representing the keyboard button that was pressed for a click event. diff --git a/crates/onboarding/src/editing_page.rs b/crates/onboarding/src/editing_page.rs index ce509ea843..f248ee6bdb 100644 --- a/crates/onboarding/src/editing_page.rs +++ b/crates/onboarding/src/editing_page.rs @@ -445,6 +445,7 @@ fn font_picker_stepper( optimistic_font_size.write(cx, Some(*new_value)); write_font_size(Pixels::from(*new_value), cx); }) + .format(|value| format!("{value}px")) .style(ui_input::NumericStepperStyle::Outlined) .tab_index({ *tab_index += 2; diff --git a/crates/ui_input/src/numeric_stepper.rs b/crates/ui_input/src/numeric_stepper.rs index 382c344fba..8601840368 100644 --- a/crates/ui_input/src/numeric_stepper.rs +++ b/crates/ui_input/src/numeric_stepper.rs @@ -278,14 +278,13 @@ impl RenderOnce for NumericStepper { let decrement_handler = { let value = self.value.clone(); let on_change = self.on_change.clone(); - let focus = self.focus_handle.clone(); let min = self.min_value; move |click: &ClickEvent, window: &mut Window, cx: &mut App| { let step = get_step(click.modifiers()); let new_value = value - step; let new_value = if new_value < min { min } else { new_value }; on_change(&new_value, window, cx); - window.focus(&focus); + window.focus_prev(); } }; @@ -332,11 +331,16 @@ impl RenderOnce for NumericStepper { NumericStepperMode::Read => div() .id("numeric_stepper_label") .child(Label::new((self.format)(&self.value)).mx_3()) + .when_some(tab_index.as_mut(), |this, tab_index| { + *tab_index += 1; + this.tab_index(*tab_index - 1).focus(|style| { + style.bg(cx.theme().colors().element_hover) + }) + }) .on_click({ let mode = self.mode.clone(); - move |click, _, cx| { - if click.click_count() == 2 { + if click.click_count() == 2 || click.is_keyboard() { mode.write(cx, NumericStepperMode::Edit); } } @@ -348,6 +352,7 @@ impl RenderOnce for NumericStepper { NumericStepperMode::Edit => div() .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(format!("{}", self.value), window, cx); @@ -368,6 +373,11 @@ impl RenderOnce for NumericStepper { new_value }; + if let Some(previous) = + previous_focus_handle.as_ref() + { + window.focus(previous); + } on_change(&new_value, window, cx); }; mode.write(cx, NumericStepperMode::Read); @@ -381,9 +391,8 @@ impl RenderOnce for NumericStepper { } })) .on_action::({ - let focus = self.focus_handle.clone(); move |_, window, _| { - window.focus(&focus); + window.blur(); } }) .size_full() @@ -396,7 +405,6 @@ impl RenderOnce for NumericStepper { .map(|increment| { let increment_handler = { let value = self.value.clone(); - let focus = self.focus_handle.clone(); let on_change = self.on_change.clone(); let max = self.max_value; move |click: &ClickEvent, window: &mut Window, cx: &mut App| { @@ -404,7 +412,7 @@ impl RenderOnce for NumericStepper { let new_value = value + step; let new_value = if new_value > max { max } else { new_value }; on_change(&new_value, window, cx); - window.focus(&focus); + // window.focus(&focus); } };