gpui_component/input/
number_input.rs

1use gpui::{
2    actions, prelude::FluentBuilder as _, px, AnyElement, App, Context, Entity, EventEmitter,
3    FocusHandle, Focusable, InteractiveElement, IntoElement, KeyBinding, ParentElement, RenderOnce,
4    SharedString, StyleRefinement, Styled, Window,
5};
6
7use crate::{
8    button::{Button, ButtonVariants as _},
9    h_flex, ActiveTheme, Disableable, IconName, Sizable, Size, StyleSized, StyledExt as _,
10};
11
12use super::{Input, InputState};
13
14actions!(number_input, [Increment, Decrement]);
15
16const CONTEXT: &str = "NumberInput";
17pub fn init(cx: &mut App) {
18    cx.bind_keys(vec![
19        KeyBinding::new("up", Increment, Some(CONTEXT)),
20        KeyBinding::new("down", Decrement, Some(CONTEXT)),
21    ]);
22}
23
24/// A number input element with increment and decrement buttons.
25#[derive(IntoElement)]
26pub struct NumberInput {
27    state: Entity<InputState>,
28    placeholder: SharedString,
29    size: Size,
30    prefix: Option<AnyElement>,
31    suffix: Option<AnyElement>,
32    appearance: bool,
33    disabled: bool,
34    style: StyleRefinement,
35}
36
37impl NumberInput {
38    /// Create a new [`NumberInput`] element bind to the [`InputState`].
39    pub fn new(state: &Entity<InputState>) -> Self {
40        Self {
41            state: state.clone(),
42            size: Size::default(),
43            placeholder: SharedString::default(),
44            prefix: None,
45            suffix: None,
46            appearance: true,
47            disabled: false,
48            style: StyleRefinement::default(),
49        }
50    }
51
52    /// Set the placeholder text of the number input.
53    pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
54        self.placeholder = placeholder.into();
55        self
56    }
57
58    /// Set the prefix element of the number input.
59    pub fn prefix(mut self, prefix: impl IntoElement) -> Self {
60        self.prefix = Some(prefix.into_any_element());
61        self
62    }
63
64    /// Set the suffix element of the number input.
65    pub fn suffix(mut self, suffix: impl IntoElement) -> Self {
66        self.suffix = Some(suffix.into_any_element());
67        self
68    }
69
70    /// Set the appearance of the number input, if false will no border and background.
71    pub fn appearance(mut self, appearance: bool) -> Self {
72        self.appearance = appearance;
73        self
74    }
75
76    fn on_increment(state: &Entity<InputState>, window: &mut Window, cx: &mut App) {
77        state.update(cx, |state, cx| {
78            state.on_action_increment(&Increment, window, cx);
79        })
80    }
81
82    fn on_decrement(state: &Entity<InputState>, window: &mut Window, cx: &mut App) {
83        state.update(cx, |state, cx| {
84            state.on_action_decrement(&Decrement, window, cx);
85        })
86    }
87}
88
89impl Disableable for NumberInput {
90    fn disabled(mut self, disabled: bool) -> Self {
91        self.disabled = disabled;
92        self
93    }
94}
95
96impl InputState {
97    fn on_action_increment(&mut self, _: &Increment, window: &mut Window, cx: &mut Context<Self>) {
98        self.on_number_input_step(StepAction::Increment, window, cx);
99    }
100
101    fn on_action_decrement(&mut self, _: &Decrement, window: &mut Window, cx: &mut Context<Self>) {
102        self.on_number_input_step(StepAction::Decrement, window, cx);
103    }
104
105    fn on_number_input_step(&mut self, action: StepAction, _: &mut Window, cx: &mut Context<Self>) {
106        if self.disabled {
107            return;
108        }
109
110        cx.emit(NumberInputEvent::Step(action));
111    }
112}
113
114#[derive(Clone, Copy, PartialEq, Eq)]
115pub enum StepAction {
116    Decrement,
117    Increment,
118}
119pub enum NumberInputEvent {
120    Step(StepAction),
121}
122impl EventEmitter<NumberInputEvent> for InputState {}
123
124impl Focusable for NumberInput {
125    fn focus_handle(&self, cx: &App) -> FocusHandle {
126        self.state.focus_handle(cx)
127    }
128}
129
130impl Sizable for NumberInput {
131    fn with_size(mut self, size: impl Into<Size>) -> Self {
132        self.size = size.into();
133        self
134    }
135}
136
137impl Styled for NumberInput {
138    fn style(&mut self) -> &mut StyleRefinement {
139        &mut self.style
140    }
141}
142
143impl RenderOnce for NumberInput {
144    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
145        let focused = self.state.focus_handle(cx).is_focused(window);
146
147        h_flex()
148            .id(("number-input", self.state.entity_id()))
149            .key_context(CONTEXT)
150            .on_action(window.listener_for(&self.state, InputState::on_action_increment))
151            .on_action(window.listener_for(&self.state, InputState::on_action_decrement))
152            .flex_1()
153            .input_size(self.size)
154            .px(self.size.input_px() / 2.)
155            .when(self.appearance, |this| {
156                this.bg(cx.theme().background)
157                    .border_color(cx.theme().input)
158                    .border_1()
159                    .rounded(cx.theme().radius)
160                    .refine_style(&self.style)
161            })
162            .when(self.disabled, |this| this.bg(cx.theme().muted))
163            .when(focused, |this| this.focused_border(cx))
164            .child(
165                Button::new("-")
166                    .ghost()
167                    .with_size(self.size.smaller())
168                    .icon(IconName::Minus)
169                    .compact()
170                    .tab_stop(false)
171                    .disabled(self.disabled)
172                    .on_click({
173                        let state = self.state.clone();
174                        move |_, window, cx| {
175                            Self::on_decrement(&state, window, cx);
176                        }
177                    }),
178            )
179            .child(
180                Input::new(&self.state)
181                    .appearance(false)
182                    .disabled(self.disabled)
183                    .px(px(2.))
184                    .gap_0()
185                    .when_some(self.prefix, |this, prefix| this.prefix(prefix))
186                    .when_some(self.suffix, |this, suffix| this.suffix(suffix)),
187            )
188            .child(
189                Button::new("+")
190                    .ghost()
191                    .with_size(self.size.smaller())
192                    .icon(IconName::Plus)
193                    .compact()
194                    .tab_stop(false)
195                    .disabled(self.disabled)
196                    .on_click({
197                        let state = self.state.clone();
198                        move |_, window, cx| {
199                            Self::on_increment(&state, window, cx);
200                        }
201                    }),
202            )
203    }
204}