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