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 style: StyleRefinement,
31}
32
33impl NumberInput {
34 pub fn new(state: &Entity<InputState>) -> Self {
36 Self {
37 state: state.clone(),
38 size: Size::default(),
39 placeholder: SharedString::default(),
40 prefix: None,
41 suffix: None,
42 appearance: true,
43 focus_ring_enabled: true,
44 disabled: false,
45 style: StyleRefinement::default(),
46 }
47 }
48
49 pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
51 self.placeholder = placeholder.into();
52 self
53 }
54
55 pub fn prefix(mut self, prefix: impl IntoElement) -> Self {
57 self.prefix = Some(prefix.into_any_element());
58 self
59 }
60
61 pub fn suffix(mut self, suffix: impl IntoElement) -> Self {
63 self.suffix = Some(suffix.into_any_element());
64 self
65 }
66
67 pub fn appearance(mut self, appearance: bool) -> Self {
69 self.appearance = appearance;
70 self
71 }
72}
73
74impl Disableable for NumberInput {
75 fn disabled(mut self, disabled: bool) -> Self {
76 self.disabled = disabled;
77 self
78 }
79}
80
81impl crate::FocusableExt for NumberInput {
82 fn focus_ring(mut self, enabled: bool) -> Self {
83 self.focus_ring_enabled = enabled;
84 self
85 }
86
87 fn is_focus_ring_enabled(&self) -> bool {
88 self.focus_ring_enabled
89 }
90}
91
92impl Focusable for NumberInput {
93 fn focus_handle(&self, cx: &App) -> FocusHandle {
94 self.state.focus_handle(cx)
95 }
96}
97
98impl Sizable for NumberInput {
99 fn with_size(mut self, size: impl Into<Size>) -> Self {
100 self.size = size.into();
101 self
102 }
103}
104
105impl Styled for NumberInput {
106 fn style(&mut self) -> &mut StyleRefinement {
107 &mut self.style
108 }
109}
110
111impl RenderOnce for NumberInput {
112 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
113 let focused = self.state.read(cx).focus_handle(cx).is_focused(window) && !self.disabled;
114 let (bg, _) = input_style(self.disabled, cx);
115 let border_color = if self.disabled {
116 cx.theme().input.opacity(0.5)
117 } else {
118 cx.theme().input
119 };
120 let button_foreground = cx.theme().secondary_foreground;
122 let button_hover = cx.theme().input.opacity(0.4);
123 let button_active = cx.theme().input.opacity(0.6);
124 let button_size = self.size;
125 let button_radius = if self.appearance {
128 (cx.theme().radius - px(1.)).max(px(0.))
129 } else {
130 cx.theme().radius
131 };
132 let base_state = self.state.clone();
133 let content = BaseNumberInput::new(&base_state)
134 .disabled(self.disabled)
135 .size_full()
136 .decrement_button(move |this| {
137 this.accessibility_label(t!("Input.Decrement"))
138 .flex()
139 .items_center()
140 .justify_center()
141 .text_color(button_foreground)
142 .hover(move |this| this.bg(button_hover))
143 .active(move |this| this.bg(button_active))
144 .h_full()
147 .map(|this| match button_size {
148 Size::XSmall | Size::Small => this.min_w_6(),
149 Size::Medium | Size::Large => this.min_w_8(),
150 Size::Size(size) => this.min_w(size),
151 })
152 .rounded_tl(button_radius)
154 .rounded_bl(button_radius)
155 .child(Icon::new(IconName::Minus).with_size(button_size))
156 })
157 .input(
158 Input::new(&self.state)
159 .appearance(false)
160 .with_size(button_size)
161 .h_full()
162 .disabled(self.disabled)
163 .gap_0()
164 .rounded_none()
165 .text_align(TextAlign::Center)
166 .when_some(self.prefix, |this, prefix| this.prefix(prefix))
167 .when_some(self.suffix, |this, suffix| this.suffix(suffix)),
168 )
169 .increment_button(move |this| {
170 this.accessibility_label(t!("Input.Increment"))
171 .flex()
172 .items_center()
173 .justify_center()
174 .text_color(button_foreground)
175 .hover(move |this| this.bg(button_hover))
176 .active(move |this| this.bg(button_active))
177 .h_full()
178 .map(|this| match button_size {
179 Size::XSmall | Size::Small => this.min_w_6(),
180 Size::Medium | Size::Large => this.min_w_8(),
181 Size::Size(size) => this.min_w(size),
182 })
183 .rounded_tr(button_radius)
184 .rounded_br(button_radius)
185 .child(Icon::new(IconName::Plus).with_size(button_size))
186 });
187
188 div()
192 .flex_1()
193 .input_h(self.size)
194 .rounded(cx.theme().radius)
195 .when(self.appearance, |this| {
196 this.bg(bg)
197 .border_1()
198 .border_color(border_color)
199 .when(focused, |this| {
200 this.border_1().border_color(cx.theme().ring)
201 })
202 })
203 .refine_style(&self.style)
204 .when(self.disabled, |this| this.opacity(0.5))
205 .child(content)
206 .when(
207 focused && self.appearance && self.focus_ring_enabled,
208 |this| this.focus_ring_style(window, cx),
209 )
210 }
211}
212
213#[cfg(test)]
214mod tests {
215 use super::StepAction;
216 use gpui_base::step_value;
217
218 #[test]
222 fn test_step_value() {
223 fn some(value: &str) -> Option<String> {
224 Some(value.to_string())
225 }
226
227 assert_eq!(
229 step_value("", StepAction::Increment, 1., None, None),
230 some("1")
231 );
232 assert_eq!(
233 step_value("", StepAction::Decrement, 1., None, None),
234 some("-1")
235 );
236 assert_eq!(
238 step_value("-", StepAction::Increment, 1., None, None),
239 some("1")
240 );
241 assert_eq!(
242 step_value("1", StepAction::Increment, 1., None, None),
243 some("2")
244 );
245 assert_eq!(
246 step_value("-2", StepAction::Increment, 1., None, None),
247 some("-1")
248 );
249
250 assert_eq!(
252 step_value("0.1", StepAction::Increment, 0.2, None, None),
253 some("0.3")
254 );
255 assert_eq!(
256 step_value("0.3", StepAction::Decrement, 0.1, None, None),
257 some("0.2")
258 );
259 assert_eq!(
261 step_value("1.25", StepAction::Increment, 1., None, None),
262 some("2.25")
263 );
264
265 assert_eq!(
267 step_value("", StepAction::Increment, 1., Some(10.), None),
268 some("10")
269 );
270 assert_eq!(
271 step_value("", StepAction::Decrement, 1., Some(10.), None),
272 some("10")
273 );
274 assert_eq!(
276 step_value("99.5", StepAction::Increment, 1., None, Some(100.)),
277 some("100.0")
278 );
279 assert_eq!(
280 step_value("1000", StepAction::Decrement, 1., None, Some(100.)),
281 some("100")
282 );
283 assert_eq!(
285 step_value("1", StepAction::Decrement, 1., Some(0.25), None),
286 some("0.25")
287 );
288
289 assert_eq!(
292 step_value("10", StepAction::Decrement, 1., Some(10.), None),
293 None
294 );
295 assert_eq!(
296 step_value("100", StepAction::Increment, 1., None, Some(100.)),
297 None
298 );
299 assert_eq!(
302 step_value("5", StepAction::Decrement, 1., Some(10.), None),
303 None
304 );
305 assert_eq!(
306 step_value("1000", StepAction::Increment, 1., None, Some(100.)),
307 None
308 );
309 }
310}