gpui-component/crates/story/src/number_input_story.rs
Jason Lee de491a790c
input: Fix Input prefix, suffix position, and mask * in vertical center. (#965)
- Fix Input, NumberInput padding x with prefix, suffix.
- Fix Input mask char to render in vertical center.

<img width="498" alt="image"
src="https://github.com/user-attachments/assets/f7b34e17-2503-4a24-9e16-3a30b48bacb5"
/>

- Fix to only use `Tab`, `Shift-Tab` to indent in multi-line mode.
- Improve blink cursor to render with 1.5px width and same as
line-height.
2025-06-17 13:59:02 +08:00

231 lines
8.1 KiB
Rust

use gpui::{
actions, App, AppContext as _, Context, Entity, FocusHandle, Focusable, InteractiveElement,
IntoElement, KeyBinding, ParentElement as _, Render, Styled, Subscription, Window,
};
use regex::Regex;
use crate::section;
use gpui_component::{
button::{Button, ButtonVariants},
input::{InputEvent, InputState, MaskPattern, NumberInput, NumberInputEvent, StepAction},
v_flex, FocusableCycle, IconName, Sizable,
};
actions!(input_story, [Tab, TabPrev]);
const CONTEXT: &str = "NumberInputStory";
pub fn init(cx: &mut App) {
cx.bind_keys([
KeyBinding::new("shift-tab", TabPrev, Some(CONTEXT)),
KeyBinding::new("tab", Tab, Some(CONTEXT)),
])
}
pub struct NumberInputStory {
number_input1_value: i64,
number_input1: Entity<InputState>,
number_input2: Entity<InputState>,
number_input2_value: u64,
number_input3: Entity<InputState>,
number_input3_value: f64,
_subscriptions: Vec<Subscription>,
}
impl super::Story for NumberInputStory {
fn title() -> &'static str {
"NumberInput"
}
fn description() -> &'static str {
"NumberInput design to support + - to adjust the input value."
}
fn closable() -> bool {
false
}
fn new_view(window: &mut Window, cx: &mut App) -> Entity<impl Render + Focusable> {
Self::view(window, cx)
}
}
impl NumberInputStory {
pub fn view(window: &mut Window, cx: &mut App) -> Entity<Self> {
cx.new(|cx| Self::new(window, cx))
}
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let number_input1_value = 1;
let number_input1 = cx.new(|cx| {
InputState::new(window, cx)
.placeholder("Number Input")
.default_value(number_input1_value.to_string())
});
let number_input2 = cx.new(|cx| {
InputState::new(window, cx)
.placeholder("Unsized Integer Number Input")
.pattern(Regex::new(r"^\d+$").unwrap())
});
let number_input3 = cx.new(|cx| {
InputState::new(window, cx)
.placeholder("Number Input with mask pattern")
.mask_pattern(MaskPattern::Number {
separator: Some(','),
fraction: Some(2),
})
});
let _subscriptions = vec![
cx.subscribe_in(&number_input1, window, Self::on_input_event),
cx.subscribe_in(&number_input1, window, Self::on_number_input_event),
cx.subscribe_in(&number_input2, window, Self::on_input_event),
cx.subscribe_in(&number_input2, window, Self::on_number_input_event),
cx.subscribe_in(&number_input3, window, Self::on_input_event),
cx.subscribe_in(&number_input3, window, Self::on_number_input_event),
];
Self {
number_input1,
number_input1_value,
number_input2,
number_input2_value: 0,
number_input3,
number_input3_value: 0.0,
_subscriptions,
}
}
fn tab(&mut self, _: &Tab, window: &mut Window, cx: &mut Context<Self>) {
self.cycle_focus(true, window, cx);
}
fn tab_prev(&mut self, _: &TabPrev, window: &mut Window, cx: &mut Context<Self>) {
self.cycle_focus(false, window, cx);
}
fn on_input_event(
&mut self,
this: &Entity<InputState>,
event: &InputEvent,
_: &mut Window,
_: &mut Context<Self>,
) {
match event {
InputEvent::Change(text) => {
if this == &self.number_input1 {
if let Ok(value) = text.parse::<i64>() {
self.number_input1_value = value;
}
} else if this == &self.number_input2 {
if let Ok(value) = text.parse::<u64>() {
self.number_input2_value = value;
}
} else if this == &self.number_input3 {
if let Ok(value) = text.parse::<f64>() {
self.number_input3_value = value;
}
}
println!("Change: {}", text);
}
InputEvent::PressEnter { secondary } => {
println!("PressEnter secondary: {}", secondary)
}
InputEvent::Focus => println!("Focus"),
InputEvent::Blur => println!("Blur"),
}
}
fn on_number_input_event(
&mut self,
this: &Entity<InputState>,
event: &NumberInputEvent,
window: &mut Window,
cx: &mut Context<Self>,
) {
match event {
NumberInputEvent::Step(step_action) => match step_action {
StepAction::Decrement => {
if this == &self.number_input1 {
self.number_input1_value = self.number_input1_value - 1;
this.update(cx, |input, cx| {
input.set_value(self.number_input1_value.to_string(), window, cx);
});
} else if this == &self.number_input2 {
self.number_input2_value = self.number_input2_value - 1;
this.update(cx, |input, cx| {
input.set_value(self.number_input2_value.to_string(), window, cx);
});
} else if this == &self.number_input3 {
self.number_input3_value = self.number_input3_value - 1.0;
this.update(cx, |input, cx| {
input.set_value(self.number_input3_value.to_string(), window, cx);
});
}
}
StepAction::Increment => {
if this == &self.number_input1 {
self.number_input1_value = self.number_input1_value + 1;
this.update(cx, |input, cx| {
input.set_value(self.number_input1_value.to_string(), window, cx);
});
} else if this == &self.number_input2 {
self.number_input2_value = self.number_input2_value + 1;
this.update(cx, |input, cx| {
input.set_value(self.number_input2_value.to_string(), window, cx);
});
} else if this == &self.number_input3 {
self.number_input3_value = self.number_input3_value + 1.0;
this.update(cx, |input, cx| {
input.set_value(self.number_input3_value.to_string(), window, cx);
});
}
}
},
}
}
}
impl FocusableCycle for NumberInputStory {
fn cycle_focus_handles(&self, _: &mut Window, _cx: &mut App) -> Vec<FocusHandle> {
[].to_vec()
}
}
impl Focusable for NumberInputStory {
fn focus_handle(&self, cx: &gpui::App) -> gpui::FocusHandle {
self.number_input1.focus_handle(cx)
}
}
impl Render for NumberInputStory {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
v_flex()
.key_context(CONTEXT)
.id("input-story")
.on_action(cx.listener(Self::tab))
.on_action(cx.listener(Self::tab_prev))
.size_full()
.justify_start()
.gap_3()
.child(
section("Normal Size")
.max_w_md()
.child(NumberInput::new(&self.number_input1)),
)
.child(
section("Small Size with suffix").max_w_md().child(
NumberInput::new(&self.number_input2)
.small()
.suffix(Button::new("info").ghost().icon(IconName::Info).xsmall()),
),
)
.child(
section("With mask pattern")
.max_w_md()
.child(NumberInput::new(&self.number_input3)),
)
}
}