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