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}
 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_variant)
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_variant)
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                    .child(Label::new(self.value).mx_3())
128                    .map(|increment| {
129                        if is_outlined {
130                            increment.child(
131                                h_flex()
132                                    .id("increment_button")
133                                    .p_1p5()
134                                    .size_full()
135                                    .justify_center()
136                                    .hover(|s| s.bg(cx.theme().colors().element_hover))
137                                    .border_l_1()
138                                    .border_color(cx.theme().colors().border_variant)
139                                    .child(Icon::new(IconName::Plus).size(IconSize::Small))
140                                    .on_click(self.on_increment),
141                            )
142                        } else {
143                            increment.child(
144                                IconButton::new("increment", IconName::Dash)
145                                    .shape(shape)
146                                    .icon_size(icon_size)
147                                    .on_click(self.on_increment),
148                            )
149                        }
150                    }),
151            )
152    }
153}
154
155impl Component for NumericStepper {
156    fn scope() -> ComponentScope {
157        ComponentScope::Input
158    }
159
160    fn name() -> &'static str {
161        "Numeric Stepper"
162    }
163
164    fn sort_name() -> &'static str {
165        Self::name()
166    }
167
168    fn description() -> Option<&'static str> {
169        Some("A button used to increment or decrement a numeric value.")
170    }
171
172    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
173        Some(
174            v_flex()
175                .gap_6()
176                .children(vec![example_group_with_title(
177                    "Styles",
178                    vec![
179                        single_example(
180                            "Default",
181                            NumericStepper::new(
182                                "numeric-stepper-component-preview",
183                                "10",
184                                move |_, _, _| {},
185                                move |_, _, _| {},
186                            )
187                            .into_any_element(),
188                        ),
189                        single_example(
190                            "Outlined",
191                            NumericStepper::new(
192                                "numeric-stepper-with-border-component-preview",
193                                "10",
194                                move |_, _, _| {},
195                                move |_, _, _| {},
196                            )
197                            .style(NumericStepperStyle::Outlined)
198                            .into_any_element(),
199                        ),
200                    ],
201                )])
202                .into_any_element(),
203        )
204    }
205}