gpui_component/input/
number_input.rs

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