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