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