numeric_stepper.rs

  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}