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