gpui-component/crates/ui/src/input/number_input.rs
2025-02-17 14:29:33 +08:00

206 lines
6.1 KiB
Rust

use gpui::{
actions, prelude::FluentBuilder as _, px, App, AppContext as _, Context, Entity, EventEmitter,
FocusHandle, Focusable, InteractiveElement, IntoElement, KeyBinding, ParentElement, Render,
SharedString, Styled, Subscription, Window,
};
use regex::Regex;
use crate::{
button::{Button, ButtonVariants as _},
h_flex,
input::{InputEvent, TextInput},
ActiveTheme, IconName, Sizable, Size, StyleSized, StyledExt,
};
actions!(number_input, [Increment, Decrement]);
const KEY_CONTENT: &str = "NumberInput";
pub fn init(cx: &mut App) {
cx.bind_keys(vec![
KeyBinding::new("up", Increment, Some(KEY_CONTENT)),
KeyBinding::new("down", Decrement, Some(KEY_CONTENT)),
]);
}
pub struct NumberInput {
input: Entity<TextInput>,
size: Size,
_subscriptions: Vec<Subscription>,
_synced_size: bool,
}
impl NumberInput {
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
// Default pattern for the number input.
let pattern = Regex::new(r"^-?(\d+)?\.?(\d+)?$").unwrap();
let input = cx.new(|cx| {
TextInput::new(window, cx)
.pattern(pattern)
.appearance(false)
});
let _subscriptions = vec![cx.subscribe(&input, |_, _, event: &InputEvent, cx| {
cx.emit(NumberInputEvent::Input(event.clone()));
})];
Self {
input,
size: Size::default(),
_synced_size: false,
_subscriptions,
}
}
pub fn placeholder(
self,
placeholder: impl Into<SharedString>,
_: &mut Window,
cx: &mut Context<Self>,
) -> Self {
self.input
.update(cx, |input, _| input.set_placeholder(placeholder));
self
}
pub fn set_size(&mut self, size: Size, window: &mut Window, cx: &mut Context<Self>) {
self.size = size;
self.sync_size_to_input_if_needed(window, cx);
}
pub fn set_placeholder(
&self,
text: impl Into<SharedString>,
_window: &mut Window,
cx: &mut Context<Self>,
) {
self.input.update(cx, |input, _| {
input.set_placeholder(text);
});
}
pub fn pattern(self, pattern: regex::Regex, _: &mut Window, cx: &mut Context<Self>) -> Self {
self.input.update(cx, |input, _| input.set_pattern(pattern));
self
}
pub fn set_value(
&self,
text: impl Into<SharedString>,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.input
.update(cx, |input, cx| input.set_text(text, window, cx))
}
pub fn set_disabled(&self, disabled: bool, window: &mut Window, cx: &mut Context<Self>) {
self.input
.update(cx, |input, cx| input.set_disabled(disabled, window, cx));
}
pub fn increment(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.on_action_increment(&Increment, window, cx);
}
pub fn decrement(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.on_action_decrement(&Decrement, window, cx);
}
fn on_action_increment(&mut self, _: &Increment, window: &mut Window, cx: &mut Context<Self>) {
self.on_step(StepAction::Increment, window, cx);
}
fn on_action_decrement(&mut self, _: &Decrement, window: &mut Window, cx: &mut Context<Self>) {
self.on_step(StepAction::Decrement, window, cx);
}
fn on_step(&mut self, action: StepAction, _: &mut Window, cx: &mut Context<Self>) {
if self.input.read(cx).disabled {
return;
}
cx.emit(NumberInputEvent::Step(action));
}
fn sync_size_to_input_if_needed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if !self._synced_size {
self.input
.update(cx, |input, cx| input.set_size(self.size, window, cx));
self._synced_size = true;
}
}
}
impl Focusable for NumberInput {
fn focus_handle(&self, cx: &App) -> FocusHandle {
self.input.focus_handle(cx)
}
}
pub enum StepAction {
Decrement,
Increment,
}
pub enum NumberInputEvent {
Input(InputEvent),
Step(StepAction),
}
impl EventEmitter<NumberInputEvent> for NumberInput {}
impl Sizable for NumberInput {
fn with_size(mut self, size: impl Into<Size>) -> Self {
self.size = size.into();
self
}
}
impl Render for NumberInput {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let focused = self.input.focus_handle(cx).is_focused(window);
// Sync size to input at first.
self.sync_size_to_input_if_needed(window, cx);
let btn_size = match self.size {
Size::XSmall | Size::Small => Size::XSmall,
_ => Size::Small,
};
h_flex()
.key_context(KEY_CONTENT)
.on_action(cx.listener(Self::on_action_increment))
.on_action(cx.listener(Self::on_action_decrement))
.flex_1()
.input_size(self.size)
.px(match self.size {
Size::XSmall => px(1.),
Size::Small => px(2.),
_ => px(3.),
})
.bg(cx.theme().background)
.border_color(cx.theme().input)
.border_1()
.rounded(cx.theme().radius)
.when(focused, |this| this.outline(cx))
.child(
Button::new("minus")
.ghost()
.with_size(btn_size)
.icon(IconName::Minus)
.on_click(cx.listener(|this, _, window, cx| {
this.on_step(StepAction::Decrement, window, cx)
})),
)
.child(self.input.clone())
.child(
Button::new("plus")
.ghost()
.with_size(btn_size)
.icon(IconName::Plus)
.on_click(cx.listener(|this, _, window, cx| {
this.on_step(StepAction::Increment, window, cx)
})),
)
}
}