ui: Add number_input (#433)
https://github.com/user-attachments/assets/ae6f4e48-b0fa-4f8c-bff5-809c70848b06
This commit is contained in:
parent
ea011d11d6
commit
ea6ec8aa5d
4 changed files with 278 additions and 4 deletions
|
|
@ -3,6 +3,7 @@ use gpui::{
|
||||||
ParentElement as _, Render, SharedString, Styled, View, ViewContext, VisualContext,
|
ParentElement as _, Render, SharedString, Styled, View, ViewContext, VisualContext,
|
||||||
WindowContext,
|
WindowContext,
|
||||||
};
|
};
|
||||||
|
use regex::Regex;
|
||||||
|
|
||||||
use crate::section;
|
use crate::section;
|
||||||
use ui::{
|
use ui::{
|
||||||
|
|
@ -10,6 +11,7 @@ use ui::{
|
||||||
checkbox::Checkbox,
|
checkbox::Checkbox,
|
||||||
h_flex,
|
h_flex,
|
||||||
input::{InputEvent, OtpInput, TextInput},
|
input::{InputEvent, OtpInput, TextInput},
|
||||||
|
number_input::{NumberInput, NumberInputEvent},
|
||||||
prelude::FluentBuilder as _,
|
prelude::FluentBuilder as _,
|
||||||
scroll::ScrollbarAxis,
|
scroll::ScrollbarAxis,
|
||||||
v_flex, FocusableCycle, IconName, Sizable, StyledExt,
|
v_flex, FocusableCycle, IconName, Sizable, StyledExt,
|
||||||
|
|
@ -29,6 +31,10 @@ pub fn init(cx: &mut AppContext) {
|
||||||
pub struct InputStory {
|
pub struct InputStory {
|
||||||
input1: View<TextInput>,
|
input1: View<TextInput>,
|
||||||
input2: View<TextInput>,
|
input2: View<TextInput>,
|
||||||
|
number_input1_value: i64,
|
||||||
|
number_input1: View<NumberInput>,
|
||||||
|
number_input2: View<NumberInput>,
|
||||||
|
number_input2_value: u64,
|
||||||
mash_input: View<TextInput>,
|
mash_input: View<TextInput>,
|
||||||
disabled_input: View<TextInput>,
|
disabled_input: View<TextInput>,
|
||||||
prefix_input1: View<TextInput>,
|
prefix_input1: View<TextInput>,
|
||||||
|
|
@ -72,13 +78,30 @@ impl InputStory {
|
||||||
);
|
);
|
||||||
input
|
input
|
||||||
});
|
});
|
||||||
|
|
||||||
cx.subscribe(&input1, Self::on_input_event).detach();
|
cx.subscribe(&input1, Self::on_input_event).detach();
|
||||||
|
|
||||||
let input2 = cx.new_view(|cx| TextInput::new(cx).placeholder("Enter text here..."));
|
let input2 = cx.new_view(|cx| TextInput::new(cx).placeholder("Enter text here..."));
|
||||||
|
|
||||||
cx.subscribe(&input2, Self::on_input_event).detach();
|
cx.subscribe(&input2, Self::on_input_event).detach();
|
||||||
|
|
||||||
|
let number_input1_value = 1;
|
||||||
|
let number_input1 = cx.new_view(|cx| {
|
||||||
|
let input = NumberInput::new(cx).placeholder("Number Input", cx);
|
||||||
|
input.set_value(number_input1_value.to_string(), cx);
|
||||||
|
input
|
||||||
|
});
|
||||||
|
cx.subscribe(&number_input1, Self::on_number_input1_event)
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
let number_input2 = cx.new_view(|cx| {
|
||||||
|
NumberInput::new(cx)
|
||||||
|
.placeholder("Unsized Integer Number Input", cx)
|
||||||
|
.pattern(Regex::new(r"^\d+$").unwrap(), cx)
|
||||||
|
.small(cx)
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.subscribe(&number_input2, Self::on_number_input2_event)
|
||||||
|
.detach();
|
||||||
|
|
||||||
let mask_input = cx.new_view(|cx| {
|
let mask_input = cx.new_view(|cx| {
|
||||||
let mut input = TextInput::new(cx).cleanable();
|
let mut input = TextInput::new(cx).cleanable();
|
||||||
input.set_masked(true, cx);
|
input.set_masked(true, cx);
|
||||||
|
|
@ -120,6 +143,10 @@ impl InputStory {
|
||||||
Self {
|
Self {
|
||||||
input1,
|
input1,
|
||||||
input2,
|
input2,
|
||||||
|
number_input1,
|
||||||
|
number_input1_value,
|
||||||
|
number_input2,
|
||||||
|
number_input2_value: 0,
|
||||||
mash_input: mask_input,
|
mash_input: mask_input,
|
||||||
disabled_input: cx.new_view(|cx| {
|
disabled_input: cx.new_view(|cx| {
|
||||||
let mut input = TextInput::new(cx);
|
let mut input = TextInput::new(cx);
|
||||||
|
|
@ -186,6 +213,70 @@ impl InputStory {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn on_number_input1_event(
|
||||||
|
&mut self,
|
||||||
|
_: View<NumberInput>,
|
||||||
|
event: &NumberInputEvent,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) {
|
||||||
|
match event {
|
||||||
|
NumberInputEvent::Input(input_event) => match input_event {
|
||||||
|
InputEvent::Change(text) => println!("Change: {}", text),
|
||||||
|
InputEvent::PressEnter => println!("PressEnter"),
|
||||||
|
InputEvent::Focus => println!("Focus"),
|
||||||
|
InputEvent::Blur => println!("Blur"),
|
||||||
|
},
|
||||||
|
NumberInputEvent::Step(step_action) => match step_action {
|
||||||
|
ui::number_input::StepAction::Decrement => {
|
||||||
|
self.number_input1_value = self.number_input1_value - 1;
|
||||||
|
self.number_input1.update(cx, |input, cx| {
|
||||||
|
input.set_value(self.number_input1_value.to_string(), cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
ui::number_input::StepAction::Increment => {
|
||||||
|
self.number_input1_value = self.number_input1_value + 1;
|
||||||
|
self.number_input1.update(cx, |input, cx| {
|
||||||
|
input.set_value(self.number_input1_value.to_string(), cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_number_input2_event(
|
||||||
|
&mut self,
|
||||||
|
_: View<NumberInput>,
|
||||||
|
event: &NumberInputEvent,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) {
|
||||||
|
match event {
|
||||||
|
NumberInputEvent::Input(input_event) => match input_event {
|
||||||
|
InputEvent::Change(text) => println!("Change: {}", text),
|
||||||
|
InputEvent::PressEnter => println!("PressEnter"),
|
||||||
|
InputEvent::Focus => println!("Focus"),
|
||||||
|
InputEvent::Blur => println!("Blur"),
|
||||||
|
},
|
||||||
|
NumberInputEvent::Step(step_action) => match step_action {
|
||||||
|
ui::number_input::StepAction::Decrement => {
|
||||||
|
if self.number_input2_value.le(&0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.number_input2_value = self.number_input2_value - 1;
|
||||||
|
self.number_input2.update(cx, |input, cx| {
|
||||||
|
input.set_value(self.number_input2_value.to_string(), cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
ui::number_input::StepAction::Increment => {
|
||||||
|
self.number_input2_value = self.number_input2_value + 1;
|
||||||
|
self.number_input2.update(cx, |input, cx| {
|
||||||
|
input.set_value(self.number_input2_value.to_string(), cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn toggle_opt_masked(&mut self, _: &bool, cx: &mut ViewContext<Self>) {
|
fn toggle_opt_masked(&mut self, _: &bool, cx: &mut ViewContext<Self>) {
|
||||||
self.otp_masked = !self.otp_masked;
|
self.otp_masked = !self.otp_masked;
|
||||||
self.otp_input
|
self.otp_input
|
||||||
|
|
@ -241,7 +332,15 @@ impl Render for InputStory {
|
||||||
.child(
|
.child(
|
||||||
section("Normal Input", cx)
|
section("Normal Input", cx)
|
||||||
.child(self.input1.clone())
|
.child(self.input1.clone())
|
||||||
.child(self.input2.clone()),
|
.child(self.input2.clone())
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.gap_y_4()
|
||||||
|
.w_full()
|
||||||
|
.child("Number Input")
|
||||||
|
.child(self.number_input1.clone())
|
||||||
|
.child(self.number_input2.clone()),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
section("Input State", cx)
|
section("Input State", cx)
|
||||||
|
|
|
||||||
|
|
@ -285,7 +285,7 @@ impl TextInput {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the placeholder text without ownership.
|
/// Set the placeholder text of the input field with reference.
|
||||||
pub fn set_placeholder(&mut self, placeholder: impl Into<SharedString>) {
|
pub fn set_placeholder(&mut self, placeholder: impl Into<SharedString>) {
|
||||||
self.placeholder = placeholder.into();
|
self.placeholder = placeholder.into();
|
||||||
}
|
}
|
||||||
|
|
@ -302,6 +302,11 @@ impl TextInput {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the regular expression pattern of the input field with reference.
|
||||||
|
pub fn set_pattern(&mut self, pattern: regex::Regex) {
|
||||||
|
self.pattern = Some(pattern);
|
||||||
|
}
|
||||||
|
|
||||||
/// Set the validation function of the input field.
|
/// Set the validation function of the input field.
|
||||||
pub fn validate(mut self, f: impl Fn(&str) -> bool + 'static) -> Self {
|
pub fn validate(mut self, f: impl Fn(&str) -> bool + 'static) -> Self {
|
||||||
self.validate = Some(Box::new(f));
|
self.validate = Some(Box::new(f));
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ pub mod link;
|
||||||
pub mod list;
|
pub mod list;
|
||||||
pub mod modal;
|
pub mod modal;
|
||||||
pub mod notification;
|
pub mod notification;
|
||||||
|
pub mod number_input;
|
||||||
pub mod popover;
|
pub mod popover;
|
||||||
pub mod popup_menu;
|
pub mod popup_menu;
|
||||||
pub mod prelude;
|
pub mod prelude;
|
||||||
|
|
@ -82,6 +83,7 @@ pub fn init(cx: &mut gpui::AppContext) {
|
||||||
drawer::init(cx);
|
drawer::init(cx);
|
||||||
dropdown::init(cx);
|
dropdown::init(cx);
|
||||||
input::init(cx);
|
input::init(cx);
|
||||||
|
number_input::init(cx);
|
||||||
list::init(cx);
|
list::init(cx);
|
||||||
modal::init(cx);
|
modal::init(cx);
|
||||||
popover::init(cx);
|
popover::init(cx);
|
||||||
|
|
|
||||||
168
crates/ui/src/number_input.rs
Normal file
168
crates/ui/src/number_input.rs
Normal file
|
|
@ -0,0 +1,168 @@
|
||||||
|
use gpui::{
|
||||||
|
actions, AppContext, EventEmitter, FocusHandle, FocusableView, InteractiveElement, IntoElement,
|
||||||
|
KeyBinding, ParentElement, Render, SharedString, Styled, Subscription, View, ViewContext,
|
||||||
|
VisualContext,
|
||||||
|
};
|
||||||
|
use regex::Regex;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
button::{Button, ButtonStyled as _},
|
||||||
|
h_flex,
|
||||||
|
input::{InputEvent, TextInput},
|
||||||
|
prelude::FluentBuilder,
|
||||||
|
theme::ActiveTheme,
|
||||||
|
IconName, Sizable, Size, StyledExt,
|
||||||
|
};
|
||||||
|
|
||||||
|
actions!(number_input, [Increment, Decrement]);
|
||||||
|
|
||||||
|
const KEY_CONTENT: &str = "NumberInput";
|
||||||
|
|
||||||
|
pub fn init(cx: &mut AppContext) {
|
||||||
|
cx.bind_keys(vec![
|
||||||
|
KeyBinding::new("up", Increment, Some(KEY_CONTENT)),
|
||||||
|
KeyBinding::new("down", Decrement, Some(KEY_CONTENT)),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct NumberInput {
|
||||||
|
input: View<TextInput>,
|
||||||
|
_subscriptions: Vec<Subscription>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NumberInput {
|
||||||
|
pub fn new(cx: &mut ViewContext<Self>) -> Self {
|
||||||
|
// Default pattern for the number input.
|
||||||
|
let pattern = Regex::new(r"^-?(\d+)?\.?(\d+)?$").unwrap();
|
||||||
|
|
||||||
|
let input = cx.new_view(|cx| TextInput::new(cx).pattern(pattern).appearance(false));
|
||||||
|
|
||||||
|
let _subscriptions = vec![cx.subscribe(&input, |_, _, event: &InputEvent, cx| {
|
||||||
|
cx.emit(NumberInputEvent::Input(event.clone()));
|
||||||
|
})];
|
||||||
|
|
||||||
|
Self {
|
||||||
|
input,
|
||||||
|
_subscriptions,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn placeholder(
|
||||||
|
self,
|
||||||
|
placeholder: impl Into<SharedString>,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) -> Self {
|
||||||
|
self.input
|
||||||
|
.update(cx, |input, _| input.set_placeholder(placeholder));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_placeholder(&self, text: impl Into<SharedString>, cx: &mut ViewContext<Self>) {
|
||||||
|
self.input.update(cx, |input, _| {
|
||||||
|
input.set_placeholder(text);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pattern(self, pattern: regex::Regex, cx: &mut ViewContext<Self>) -> Self {
|
||||||
|
self.input.update(cx, |input, _| input.set_pattern(pattern));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_size(self, size: Size, cx: &mut ViewContext<Self>) -> Self {
|
||||||
|
self.input.update(cx, |input, cx| input.set_size(size, cx));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn small(self, cx: &mut ViewContext<Self>) -> Self {
|
||||||
|
self.set_size(Size::Small, cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn xsmall(self, cx: &mut ViewContext<Self>) -> Self {
|
||||||
|
self.set_size(Size::XSmall, cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn large(self, cx: &mut ViewContext<Self>) -> Self {
|
||||||
|
self.set_size(Size::Large, cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_value(&self, text: impl Into<SharedString>, cx: &mut ViewContext<Self>) {
|
||||||
|
self.input.update(cx, |input, cx| input.set_text(text, cx))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_disabled(&self, disabled: bool, cx: &mut ViewContext<Self>) {
|
||||||
|
self.input
|
||||||
|
.update(cx, |input, cx| input.set_disabled(disabled, cx));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn increment(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
|
self.handle_increment(&Increment, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decrement(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
|
self.handle_decrement(&Decrement, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_increment(&mut self, _: &Increment, cx: &mut ViewContext<Self>) {
|
||||||
|
self.on_step(StepAction::Increment, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_decrement(&mut self, _: &Decrement, cx: &mut ViewContext<Self>) {
|
||||||
|
self.on_step(StepAction::Decrement, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_step(&mut self, action: StepAction, cx: &mut ViewContext<Self>) {
|
||||||
|
cx.emit(NumberInputEvent::Step(action));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FocusableView for NumberInput {
|
||||||
|
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
||||||
|
self.input.focus_handle(cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum StepAction {
|
||||||
|
Decrement,
|
||||||
|
Increment,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum NumberInputEvent {
|
||||||
|
Input(InputEvent),
|
||||||
|
Step(StepAction),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventEmitter<NumberInputEvent> for NumberInput {}
|
||||||
|
|
||||||
|
impl Render for NumberInput {
|
||||||
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
|
let focused = self.input.focus_handle(cx).is_focused(cx);
|
||||||
|
|
||||||
|
h_flex()
|
||||||
|
.key_context(KEY_CONTENT)
|
||||||
|
.on_action(cx.listener(Self::handle_increment))
|
||||||
|
.on_action(cx.listener(Self::handle_decrement))
|
||||||
|
.flex_1()
|
||||||
|
.px_1()
|
||||||
|
.gap_x_3()
|
||||||
|
.bg(cx.theme().background)
|
||||||
|
.border_color(cx.theme().border)
|
||||||
|
.border_1()
|
||||||
|
.rounded_md()
|
||||||
|
.when(focused, |this| this.outline(cx))
|
||||||
|
.child(
|
||||||
|
Button::new("minus")
|
||||||
|
.ghost()
|
||||||
|
.xsmall()
|
||||||
|
.icon(IconName::Minus)
|
||||||
|
.on_click(cx.listener(|this, _, cx| this.on_step(StepAction::Decrement, cx))),
|
||||||
|
)
|
||||||
|
.child(self.input.clone())
|
||||||
|
.child(
|
||||||
|
Button::new("plus")
|
||||||
|
.ghost()
|
||||||
|
.xsmall()
|
||||||
|
.icon(IconName::Plus)
|
||||||
|
.on_click(cx.listener(|this, _, cx| this.on_step(StepAction::Increment, cx))),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue