Detailed changes
@@ -10640,6 +10640,7 @@ dependencies = [
"telemetry",
"theme",
"ui",
+ "ui_input",
"util",
"vim_mode_setting",
"workspace",
@@ -17324,6 +17325,7 @@ dependencies = [
"component",
"editor",
"gpui",
+ "menu",
"settings",
"theme",
"ui",
@@ -5,8 +5,7 @@ use project::project_settings::ProjectSettings;
use settings::{EditableSettingControl, Settings, SettingsContent};
use theme::{FontFamilyCache, FontFamilyName, ThemeSettings};
use ui::{
- CheckboxWithLabel, ContextMenu, DropdownMenu, NumericStepper, SettingsContainer, SettingsGroup,
- prelude::*,
+ CheckboxWithLabel, ContextMenu, DropdownMenu, SettingsContainer, SettingsGroup, prelude::*,
};
use crate::EditorSettings;
@@ -129,21 +128,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
}
}
@@ -80,6 +80,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<T>) + '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<T2, Evt>(
&mut self,
@@ -2809,6 +2809,12 @@ impl From<Pixels> for u32 {
}
}
+impl From<&Pixels> for u32 {
+ fn from(pixels: &Pixels) -> Self {
+ pixels.0 as u32
+ }
+}
+
impl From<u32> for Pixels {
fn from(pixels: u32) -> Self {
Pixels(pixels as f32)
@@ -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.
@@ -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
@@ -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};
@@ -341,23 +341,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(
@@ -405,27 +396,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<u32> {
+ window.with_id(id, |window| {
+ let optimistic_font_size: gpui::Entity<Option<u32>> = 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 == u32::from(font_size) {
+ *optimistic_font_size = None;
+ }
+ }
+ });
+
+ let stepper_font_size = optimistic_font_size
+ .read(cx)
+ .unwrap_or_else(|| font_size.into());
+
+ 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<FontPickerDelegate>;
pub struct FontPickerDelegate {
@@ -23,7 +23,6 @@ mod list;
mod modal;
mod navigable;
mod notification;
-mod numeric_stepper;
mod popover;
mod popover_menu;
mod progress;
@@ -68,7 +67,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::*;
@@ -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<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
- on_increment: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
- /// Whether to reserve space for the reset button.
- reserve_space_for_reset: bool,
- on_reset: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
- tab_index: Option<isize>,
-}
-
-impl NumericStepper {
- pub fn new(
- id: impl Into<ElementId>,
- value: impl Into<SharedString>,
- 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<AnyElement> {
- 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(),
- )
- }
-}
@@ -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
@@ -0,0 +1,517 @@
+use std::{
+ fmt::Display,
+ ops::{Add, Sub},
+ rc::Rc,
+ str::FromStr,
+};
+
+use editor::{Editor, EditorStyle};
+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<Output = Self>
+ + Sub<Output = Self>
+ + 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<T = usize> {
+ id: ElementId,
+ value: T,
+ style: NumericStepperStyle,
+ focus_handle: FocusHandle,
+ mode: Entity<NumericStepperMode>,
+ format: Box<dyn FnOnce(&T) -> String>,
+ large_step: T,
+ small_step: T,
+ step: T,
+ min_value: T,
+ max_value: T,
+ on_reset: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
+ on_change: Rc<dyn Fn(&T, &mut Window, &mut App) + 'static>,
+ tab_index: Option<isize>,
+}
+
+impl<T: NumericStepperType> NumericStepper<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 = 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<T: NumericStepperType> IntoElement for NumericStepper<T> {
+ type Element = gpui::Component<Self>;
+
+ fn into_element(self) -> Self::Element {
+ gpui::Component::new(self)
+ }
+}
+
+impl<T: NumericStepperType> RenderOnce for NumericStepper<T> {
+ 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;
+ 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(
+ h_flex()
+ .h_8()
+ .min_w_16()
+ .w_full()
+ .border_1()
+ .border_color(cx.theme().colors().border_transparent)
+ .in_focus(|this| this.border_color(cx.theme().colors().border_focused))
+ .child(match *self.mode.read(cx) {
+ NumericStepperMode::Read => h_flex()
+ .id("numeric_stepper_label")
+ .flex_1()
+ .justify_center()
+ .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() {
+ // Edit mode is disabled until we implement center text alignment for editor
+ // mode.write(cx, NumericStepperMode::Edit);
+ }
+ }
+ })
+ .into_any_element(),
+ NumericStepperMode::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);
+ let mut style = EditorStyle::default();
+ style.text.text_align = gpui::TextAlign::Right;
+ editor.set_style(style, 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::<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);
+ }
+ on_change(&new_value, window, cx);
+ };
+ mode.write(cx, NumericStepperMode::Read);
+ }
+ })
+ .detach();
+
+ window.focus(&editor.focus_handle(cx));
+
+ editor
+ }
+ }))
+ .on_action::<menu::Confirm>({
+ move |_, window, _| {
+ window.blur();
+ }
+ })
+ .into_any_element(),
+ }),
+ )
+ .map(|increment| {
+ let increment_handler = {
+ let value = self.value;
+ 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);
+ }
+ };
+
+ 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<usize> {
+ 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<AnyElement> {
+ 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(),
+ )
+ }
+}
@@ -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, Length, TextStyle};
+pub use numeric_stepper::*;
use settings::Settings;
use std::sync::Arc;
use theme::ThemeSettings;
@@ -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),