1use std::{
2 fmt::Display,
3 num::{NonZero, NonZeroU32, NonZeroU64},
4 rc::Rc,
5 str::FromStr,
6};
7
8use editor::{Editor, EditorStyle};
9use gpui::{ClickEvent, Entity, FocusHandle, Focusable, FontWeight, Modifiers};
10
11use settings::{CodeFade, DelayMs, InactiveOpacity, MinimumContrast};
12use ui::prelude::*;
13
14#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
15pub enum NumberFieldMode {
16 #[default]
17 Read,
18 Edit,
19}
20
21pub trait NumberFieldType: Display + Copy + Clone + Sized + PartialOrd + FromStr + 'static {
22 fn default_format(value: &Self) -> String {
23 format!("{}", value)
24 }
25 fn default_step() -> Self;
26 fn large_step() -> Self;
27 fn small_step() -> Self;
28 fn min_value() -> Self;
29 fn max_value() -> Self;
30 fn saturating_add(self, rhs: Self) -> Self;
31 fn saturating_sub(self, rhs: Self) -> Self;
32}
33
34macro_rules! impl_newtype_numeric_stepper {
35 ($type:ident, $default:expr, $large:expr, $small:expr, $min:expr, $max:expr) => {
36 impl NumberFieldType for $type {
37 fn default_step() -> Self {
38 $default.into()
39 }
40
41 fn large_step() -> Self {
42 $large.into()
43 }
44
45 fn small_step() -> Self {
46 $small.into()
47 }
48
49 fn min_value() -> Self {
50 $min.into()
51 }
52
53 fn max_value() -> Self {
54 $max.into()
55 }
56
57 fn saturating_add(self, rhs: Self) -> Self {
58 $type((self.0 + rhs.0).min(Self::max_value().0))
59 }
60
61 fn saturating_sub(self, rhs: Self) -> Self {
62 $type((self.0 - rhs.0).max(Self::min_value().0))
63 }
64 }
65 };
66}
67
68#[rustfmt::skip]
69impl_newtype_numeric_stepper!(FontWeight, 50., 100., 10., FontWeight::THIN, FontWeight::BLACK);
70impl_newtype_numeric_stepper!(CodeFade, 0.1, 0.2, 0.05, 0.0, 0.9);
71impl_newtype_numeric_stepper!(InactiveOpacity, 0.1, 0.2, 0.05, 0.0, 1.0);
72impl_newtype_numeric_stepper!(MinimumContrast, 1., 10., 0.5, 0.0, 106.0);
73impl_newtype_numeric_stepper!(DelayMs, 100, 500, 10, 0, 2000);
74
75macro_rules! impl_numeric_stepper_int {
76 ($type:ident) => {
77 impl NumberFieldType for $type {
78 fn default_step() -> Self {
79 1
80 }
81
82 fn large_step() -> Self {
83 10
84 }
85
86 fn small_step() -> Self {
87 1
88 }
89
90 fn min_value() -> Self {
91 <$type>::MIN
92 }
93
94 fn max_value() -> Self {
95 <$type>::MAX
96 }
97
98 fn saturating_add(self, rhs: Self) -> Self {
99 self.saturating_add(rhs)
100 }
101
102 fn saturating_sub(self, rhs: Self) -> Self {
103 self.saturating_sub(rhs)
104 }
105 }
106 };
107}
108
109macro_rules! impl_numeric_stepper_nonzero_int {
110 ($nonzero:ty, $inner:ty) => {
111 impl NumberFieldType for $nonzero {
112 fn default_step() -> Self {
113 <$nonzero>::new(1).unwrap()
114 }
115
116 fn large_step() -> Self {
117 <$nonzero>::new(10).unwrap()
118 }
119
120 fn small_step() -> Self {
121 <$nonzero>::new(1).unwrap()
122 }
123
124 fn min_value() -> Self {
125 <$nonzero>::MIN
126 }
127
128 fn max_value() -> Self {
129 <$nonzero>::MAX
130 }
131
132 fn saturating_add(self, rhs: Self) -> Self {
133 let result = self.get().saturating_add(rhs.get());
134 <$nonzero>::new(result.max(1)).unwrap()
135 }
136
137 fn saturating_sub(self, rhs: Self) -> Self {
138 let result = self.get().saturating_sub(rhs.get()).max(1);
139 <$nonzero>::new(result).unwrap()
140 }
141 }
142 };
143}
144
145macro_rules! impl_numeric_stepper_float {
146 ($type:ident) => {
147 impl NumberFieldType for $type {
148 fn default_format(value: &Self) -> String {
149 format!("{:.2}", value)
150 }
151
152 fn default_step() -> Self {
153 1.0
154 }
155
156 fn large_step() -> Self {
157 10.0
158 }
159
160 fn small_step() -> Self {
161 0.1
162 }
163
164 fn min_value() -> Self {
165 <$type>::MIN
166 }
167
168 fn max_value() -> Self {
169 <$type>::MAX
170 }
171
172 fn saturating_add(self, rhs: Self) -> Self {
173 (self + rhs).clamp(Self::min_value(), Self::max_value())
174 }
175
176 fn saturating_sub(self, rhs: Self) -> Self {
177 (self - rhs).clamp(Self::min_value(), Self::max_value())
178 }
179 }
180 };
181}
182
183impl_numeric_stepper_float!(f32);
184impl_numeric_stepper_float!(f64);
185impl_numeric_stepper_int!(isize);
186impl_numeric_stepper_int!(usize);
187impl_numeric_stepper_int!(i32);
188impl_numeric_stepper_int!(u32);
189impl_numeric_stepper_int!(i64);
190impl_numeric_stepper_int!(u64);
191
192impl_numeric_stepper_nonzero_int!(NonZeroU32, u32);
193impl_numeric_stepper_nonzero_int!(NonZeroU64, u64);
194impl_numeric_stepper_nonzero_int!(NonZero<usize>, usize);
195
196#[derive(RegisterComponent)]
197pub struct NumberField<T = usize> {
198 id: ElementId,
199 value: T,
200 focus_handle: FocusHandle,
201 mode: Entity<NumberFieldMode>,
202 format: Box<dyn FnOnce(&T) -> String>,
203 large_step: T,
204 small_step: T,
205 step: T,
206 min_value: T,
207 max_value: T,
208 on_reset: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
209 on_change: Rc<dyn Fn(&T, &mut Window, &mut App) + 'static>,
210 tab_index: Option<isize>,
211}
212
213impl<T: NumberFieldType> NumberField<T> {
214 pub fn new(id: impl Into<ElementId>, value: T, window: &mut Window, cx: &mut App) -> Self {
215 let id = id.into();
216
217 let (mode, focus_handle) = window.with_id(id.clone(), |window| {
218 let mode = window.use_state(cx, |_, _| NumberFieldMode::default());
219 let focus_handle = window.use_state(cx, |_, cx| cx.focus_handle());
220 (mode, focus_handle)
221 });
222
223 Self {
224 id,
225 mode,
226 value,
227 focus_handle: focus_handle.read(cx).clone(),
228 format: Box::new(T::default_format),
229 large_step: T::large_step(),
230 step: T::default_step(),
231 small_step: T::small_step(),
232 min_value: T::min_value(),
233 max_value: T::max_value(),
234 on_reset: None,
235 on_change: Rc::new(|_, _, _| {}),
236 tab_index: None,
237 }
238 }
239
240 pub fn format(mut self, format: impl FnOnce(&T) -> String + 'static) -> Self {
241 self.format = Box::new(format);
242 self
243 }
244
245 pub fn small_step(mut self, step: T) -> Self {
246 self.small_step = step;
247 self
248 }
249
250 pub fn normal_step(mut self, step: T) -> Self {
251 self.step = step;
252 self
253 }
254
255 pub fn large_step(mut self, step: T) -> Self {
256 self.large_step = step;
257 self
258 }
259
260 pub fn min(mut self, min: T) -> Self {
261 self.min_value = min;
262 self
263 }
264
265 pub fn max(mut self, max: T) -> Self {
266 self.max_value = max;
267 self
268 }
269
270 pub fn on_reset(
271 mut self,
272 on_reset: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
273 ) -> Self {
274 self.on_reset = Some(Box::new(on_reset));
275 self
276 }
277
278 pub fn tab_index(mut self, tab_index: isize) -> Self {
279 self.tab_index = Some(tab_index);
280 self
281 }
282
283 pub fn on_change(mut self, on_change: impl Fn(&T, &mut Window, &mut App) + 'static) -> Self {
284 self.on_change = Rc::new(on_change);
285 self
286 }
287}
288
289impl<T: NumberFieldType> IntoElement for NumberField<T> {
290 type Element = gpui::Component<Self>;
291
292 fn into_element(self) -> Self::Element {
293 gpui::Component::new(self)
294 }
295}
296
297impl<T: NumberFieldType> RenderOnce for NumberField<T> {
298 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
299 let mut tab_index = self.tab_index;
300
301 let get_step = {
302 let large_step = self.large_step;
303 let step = self.step;
304 let small_step = self.small_step;
305 move |modifiers: Modifiers| -> T {
306 if modifiers.shift {
307 large_step
308 } else if modifiers.alt {
309 small_step
310 } else {
311 step
312 }
313 }
314 };
315
316 let bg_color = cx.theme().colors().surface_background;
317 let hover_bg_color = cx.theme().colors().element_hover;
318
319 let border_color = cx.theme().colors().border_variant;
320 let focus_border_color = cx.theme().colors().border_focused;
321
322 let base_button = |icon: IconName| {
323 h_flex()
324 .cursor_pointer()
325 .p_1p5()
326 .size_full()
327 .justify_center()
328 .overflow_hidden()
329 .border_1()
330 .border_color(border_color)
331 .bg(bg_color)
332 .hover(|s| s.bg(hover_bg_color))
333 .focus(|s| s.border_color(focus_border_color).bg(hover_bg_color))
334 .child(Icon::new(icon).size(IconSize::Small))
335 };
336
337 h_flex()
338 .id(self.id.clone())
339 .track_focus(&self.focus_handle)
340 .gap_1()
341 .when_some(self.on_reset, |this, on_reset| {
342 this.child(
343 IconButton::new("reset", IconName::RotateCcw)
344 .icon_size(IconSize::Small)
345 .when_some(tab_index.as_mut(), |this, tab_index| {
346 *tab_index += 1;
347 this.tab_index(*tab_index - 1)
348 })
349 .on_click(on_reset),
350 )
351 })
352 .child(
353 h_flex()
354 .map(|decrement| {
355 let decrement_handler = {
356 let value = self.value;
357 let on_change = self.on_change.clone();
358 let min = self.min_value;
359 move |click: &ClickEvent, window: &mut Window, cx: &mut App| {
360 let step = get_step(click.modifiers());
361 let new_value = value.saturating_sub(step);
362 let new_value = if new_value < min { min } else { new_value };
363 on_change(&new_value, window, cx);
364 window.focus_prev();
365 }
366 };
367
368 decrement.child(
369 base_button(IconName::Dash)
370 .id("decrement_button")
371 .rounded_tl_sm()
372 .rounded_bl_sm()
373 .tab_index(
374 tab_index
375 .as_mut()
376 .map(|tab_index| {
377 *tab_index += 1;
378 *tab_index - 1
379 })
380 .unwrap_or(0),
381 )
382 .on_click(decrement_handler),
383 )
384 })
385 .child(
386 h_flex()
387 .min_w_16()
388 .size_full()
389 .border_y_1()
390 .border_color(border_color)
391 .bg(bg_color)
392 .in_focus(|this| this.border_color(focus_border_color))
393 .child(match *self.mode.read(cx) {
394 NumberFieldMode::Read => h_flex()
395 .px_1()
396 .flex_1()
397 .justify_center()
398 .child(Label::new((self.format)(&self.value)))
399 .into_any_element(),
400 // Edit mode is disabled until we implement center text alignment for editor
401 // mode.write(cx, NumberFieldMode::Edit);
402 //
403 // When we get to making Edit mode work, we shouldn't even focus the decrement/increment buttons.
404 // Focus should go instead straight to the editor, avoiding any double-step focus.
405 // In this world, the buttons become a mouse-only interaction, given users should be able
406 // to do everything they'd do with the buttons straight in the editor anyway.
407 NumberFieldMode::Edit => h_flex()
408 .flex_1()
409 .child(window.use_state(cx, {
410 |window, cx| {
411 let previous_focus_handle = window.focused(cx);
412 let mut editor = Editor::single_line(window, cx);
413 let mut style = EditorStyle::default();
414 style.text.text_align = gpui::TextAlign::Right;
415 editor.set_style(style, window, cx);
416
417 editor.set_text(format!("{}", self.value), window, cx);
418 cx.on_focus_out(&editor.focus_handle(cx), window, {
419 let mode = self.mode.clone();
420 let min = self.min_value;
421 let max = self.max_value;
422 let on_change = self.on_change.clone();
423 move |this, _, window, cx| {
424 if let Ok(new_value) =
425 this.text(cx).parse::<T>()
426 {
427 let new_value = if new_value < min {
428 min
429 } else if new_value > max {
430 max
431 } else {
432 new_value
433 };
434
435 if let Some(previous) =
436 previous_focus_handle.as_ref()
437 {
438 window.focus(previous);
439 }
440 on_change(&new_value, window, cx);
441 };
442 mode.write(cx, NumberFieldMode::Read);
443 }
444 })
445 .detach();
446
447 window.focus(&editor.focus_handle(cx));
448
449 editor
450 }
451 }))
452 .on_action::<menu::Confirm>({
453 move |_, window, _| {
454 window.blur();
455 }
456 })
457 .into_any_element(),
458 }),
459 )
460 .map(|increment| {
461 let increment_handler = {
462 let value = self.value;
463 let on_change = self.on_change.clone();
464 let max = self.max_value;
465 move |click: &ClickEvent, window: &mut Window, cx: &mut App| {
466 let step = get_step(click.modifiers());
467 let new_value = value.saturating_add(step);
468 let new_value = if new_value > max { max } else { new_value };
469 on_change(&new_value, window, cx);
470 }
471 };
472
473 increment.child(
474 base_button(IconName::Plus)
475 .id("increment_button")
476 .rounded_tr_sm()
477 .rounded_br_sm()
478 .tab_index(
479 tab_index
480 .as_mut()
481 .map(|tab_index| {
482 *tab_index += 1;
483 *tab_index - 1
484 })
485 .unwrap_or(0),
486 )
487 .on_click(increment_handler),
488 )
489 }),
490 )
491 }
492}
493
494impl Component for NumberField<usize> {
495 fn scope() -> ComponentScope {
496 ComponentScope::Input
497 }
498
499 fn name() -> &'static str {
500 "Number Field"
501 }
502
503 fn sort_name() -> &'static str {
504 Self::name()
505 }
506
507 fn description() -> Option<&'static str> {
508 Some("A numeric input element with increment and decrement buttons.")
509 }
510
511 fn preview(window: &mut Window, cx: &mut App) -> Option<AnyElement> {
512 let stepper_example = window.use_state(cx, |_, _| 100.0);
513
514 Some(
515 v_flex()
516 .gap_6()
517 .children(vec![single_example(
518 "Default Numeric Stepper",
519 NumberField::new(
520 "numeric-stepper-component-preview",
521 *stepper_example.read(cx),
522 window,
523 cx,
524 )
525 .on_change({
526 let stepper_example = stepper_example.clone();
527 move |value, _, cx| stepper_example.write(cx, *value)
528 })
529 .min(1.0)
530 .max(100.0)
531 .into_any_element(),
532 )])
533 .into_any_element(),
534 )
535 }
536}