1use crate::theme::ActiveTheme;
2use gpui::{
3 AnyElement, App, Entity, FocusHandle, Focusable, InteractiveElement as _,
4 StatefulInteractiveElement as _, Window, div, px,
5};
6use gpui::{
7 IntoElement, ParentElement, RenderOnce, SharedString, StyleRefinement, Styled, TextAlign,
8 prelude::FluentBuilder as _,
9};
10
11use crate::{Disableable, Icon, IconName, Sizable, Size, StyleSized as _, StyledExt as _};
12
13use super::{Input, InputState, input::input_style};
14use crate::ThemeStyled as _;
15use gpui_base::NumberInput as BaseNumberInput;
16pub use gpui_base::{NumberInputEvent, NumberStep, StepAction};
17use rust_i18n::t;
18
19#[derive(IntoElement)]
21pub struct NumberInput {
22 state: Entity<InputState>,
23 placeholder: SharedString,
24 size: Size,
25 prefix: Option<AnyElement>,
26 suffix: Option<AnyElement>,
27 appearance: bool,
28 focus_ring_enabled: bool,
29 disabled: bool,
30 accessibility_label: Option<SharedString>,
31 style: StyleRefinement,
32}
33
34impl NumberInput {
35 pub fn new(state: &Entity<InputState>) -> Self {
37 Self {
38 state: state.clone(),
39 size: Size::default(),
40 placeholder: SharedString::default(),
41 prefix: None,
42 suffix: None,
43 appearance: true,
44 focus_ring_enabled: true,
45 disabled: false,
46 accessibility_label: None,
47 style: StyleRefinement::default(),
48 }
49 }
50
51 pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
53 self.placeholder = placeholder.into();
54 self
55 }
56
57 pub fn prefix(mut self, prefix: impl IntoElement) -> Self {
59 self.prefix = Some(prefix.into_any_element());
60 self
61 }
62
63 pub fn suffix(mut self, suffix: impl IntoElement) -> Self {
65 self.suffix = Some(suffix.into_any_element());
66 self
67 }
68
69 pub fn appearance(mut self, appearance: bool) -> Self {
71 self.appearance = appearance;
72 self
73 }
74
75 pub fn accessibility_label(mut self, label: impl Into<SharedString>) -> Self {
81 self.accessibility_label = Some(label.into());
82 self
83 }
84}
85
86impl Disableable for NumberInput {
87 fn disabled(mut self, disabled: bool) -> Self {
88 self.disabled = disabled;
89 self
90 }
91}
92
93impl crate::FocusableExt for NumberInput {
94 fn focus_ring(mut self, enabled: bool) -> Self {
95 self.focus_ring_enabled = enabled;
96 self
97 }
98
99 fn is_focus_ring_enabled(&self) -> bool {
100 self.focus_ring_enabled
101 }
102}
103
104impl Focusable for NumberInput {
105 fn focus_handle(&self, cx: &App) -> FocusHandle {
106 self.state.focus_handle(cx)
107 }
108}
109
110impl Sizable for NumberInput {
111 fn with_size(mut self, size: impl Into<Size>) -> Self {
112 self.size = size.into();
113 self
114 }
115}
116
117impl Styled for NumberInput {
118 fn style(&mut self) -> &mut StyleRefinement {
119 &mut self.style
120 }
121}
122
123impl RenderOnce for NumberInput {
124 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
125 let focused = self.state.read(cx).focus_handle(cx).is_focused(window) && !self.disabled;
126 let (bg, _) = input_style(self.disabled, cx);
127 let border_color = if self.disabled {
128 cx.theme().input.opacity(0.5)
129 } else {
130 cx.theme().input
131 };
132 let button_foreground = cx.theme().secondary_foreground;
134 let button_hover = cx.theme().input.opacity(0.4);
135 let button_active = cx.theme().input.opacity(0.6);
136 let button_size = self.size;
137 let button_radius = if self.appearance {
140 (cx.theme().radius - px(1.)).max(px(0.))
141 } else {
142 cx.theme().radius
143 };
144 let base_state = self.state.clone();
145 let content = BaseNumberInput::new(&base_state)
146 .disabled(self.disabled)
147 .when_some(self.accessibility_label, |this, label| {
148 this.accessibility_label(label)
149 })
150 .size_full()
151 .decrement_button(move |this| {
152 this.accessibility_label(t!("Input.Decrement"))
153 .flex()
154 .items_center()
155 .justify_center()
156 .text_color(button_foreground)
157 .hover(move |this| this.bg(button_hover))
158 .active(move |this| this.bg(button_active))
159 .h_full()
162 .map(|this| match button_size {
163 Size::XSmall | Size::Small => this.min_w_6(),
164 Size::Medium | Size::Large => this.min_w_8(),
165 Size::Size(size) => this.min_w(size),
166 })
167 .rounded_tl(button_radius)
169 .rounded_bl(button_radius)
170 .child(Icon::new(IconName::Minus).with_size(button_size))
171 })
172 .input(
173 Input::new(&self.state)
174 .appearance(false)
175 .with_size(button_size)
176 .h_full()
177 .disabled(self.disabled)
178 .gap_0()
179 .rounded_none()
180 .text_align(TextAlign::Center)
181 .when_some(self.prefix, |this, prefix| this.prefix(prefix))
182 .when_some(self.suffix, |this, suffix| this.suffix(suffix)),
183 )
184 .increment_button(move |this| {
185 this.accessibility_label(t!("Input.Increment"))
186 .flex()
187 .items_center()
188 .justify_center()
189 .text_color(button_foreground)
190 .hover(move |this| this.bg(button_hover))
191 .active(move |this| this.bg(button_active))
192 .h_full()
193 .map(|this| match button_size {
194 Size::XSmall | Size::Small => this.min_w_6(),
195 Size::Medium | Size::Large => this.min_w_8(),
196 Size::Size(size) => this.min_w(size),
197 })
198 .rounded_tr(button_radius)
199 .rounded_br(button_radius)
200 .child(Icon::new(IconName::Plus).with_size(button_size))
201 });
202
203 div()
207 .flex_1()
208 .input_h(self.size)
209 .rounded(cx.theme().radius)
210 .when(self.appearance, |this| {
211 this.bg(bg)
212 .border_1()
213 .border_color(border_color)
214 .when(focused, |this| {
215 this.border_1().border_color(cx.theme().ring)
216 })
217 })
218 .refine_style(&self.style)
219 .when(self.disabled, |this| this.opacity(0.5))
220 .child(content)
221 .when(
222 focused && self.appearance && self.focus_ring_enabled,
223 |this| this.focus_ring_style(window, cx),
224 )
225 }
226}
227
228#[cfg(test)]
229mod tests {
230 use super::StepAction;
231 use gpui_base::step_value;
232
233 #[test]
237 fn test_step_value() {
238 fn some(value: &str) -> Option<String> {
239 Some(value.to_string())
240 }
241
242 assert_eq!(
244 step_value("", StepAction::Increment, 1., None, None),
245 some("1")
246 );
247 assert_eq!(
248 step_value("", StepAction::Decrement, 1., None, None),
249 some("-1")
250 );
251 assert_eq!(
253 step_value("-", StepAction::Increment, 1., None, None),
254 some("1")
255 );
256 assert_eq!(
257 step_value("1", StepAction::Increment, 1., None, None),
258 some("2")
259 );
260 assert_eq!(
261 step_value("-2", StepAction::Increment, 1., None, None),
262 some("-1")
263 );
264
265 assert_eq!(
267 step_value("0.1", StepAction::Increment, 0.2, None, None),
268 some("0.3")
269 );
270 assert_eq!(
271 step_value("0.3", StepAction::Decrement, 0.1, None, None),
272 some("0.2")
273 );
274 assert_eq!(
276 step_value("1.25", StepAction::Increment, 1., None, None),
277 some("2.25")
278 );
279
280 assert_eq!(
282 step_value("", StepAction::Increment, 1., Some(10.), None),
283 some("10")
284 );
285 assert_eq!(
286 step_value("", StepAction::Decrement, 1., Some(10.), None),
287 some("10")
288 );
289 assert_eq!(
291 step_value("99.5", StepAction::Increment, 1., None, Some(100.)),
292 some("100.0")
293 );
294 assert_eq!(
295 step_value("1000", StepAction::Decrement, 1., None, Some(100.)),
296 some("100")
297 );
298 assert_eq!(
300 step_value("1", StepAction::Decrement, 1., Some(0.25), None),
301 some("0.25")
302 );
303
304 assert_eq!(
307 step_value("10", StepAction::Decrement, 1., Some(10.), None),
308 None
309 );
310 assert_eq!(
311 step_value("100", StepAction::Increment, 1., None, Some(100.)),
312 None
313 );
314 assert_eq!(
317 step_value("5", StepAction::Decrement, 1., Some(10.), None),
318 None
319 );
320 assert_eq!(
321 step_value("1000", StepAction::Increment, 1., None, Some(100.)),
322 None
323 );
324 }
325}