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)
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}