| Before | After | | - | - | | <img width="268" height="114" alt="SCR-20251118-nasx" src="https://github.com/user-attachments/assets/d66d4178-d6f9-4f00-b3ad-94d1aee3a262" /> | <img width="268" height="112" alt="SCR-20251118-nazg" src="https://github.com/user-attachments/assets/df1bdc1d-ee8b-4249-b301-4479145f393a" /> |
222 lines
7.1 KiB
Rust
222 lines
7.1 KiB
Rust
use gpui::{
|
|
actions, prelude::FluentBuilder as _, AnyElement, App, Context, Corners, Edges, Entity,
|
|
EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement, KeyBinding,
|
|
ParentElement, RenderOnce, SharedString, StyleRefinement, Styled, Window,
|
|
};
|
|
|
|
use crate::{
|
|
button::Button, h_flex, ActiveTheme, Disableable, IconName, Sizable, Size, StyledExt as _,
|
|
};
|
|
|
|
use super::{Input, InputState};
|
|
|
|
actions!(number_input, [Increment, Decrement]);
|
|
|
|
const CONTEXT: &str = "NumberInput";
|
|
pub fn init(cx: &mut App) {
|
|
cx.bind_keys(vec![
|
|
KeyBinding::new("up", Increment, Some(CONTEXT)),
|
|
KeyBinding::new("down", Decrement, Some(CONTEXT)),
|
|
]);
|
|
}
|
|
|
|
/// A number input element with increment and decrement buttons.
|
|
#[derive(IntoElement)]
|
|
pub struct NumberInput {
|
|
state: Entity<InputState>,
|
|
placeholder: SharedString,
|
|
size: Size,
|
|
prefix: Option<AnyElement>,
|
|
suffix: Option<AnyElement>,
|
|
appearance: bool,
|
|
disabled: bool,
|
|
style: StyleRefinement,
|
|
}
|
|
|
|
impl NumberInput {
|
|
/// Create a new [`NumberInput`] element bind to the [`InputState`].
|
|
pub fn new(state: &Entity<InputState>) -> Self {
|
|
Self {
|
|
state: state.clone(),
|
|
size: Size::default(),
|
|
placeholder: SharedString::default(),
|
|
prefix: None,
|
|
suffix: None,
|
|
appearance: true,
|
|
disabled: false,
|
|
style: StyleRefinement::default(),
|
|
}
|
|
}
|
|
|
|
/// Set the placeholder text of the number input.
|
|
pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
|
|
self.placeholder = placeholder.into();
|
|
self
|
|
}
|
|
|
|
/// Set the prefix element of the number input.
|
|
pub fn prefix(mut self, prefix: impl IntoElement) -> Self {
|
|
self.prefix = Some(prefix.into_any_element());
|
|
self
|
|
}
|
|
|
|
/// Set the suffix element of the number input.
|
|
pub fn suffix(mut self, suffix: impl IntoElement) -> Self {
|
|
self.suffix = Some(suffix.into_any_element());
|
|
self
|
|
}
|
|
|
|
/// Set the appearance of the number input, if false will no border and background.
|
|
pub fn appearance(mut self, appearance: bool) -> Self {
|
|
self.appearance = appearance;
|
|
self
|
|
}
|
|
|
|
fn on_increment(state: &Entity<InputState>, window: &mut Window, cx: &mut App) {
|
|
state.update(cx, |state, cx| {
|
|
state.focus(window, cx);
|
|
state.on_action_increment(&Increment, window, cx);
|
|
})
|
|
}
|
|
|
|
fn on_decrement(state: &Entity<InputState>, window: &mut Window, cx: &mut App) {
|
|
state.update(cx, |state, cx| {
|
|
state.focus(window, cx);
|
|
state.on_action_decrement(&Decrement, window, cx);
|
|
})
|
|
}
|
|
}
|
|
|
|
impl Disableable for NumberInput {
|
|
fn disabled(mut self, disabled: bool) -> Self {
|
|
self.disabled = disabled;
|
|
self
|
|
}
|
|
}
|
|
|
|
impl InputState {
|
|
fn on_action_increment(&mut self, _: &Increment, window: &mut Window, cx: &mut Context<Self>) {
|
|
self.on_number_input_step(StepAction::Increment, window, cx);
|
|
}
|
|
|
|
fn on_action_decrement(&mut self, _: &Decrement, window: &mut Window, cx: &mut Context<Self>) {
|
|
self.on_number_input_step(StepAction::Decrement, window, cx);
|
|
}
|
|
|
|
fn on_number_input_step(&mut self, action: StepAction, _: &mut Window, cx: &mut Context<Self>) {
|
|
if self.disabled {
|
|
return;
|
|
}
|
|
|
|
cx.emit(NumberInputEvent::Step(action));
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
|
pub enum StepAction {
|
|
Decrement,
|
|
Increment,
|
|
}
|
|
pub enum NumberInputEvent {
|
|
Step(StepAction),
|
|
}
|
|
impl EventEmitter<NumberInputEvent> for InputState {}
|
|
|
|
impl Focusable for NumberInput {
|
|
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
|
self.state.focus_handle(cx)
|
|
}
|
|
}
|
|
|
|
impl Sizable for NumberInput {
|
|
fn with_size(mut self, size: impl Into<Size>) -> Self {
|
|
self.size = size.into();
|
|
self
|
|
}
|
|
}
|
|
|
|
impl Styled for NumberInput {
|
|
fn style(&mut self) -> &mut StyleRefinement {
|
|
&mut self.style
|
|
}
|
|
}
|
|
|
|
impl RenderOnce for NumberInput {
|
|
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
|
h_flex()
|
|
.id(("number-input", self.state.entity_id()))
|
|
.key_context(CONTEXT)
|
|
.on_action(window.listener_for(&self.state, InputState::on_action_increment))
|
|
.on_action(window.listener_for(&self.state, InputState::on_action_decrement))
|
|
.flex_1()
|
|
.rounded(cx.theme().radius)
|
|
.refine_style(&self.style)
|
|
.when(self.disabled, |this| this.bg(cx.theme().muted))
|
|
.child(
|
|
Button::new("-")
|
|
.outline()
|
|
.with_size(self.size)
|
|
.icon(IconName::Minus)
|
|
.compact()
|
|
.tab_stop(false)
|
|
.disabled(self.disabled)
|
|
.border_color(cx.theme().input)
|
|
.border_corners(Corners {
|
|
top_left: true,
|
|
top_right: false,
|
|
bottom_right: false,
|
|
bottom_left: true,
|
|
})
|
|
.border_edges(Edges {
|
|
top: self.appearance,
|
|
right: false,
|
|
bottom: self.appearance,
|
|
left: self.appearance,
|
|
})
|
|
.on_click({
|
|
let state = self.state.clone();
|
|
move |_, window, cx| {
|
|
Self::on_decrement(&state, window, cx);
|
|
}
|
|
}),
|
|
)
|
|
.child(
|
|
Input::new(&self.state)
|
|
.appearance(self.appearance)
|
|
.with_size(self.size)
|
|
.disabled(self.disabled)
|
|
.gap_0()
|
|
.rounded_none()
|
|
.when_some(self.prefix, |this, prefix| this.prefix(prefix))
|
|
.when_some(self.suffix, |this, suffix| this.suffix(suffix)),
|
|
)
|
|
.child(
|
|
Button::new("+")
|
|
.outline()
|
|
.with_size(self.size)
|
|
.icon(IconName::Plus)
|
|
.compact()
|
|
.tab_stop(false)
|
|
.disabled(self.disabled)
|
|
.border_color(cx.theme().input)
|
|
.border_corners(Corners {
|
|
top_left: false,
|
|
top_right: true,
|
|
bottom_right: true,
|
|
bottom_left: false,
|
|
})
|
|
.border_edges(Edges {
|
|
top: self.appearance,
|
|
right: self.appearance,
|
|
bottom: self.appearance,
|
|
left: false,
|
|
})
|
|
.on_click({
|
|
let state = self.state.clone();
|
|
move |_, window, cx| {
|
|
Self::on_increment(&state, window, cx);
|
|
}
|
|
}),
|
|
)
|
|
}
|
|
}
|