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..2562a94d3d 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: Re-evaluate this whole crate once settings UI is complete } } 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/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/gpui/src/window.rs b/crates/gpui/src/window.rs index 62aeb0df11..ec83fc6e30 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()), @@ -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/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..f248ee6bdb 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}; @@ -346,23 +346,14 @@ fn render_font_customization_section( }) .with_handle(ui_font_handle), ) - .child( - NumericStepper::new( - "ui-font-size", - ui_font_size.to_string(), - move |_, _, cx| { - write_ui_font_size(ui_font_size - px(1.), cx); - }, - move |_, _, cx| { - write_ui_font_size(ui_font_size + px(1.), cx); - }, - ) - .style(ui::NumericStepperStyle::Outlined) - .tab_index({ - *tab_index += 2; - *tab_index - 2 - }), - ), + .child(font_picker_stepper( + "ui-font-size", + &ui_font_size, + tab_index, + write_ui_font_size, + window, + cx, + )), ), ) .child( @@ -410,27 +401,61 @@ fn render_font_customization_section( }) .with_handle(buffer_font_handle), ) - .child( - NumericStepper::new( - "buffer-font-size", - buffer_font_size.to_string(), - move |_, _, cx| { - write_buffer_font_size(buffer_font_size - px(1.), cx); - }, - move |_, _, cx| { - write_buffer_font_size(buffer_font_size + px(1.), cx); - }, - ) - .style(ui::NumericStepperStyle::Outlined) - .tab_index({ - *tab_index += 2; - *tab_index - 2 - }), - ), + .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); + }) + .format(|value| format!("{value}px")) + .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/settings_ui/src/appearance_settings_controls.rs b/crates/settings_ui/src/appearance_settings_controls.rs index 141ae13182..c6ccacb65c 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,22 +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 |_, _, cx| { - Self::write(value - px(1.), cx); - }, - move |_, _, cx| { - Self::write(value + px(1.), 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/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/src/components/numeric_stepper.rs b/crates/ui/src/components/numeric_stepper.rs deleted file mode 100644 index 2ddb86d9a0..0000000000 --- a/crates/ui/src/components/numeric_stepper.rs +++ /dev/null @@ -1,237 +0,0 @@ -use gpui::ClickEvent; - -use crate::{IconButtonShape, prelude::*}; - -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] -pub enum NumericStepperStyle { - Outlined, - #[default] - Ghost, -} - -#[derive(IntoElement, RegisterComponent)] -pub struct NumericStepper { - id: ElementId, - value: SharedString, - style: NumericStepperStyle, - on_decrement: Box, - on_increment: Box, - /// Whether to reserve space for the reset button. - reserve_space_for_reset: bool, - on_reset: Option>, - tab_index: Option, -} - -impl NumericStepper { - pub fn new( - id: impl Into, - value: impl Into, - on_decrement: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, - on_increment: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, - ) -> Self { - Self { - id: id.into(), - value: value.into(), - style: NumericStepperStyle::default(), - on_decrement: Box::new(on_decrement), - on_increment: Box::new(on_increment), - reserve_space_for_reset: false, - on_reset: None, - tab_index: None, - } - } - - pub fn style(mut self, style: NumericStepperStyle) -> Self { - self.style = style; - self - } - - pub fn reserve_space_for_reset(mut self, reserve_space_for_reset: bool) -> Self { - self.reserve_space_for_reset = reserve_space_for_reset; - self - } - - pub fn on_reset( - mut self, - on_reset: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, - ) -> Self { - self.on_reset = Some(Box::new(on_reset)); - self - } - - pub fn tab_index(mut self, tab_index: isize) -> Self { - self.tab_index = Some(tab_index); - 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; - - let is_outlined = matches!(self.style, NumericStepperStyle::Outlined); - let mut tab_index = self.tab_index; - - h_flex() - .id(self.id) - .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 - } - }) - .child( - h_flex() - .gap_1() - .rounded_sm() - .map(|this| { - if is_outlined { - this.overflow_hidden() - .bg(cx.theme().colors().surface_background) - .border_1() - .border_color(cx.theme().colors().border_variant) - } else { - this.px_1().bg(cx.theme().colors().editor_background) - } - }) - .map(|decrement| { - if is_outlined { - decrement.child( - h_flex() - .id("decrement_button") - .p_1p5() - .size_full() - .justify_center() - .hover(|s| s.bg(cx.theme().colors().element_hover)) - .border_r_1() - .border_color(cx.theme().colors().border_variant) - .child(Icon::new(IconName::Dash).size(IconSize::Small)) - .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(self.on_decrement), - ) - } else { - decrement.child( - IconButton::new("decrement", IconName::Dash) - .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(self.on_decrement), - ) - } - }) - .child(Label::new(self.value).mx_3()) - .map(|increment| { - if is_outlined { - increment.child( - h_flex() - .id("increment_button") - .p_1p5() - .size_full() - .justify_center() - .hover(|s| s.bg(cx.theme().colors().element_hover)) - .border_l_1() - .border_color(cx.theme().colors().border_variant) - .child(Icon::new(IconName::Plus).size(IconSize::Small)) - .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(self.on_increment), - ) - } else { - increment.child( - IconButton::new("increment", IconName::Dash) - .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(self.on_increment), - ) - } - }), - ) - } -} - -impl Component for NumericStepper { - fn scope() -> ComponentScope { - ComponentScope::Input - } - - fn name() -> &'static str { - "Numeric Stepper" - } - - fn sort_name() -> &'static str { - Self::name() - } - - fn description() -> Option<&'static str> { - Some("A button used to increment or decrement a numeric value.") - } - - fn preview(_window: &mut Window, _cx: &mut App) -> Option { - Some( - v_flex() - .gap_6() - .children(vec![example_group_with_title( - "Styles", - vec![ - single_example( - "Default", - NumericStepper::new( - "numeric-stepper-component-preview", - "10", - move |_, _, _| {}, - move |_, _, _| {}, - ) - .into_any_element(), - ), - single_example( - "Outlined", - NumericStepper::new( - "numeric-stepper-with-border-component-preview", - "10", - move |_, _, _| {}, - move |_, _, _| {}, - ) - .style(NumericStepperStyle::Outlined) - .into_any_element(), - ), - ], - )]) - .into_any_element(), - ) - } -} 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_input/src/numeric_stepper.rs b/crates/ui_input/src/numeric_stepper.rs new file mode 100644 index 0000000000..8601840368 --- /dev/null +++ b/crates/ui_input/src/numeric_stepper.rs @@ -0,0 +1,517 @@ +use std::{ + fmt::Display, + ops::{Add, Sub}, + rc::Rc, + str::FromStr, +}; + +use editor::Editor; +use gpui::{ClickEvent, Entity, FocusHandle, Focusable, Modifiers}; + +use ui::{IconButtonShape, prelude::*}; + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] +pub enum NumericStepperStyle { + Outlined, + #[default] + Ghost, +} + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] +pub enum NumericStepperMode { + #[default] + Read, + Edit, +} + +pub trait NumericStepperType: + Display + + Add + + Sub + + Copy + + Clone + + Sized + + PartialOrd + + FromStr + + 'static +{ + fn default_format(value: &Self) -> String { + format!("{}", value) + } + 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 { + ($type:ident) => { + impl NumericStepperType for $type { + fn default_step() -> Self { + 1 + } + + fn large_step() -> Self { + 10 + } + + fn small_step() -> Self { + 1 + } + + fn min_value() -> Self { + <$type>::MIN + } + + fn max_value() -> Self { + <$type>::MAX + } + } + }; +} + +macro_rules! impl_numeric_stepper_float { + ($type:ident) => { + impl NumericStepperType for $type { + fn default_format(value: &Self) -> String { + format!("{:^4}", value) + .trim_end_matches('0') + .trim_end_matches('.') + .to_string() + } + + fn default_step() -> Self { + 1.0 + } + + fn large_step() -> Self { + 10.0 + } + + fn small_step() -> Self { + 0.1 + } + + fn min_value() -> Self { + <$type>::MIN + } + + fn max_value() -> Self { + <$type>::MAX + } + } + }; +} + +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); + +#[derive(RegisterComponent)] +pub struct NumericStepper { + id: ElementId, + value: T, + style: NumericStepperStyle, + focus_handle: FocusHandle, + mode: Entity, + format: Box String>, + large_step: T, + small_step: T, + step: T, + min_value: T, + max_value: T, + on_reset: Option>, + on_change: Rc, + tab_index: Option, +} + +impl NumericStepper { + 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 = window.use_state(cx, |_, _| NumericStepperMode::default()); + let focus_handle = window.use_state(cx, |_, cx| cx.focus_handle()); + (mode, focus_handle) + }); + + Self { + id, + 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, + on_change: Rc::new(|_, _, _| {}), + tab_index: None, + } + } + + pub fn format(mut self, format: impl FnOnce(&T) -> String + 'static) -> Self { + self.format = Box::new(format); + self + } + + 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 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 + } + + pub fn on_reset( + mut self, + on_reset: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, + ) -> Self { + self.on_reset = Some(Box::new(on_reset)); + self + } + + pub fn tab_index(mut self, tab_index: isize) -> Self { + 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 { + 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; + + 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() + .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() + .gap_1() + .rounded_sm() + .map(|this| { + if is_outlined { + this.overflow_hidden() + .bg(cx.theme().colors().surface_background) + .border_1() + .border_color(cx.theme().colors().border_variant) + } else { + this.px_1().bg(cx.theme().colors().editor_background) + } + }) + .map(|decrement| { + let decrement_handler = { + let value = self.value.clone(); + let on_change = self.on_change.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_prev(); + } + }; + + if is_outlined { + decrement.child( + h_flex() + .id("decrement_button") + .p_1p5() + .size_full() + .justify_center() + .hover(|s| s.bg(cx.theme().colors().element_hover)) + .border_r_1() + .border_color(cx.theme().colors().border_variant) + .child(Icon::new(IconName::Dash).size(IconSize::Small)) + .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(decrement_handler), + ) + } else { + decrement.child( + IconButton::new("decrement", IconName::Dash) + .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(decrement_handler), + ) + } + }) + .child( + div() + .text_color(gpui::red()) + .in_focus(|this| { + this.border_1() + .border_color(cx.theme().colors().border_focused) + }) + .child(match *self.mode.read(cx) { + 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 || click.is_keyboard() { + mode.write(cx, NumericStepperMode::Edit); + } + } + }) + .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 previous_focus_handle = window.focused(cx); + let mut editor = Editor::single_line(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 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) = + 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); + } + on_change(&new_value, window, cx); + }; + mode.write(cx, NumericStepperMode::Read); + } + }) + .detach(); + + window.focus(&editor.focus_handle(cx)); + + editor + } + })) + .on_action::({ + move |_, window, _| { + window.blur(); + } + }) + .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(); + 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 new_value = value + step; + let new_value = if new_value > max { max } else { new_value }; + on_change(&new_value, window, cx); + // window.focus(&focus); + } + }; + + if is_outlined { + increment.child( + h_flex() + .id("increment_button") + .p_1p5() + .size_full() + .justify_center() + .hover(|s| s.bg(cx.theme().colors().element_hover)) + .border_l_1() + .border_color(cx.theme().colors().border_variant) + .child(Icon::new(IconName::Plus).size(IconSize::Small)) + .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(increment_handler), + ) + } else { + increment.child( + IconButton::new("increment", IconName::Plus) + .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(increment_handler), + ) + } + }), + ) + } +} + +impl Component for NumericStepper { + fn scope() -> ComponentScope { + ComponentScope::Input + } + + fn name() -> &'static str { + "Numeric Stepper" + } + + fn sort_name() -> &'static str { + Self::name() + } + + fn description() -> Option<&'static str> { + Some("A button used to increment or decrement a numeric value.") + } + + fn preview(window: &mut Window, cx: &mut App) -> Option { + let first_stepper = window.use_state(cx, |_, _| 100usize); + let second_stepper = window.use_state(cx, |_, _| 100.0); + Some( + v_flex() + .gap_6() + .children(vec![example_group_with_title( + "Styles", + vec![ + single_example( + "Default", + NumericStepper::new( + "numeric-stepper-component-preview", + *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.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) + .into_any_element(), + ), + ], + )]) + .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::*; 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),