Compare commits
5 commits
main
...
numperic-s
Author | SHA1 | Date | |
---|---|---|---|
![]() |
2eeede1efb | ||
![]() |
e47b9d3b38 | ||
![]() |
b775a2e3cb | ||
![]() |
e6f06f14fd | ||
![]() |
c5a258e0b8 |
14 changed files with 641 additions and 308 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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<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,
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -2504,7 +2504,7 @@ impl Window {
|
|||
&mut self,
|
||||
key: impl Into<ElementId>,
|
||||
cx: &mut App,
|
||||
init: impl FnOnce(&mut Self, &mut App) -> S,
|
||||
init: impl FnOnce(&mut Self, &mut Context<S>) -> S,
|
||||
) -> Entity<S> {
|
||||
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<S: 'static>(
|
||||
&mut self,
|
||||
cx: &mut App,
|
||||
init: impl FnOnce(&mut Self, &mut App) -> S,
|
||||
init: impl FnOnce(&mut Self, &mut Context<S>) -> S,
|
||||
) -> Entity<S> {
|
||||
self.use_keyed_state(
|
||||
ElementId::CodeLocation(*core::panic::Location::caller()),
|
||||
|
@ -4838,6 +4838,12 @@ impl<T: Into<SharedString>> From<(ElementId, T)> for ElementId {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<core::panic::Location<'static>> 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)]
|
||||
|
|
|
@ -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};
|
||||
|
||||
|
@ -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<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 == 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<FontPickerDelegate>;
|
||||
|
||||
pub struct FontPickerDelegate {
|
||||
|
|
|
@ -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,
|
||||
// ))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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::*;
|
||||
|
|
|
@ -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
|
||||
|
|
517
crates/ui_input/src/numeric_stepper.rs
Normal file
517
crates/ui_input/src/numeric_stepper.rs
Normal file
|
@ -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<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.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::<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();
|
||||
}
|
||||
})
|
||||
.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<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, TextStyle};
|
||||
pub use numeric_stepper::*;
|
||||
use settings::Settings;
|
||||
use theme::ThemeSettings;
|
||||
use ui::prelude::*;
|
||||
|
|
|
@ -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),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue