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, 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 h_flex()
348 .id(self.id.clone())
349 .track_focus(&self.focus_handle)
350 .gap_1()
351 .when_some(self.on_reset, |this, on_reset| {
352 this.child(
353 IconButton::new("reset", IconName::RotateCcw)
354 .icon_size(IconSize::Small)
355 .when_some(tab_index.as_mut(), |this, tab_index| {
356 *tab_index += 1;
357 this.tab_index(*tab_index - 1)
358 })
359 .on_click(on_reset),
360 )
361 })
362 .child(
363 h_flex()
364 .map(|decrement| {
365 let decrement_handler = {
366 let value = self.value;
367 let on_change = self.on_change.clone();
368 let min = self.min_value;
369 move |click: &ClickEvent, window: &mut Window, cx: &mut App| {
370 let step = get_step(click.modifiers());
371 let new_value = value.saturating_sub(step);
372 let new_value = if new_value < min { min } else { new_value };
373 on_change(&new_value, window, cx);
374 window.focus_prev();
375 }
376 };
377
378 decrement.child(
379 h_flex()
380 .id("decrement_button")
381 .cursor(gpui::CursorStyle::PointingHand)
382 .p_1p5()
383 .size_full()
384 .justify_center()
385 .overflow_hidden()
386 .rounded_tl_sm()
387 .rounded_bl_sm()
388 .border_1()
389 .border_color(cx.theme().colors().border_variant)
390 .bg(cx.theme().colors().surface_background)
391 .hover(|s| s.bg(cx.theme().colors().element_hover))
392 .child(Icon::new(IconName::Dash).size(IconSize::Small))
393 .when_some(tab_index.as_mut(), |this, tab_index| {
394 *tab_index += 1;
395 this.tab_index(*tab_index - 1).focus(|style| {
396 style
397 .border_color(cx.theme().colors().border_focused)
398 .bg(cx.theme().colors().element_hover)
399 })
400 })
401 .on_click(decrement_handler),
402 )
403 })
404 .child(
405 h_flex()
406 .min_w_16()
407 .size_full()
408 .border_y_1()
409 .border_color(cx.theme().colors().border_variant)
410 .bg(cx.theme().colors().surface_background)
411 .in_focus(|this| this.border_color(cx.theme().colors().border_focused))
412 .child(match *self.mode.read(cx) {
413 NumberFieldMode::Read => h_flex()
414 .id("numeric_stepper_label")
415 .px_1()
416 .flex_1()
417 .justify_center()
418 .child(Label::new((self.format)(&self.value)))
419 .when_some(tab_index.as_mut(), |this, tab_index| {
420 *tab_index += 1;
421 this.tab_index(*tab_index - 1).focus(|style| {
422 style
423 .border_color(cx.theme().colors().border_focused)
424 .bg(cx.theme().colors().element_hover)
425 })
426 })
427 .on_click({
428 let _mode = self.mode.clone();
429 move |click, _, _cx| {
430 if click.click_count() == 2 || click.is_keyboard() {
431 // Edit mode is disabled until we implement center text alignment for editor
432 // mode.write(cx, NumberFieldMode::Edit);
433 }
434 }
435 })
436 .into_any_element(),
437 NumberFieldMode::Edit => h_flex()
438 .flex_1()
439 .child(window.use_state(cx, {
440 |window, cx| {
441 let previous_focus_handle = window.focused(cx);
442 let mut editor = Editor::single_line(window, cx);
443 let mut style = EditorStyle::default();
444 style.text.text_align = gpui::TextAlign::Right;
445 editor.set_style(style, window, cx);
446
447 editor.set_text(format!("{}", self.value), window, cx);
448 cx.on_focus_out(&editor.focus_handle(cx), window, {
449 let mode = self.mode.clone();
450 let min = self.min_value;
451 let max = self.max_value;
452 let on_change = self.on_change.clone();
453 move |this, _, window, cx| {
454 if let Ok(new_value) =
455 this.text(cx).parse::<T>()
456 {
457 let new_value = if new_value < min {
458 min
459 } else if new_value > max {
460 max
461 } else {
462 new_value
463 };
464
465 if let Some(previous) =
466 previous_focus_handle.as_ref()
467 {
468 window.focus(previous);
469 }
470 on_change(&new_value, window, cx);
471 };
472 mode.write(cx, NumberFieldMode::Read);
473 }
474 })
475 .detach();
476
477 window.focus(&editor.focus_handle(cx));
478
479 editor
480 }
481 }))
482 .on_action::<menu::Confirm>({
483 move |_, window, _| {
484 window.blur();
485 }
486 })
487 .into_any_element(),
488 }),
489 )
490 .map(|increment| {
491 let increment_handler = {
492 let value = self.value;
493 let on_change = self.on_change.clone();
494 let max = self.max_value;
495 move |click: &ClickEvent, window: &mut Window, cx: &mut App| {
496 let step = get_step(click.modifiers());
497 let new_value = value.saturating_add(step);
498 let new_value = if new_value > max { max } else { new_value };
499 on_change(&new_value, window, cx);
500 }
501 };
502
503 increment.child(
504 h_flex()
505 .id("increment_button")
506 .cursor(gpui::CursorStyle::PointingHand)
507 .p_1p5()
508 .size_full()
509 .justify_center()
510 .overflow_hidden()
511 .rounded_tr_sm()
512 .rounded_br_sm()
513 .border_1()
514 .border_color(cx.theme().colors().border_variant)
515 .bg(cx.theme().colors().surface_background)
516 .hover(|s| s.bg(cx.theme().colors().element_hover))
517 .child(Icon::new(IconName::Plus).size(IconSize::Small))
518 .when_some(tab_index.as_mut(), |this, tab_index| {
519 *tab_index += 1;
520 this.tab_index(*tab_index - 1).focus(|style| {
521 style
522 .border_color(cx.theme().colors().border_focused)
523 .bg(cx.theme().colors().element_hover)
524 })
525 })
526 .on_click(increment_handler),
527 )
528 }),
529 )
530 }
531}
532
533impl Component for NumberField<usize> {
534 fn scope() -> ComponentScope {
535 ComponentScope::Input
536 }
537
538 fn name() -> &'static str {
539 "Number Field"
540 }
541
542 fn sort_name() -> &'static str {
543 Self::name()
544 }
545
546 fn description() -> Option<&'static str> {
547 Some("A numeric input element with increment and decrement buttons.")
548 }
549
550 fn preview(window: &mut Window, cx: &mut App) -> Option<AnyElement> {
551 let stepper_example = window.use_state(cx, |_, _| 100.0);
552
553 Some(
554 v_flex()
555 .gap_6()
556 .children(vec![single_example(
557 "Default Numeric Stepper",
558 NumberField::new(
559 "numeric-stepper-component-preview",
560 *stepper_example.read(cx),
561 window,
562 cx,
563 )
564 .on_change({
565 let stepper_example = stepper_example.clone();
566 move |value, _, cx| stepper_example.write(cx, *value)
567 })
568 .min(1.0)
569 .max(100.0)
570 .into_any_element(),
571 )])
572 .into_any_element(),
573 )
574 }
575}