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