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::{InputState, TextInput};
13
14actions!(number_input, [Increment, Decrement]);
15
16const KEY_CONTENT: &str = "NumberInput";
17
18pub fn init(cx: &mut App) {
19    cx.bind_keys(vec![
20        KeyBinding::new("up", Increment, Some(KEY_CONTENT)),
21        KeyBinding::new("down", Decrement, Some(KEY_CONTENT)),
22    ]);
23}
24
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    pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
53        self.placeholder = placeholder.into();
54        self
55    }
56
57    pub fn size(mut self, size: impl Into<Size>) -> Self {
58        self.size = size.into();
59        self
60    }
61
62    pub fn increment(state: &Entity<InputState>, window: &mut Window, cx: &mut App) {
63        state.update(cx, |state, cx| {
64            state.on_action_increment(&Increment, window, cx);
65        })
66    }
67
68    pub fn decrement(state: &Entity<InputState>, window: &mut Window, cx: &mut App) {
69        state.update(cx, |state, cx| {
70            state.on_action_decrement(&Decrement, window, cx);
71        })
72    }
73
74    pub fn prefix(mut self, prefix: impl IntoElement) -> Self {
75        self.prefix = Some(prefix.into_any_element());
76        self
77    }
78
79    pub fn suffix(mut self, suffix: impl IntoElement) -> Self {
80        self.suffix = Some(suffix.into_any_element());
81        self
82    }
83
84    /// Set the appearance of the number input, if false will no border and background.
85    pub fn appearance(mut self, appearance: bool) -> Self {
86        self.appearance = appearance;
87        self
88    }
89}
90
91impl Disableable for NumberInput {
92    fn disabled(mut self, disabled: bool) -> Self {
93        self.disabled = disabled;
94        self
95    }
96}
97
98impl InputState {
99    fn on_action_increment(&mut self, _: &Increment, window: &mut Window, cx: &mut Context<Self>) {
100        self.on_number_input_step(StepAction::Increment, window, cx);
101    }
102
103    fn on_action_decrement(&mut self, _: &Decrement, window: &mut Window, cx: &mut Context<Self>) {
104        self.on_number_input_step(StepAction::Decrement, window, cx);
105    }
106
107    fn on_number_input_step(&mut self, action: StepAction, _: &mut Window, cx: &mut Context<Self>) {
108        if self.disabled {
109            return;
110        }
111
112        cx.emit(NumberInputEvent::Step(action));
113    }
114}
115
116#[derive(Clone, Copy, PartialEq, Eq)]
117pub enum StepAction {
118    Decrement,
119    Increment,
120}
121pub enum NumberInputEvent {
122    Step(StepAction),
123}
124impl EventEmitter<NumberInputEvent> for InputState {}
125
126impl Focusable for NumberInput {
127    fn focus_handle(&self, cx: &App) -> FocusHandle {
128        self.state.focus_handle(cx)
129    }
130}
131
132impl Sizable for NumberInput {
133    fn with_size(mut self, size: impl Into<Size>) -> Self {
134        self.size = size.into();
135        self
136    }
137}
138
139impl Styled for NumberInput {
140    fn style(&mut self) -> &mut StyleRefinement {
141        &mut self.style
142    }
143}
144
145impl RenderOnce for NumberInput {
146    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
147        let focused = self.state.focus_handle(cx).is_focused(window);
148
149        h_flex()
150            .id(("number-input", self.state.entity_id()))
151            .key_context(KEY_CONTENT)
152            .on_action(window.listener_for(&self.state, InputState::on_action_increment))
153            .on_action(window.listener_for(&self.state, InputState::on_action_decrement))
154            .flex_1()
155            .input_size(self.size)
156            .px(self.size.input_px() / 2.)
157            .when(self.appearance, |this| {
158                this.bg(cx.theme().background)
159                    .border_color(cx.theme().input)
160                    .border_1()
161                    .rounded(cx.theme().radius)
162                    .refine_style(&self.style)
163            })
164            .when(self.disabled, |this| this.bg(cx.theme().muted))
165            .when(focused, |this| this.focused_border(cx))
166            .child(
167                Button::new("minus")
168                    .ghost()
169                    .with_size(self.size.smaller())
170                    .icon(IconName::Minus)
171                    .compact()
172                    .tab_stop(false)
173                    .disabled(self.disabled)
174                    .on_click({
175                        let state = self.state.clone();
176                        move |_, window, cx| {
177                            Self::decrement(&state, window, cx);
178                        }
179                    }),
180            )
181            .child(
182                TextInput::new(&self.state)
183                    .appearance(false)
184                    .disabled(self.disabled)
185                    .px(px(2.))
186                    .gap_0()
187                    .when_some(self.prefix, |this, prefix| this.prefix(prefix))
188                    .when_some(self.suffix, |this, suffix| this.suffix(suffix)),
189            )
190            .child(
191                Button::new("plus")
192                    .ghost()
193                    .with_size(self.size.smaller())
194                    .icon(IconName::Plus)
195                    .compact()
196                    .tab_stop(false)
197                    .disabled(self.disabled)
198                    .on_click({
199                        let state = self.state.clone();
200                        move |_, window, cx| {
201                            Self::increment(&state, window, cx);
202                        }
203                    }),
204            )
205    }
206}