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 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 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}