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