1use gpui::ClickEvent;
2
3use crate::{IconButtonShape, prelude::*};
4
5#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
6pub enum NumericStepperStyle {
7 Outlined,
8 #[default]
9 Ghost,
10}
11
12#[derive(IntoElement, RegisterComponent)]
13pub struct NumericStepper {
14 id: ElementId,
15 value: SharedString,
16 style: NumericStepperStyle,
17 on_decrement: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
18 on_increment: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
19 /// Whether to reserve space for the reset button.
20 reserve_space_for_reset: bool,
21 on_reset: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
22}
23
24impl NumericStepper {
25 pub fn new(
26 id: impl Into<ElementId>,
27 value: impl Into<SharedString>,
28 on_decrement: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
29 on_increment: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
30 ) -> Self {
31 Self {
32 id: id.into(),
33 value: value.into(),
34 style: NumericStepperStyle::default(),
35 on_decrement: Box::new(on_decrement),
36 on_increment: Box::new(on_increment),
37 reserve_space_for_reset: false,
38 on_reset: None,
39 }
40 }
41
42 pub fn style(mut self, style: NumericStepperStyle) -> Self {
43 self.style = style;
44 self
45 }
46
47 pub fn reserve_space_for_reset(mut self, reserve_space_for_reset: bool) -> Self {
48 self.reserve_space_for_reset = reserve_space_for_reset;
49 self
50 }
51
52 pub fn on_reset(
53 mut self,
54 on_reset: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
55 ) -> Self {
56 self.on_reset = Some(Box::new(on_reset));
57 self
58 }
59}
60
61impl RenderOnce for NumericStepper {
62 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
63 let shape = IconButtonShape::Square;
64 let icon_size = IconSize::Small;
65
66 let is_outlined = matches!(self.style, NumericStepperStyle::Outlined);
67
68 h_flex()
69 .id(self.id)
70 .gap_1()
71 .map(|element| {
72 if let Some(on_reset) = self.on_reset {
73 element.child(
74 IconButton::new("reset", IconName::RotateCcw)
75 .shape(shape)
76 .icon_size(icon_size)
77 .on_click(on_reset),
78 )
79 } else if self.reserve_space_for_reset {
80 element.child(
81 h_flex()
82 .size(icon_size.square(window, cx))
83 .flex_none()
84 .into_any_element(),
85 )
86 } else {
87 element
88 }
89 })
90 .child(
91 h_flex()
92 .gap_1()
93 .rounded_sm()
94 .map(|this| {
95 if is_outlined {
96 this.overflow_hidden()
97 .bg(cx.theme().colors().surface_background)
98 .border_1()
99 .border_color(cx.theme().colors().border)
100 } else {
101 this.px_1().bg(cx.theme().colors().editor_background)
102 }
103 })
104 .map(|decrement| {
105 if is_outlined {
106 decrement.child(
107 h_flex()
108 .id("decrement_button")
109 .p_1p5()
110 .size_full()
111 .justify_center()
112 .hover(|s| s.bg(cx.theme().colors().element_hover))
113 .border_r_1()
114 .border_color(cx.theme().colors().border)
115 .child(Icon::new(IconName::Dash).size(IconSize::Small))
116 .on_click(self.on_decrement),
117 )
118 } else {
119 decrement.child(
120 IconButton::new("decrement", IconName::Dash)
121 .shape(shape)
122 .icon_size(icon_size)
123 .on_click(self.on_decrement),
124 )
125 }
126 })
127 .when(is_outlined, |this| this)
128 .child(Label::new(self.value).mx_3())
129 .map(|increment| {
130 if is_outlined {
131 increment.child(
132 h_flex()
133 .id("increment_button")
134 .p_1p5()
135 .size_full()
136 .justify_center()
137 .hover(|s| s.bg(cx.theme().colors().element_hover))
138 .border_l_1()
139 .border_color(cx.theme().colors().border)
140 .child(Icon::new(IconName::Plus).size(IconSize::Small))
141 .on_click(self.on_increment),
142 )
143 } else {
144 increment.child(
145 IconButton::new("increment", IconName::Dash)
146 .shape(shape)
147 .icon_size(icon_size)
148 .on_click(self.on_increment),
149 )
150 }
151 }),
152 )
153 }
154}
155
156impl Component for NumericStepper {
157 fn scope() -> ComponentScope {
158 ComponentScope::Input
159 }
160
161 fn name() -> &'static str {
162 "Numeric Stepper"
163 }
164
165 fn sort_name() -> &'static str {
166 Self::name()
167 }
168
169 fn description() -> Option<&'static str> {
170 Some("A button used to increment or decrement a numeric value.")
171 }
172
173 fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
174 Some(
175 v_flex()
176 .gap_6()
177 .children(vec![example_group_with_title(
178 "Styles",
179 vec![
180 single_example(
181 "Default",
182 NumericStepper::new(
183 "numeric-stepper-component-preview",
184 "10",
185 move |_, _, _| {},
186 move |_, _, _| {},
187 )
188 .into_any_element(),
189 ),
190 single_example(
191 "Outlined",
192 NumericStepper::new(
193 "numeric-stepper-with-border-component-preview",
194 "10",
195 move |_, _, _| {},
196 move |_, _, _| {},
197 )
198 .style(NumericStepperStyle::Outlined)
199 .into_any_element(),
200 ),
201 ],
202 )])
203 .into_any_element(),
204 )
205 }
206}