settings ui: Improve numeric stepper component interface (#36513)

This is the first step to allowing users to type into a numeric stepper
to set its value. This PR makes the numeric stepper take in a generic
type `T` where T: `NumericStepperType`

```rust
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;
}
```

This allows setting of step sizes and min/max values as well as making
the component easier to use.

cc @danilo-leal 

Release Notes:

- N/A

---------

Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
Co-authored-by: Gaauwe Rombouts <mail@grombouts.nl>
This commit is contained in:
Anthony Eid
2025-10-03 17:35:30 +00:00
committed by GitHub
co-authored by Mikayla Maki Gaauwe Rombouts
parent f499504b13
commit e5f05a21ce
13 changed files with 617 additions and 289 deletions
Generated
+2
View File
@@ -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",
+3 -13
View File
@@ -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
}
}
+14
View File
@@ -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,
+6
View File
@@ -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)
+8
View File
@@ -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.
+1
View File
@@ -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
+62 -37
View File
@@ -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 {
-2
View File
@@ -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::*;
-237
View File
@@ -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(),
)
}
}
+1
View File
@@ -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
+517
View File
@@ -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(),
)
}
}
+2
View File
@@ -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),