use gpui::ClickEvent; use crate::{IconButtonShape, prelude::*}; #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] pub enum NumericStepperStyle { Outlined, #[default] Ghost, } #[derive(IntoElement, RegisterComponent)] pub struct NumericStepper { id: ElementId, value: SharedString, style: NumericStepperStyle, on_decrement: Box, on_increment: Box, /// Whether to reserve space for the reset button. reserve_space_for_reset: bool, on_reset: Option>, } impl NumericStepper { pub fn new( id: impl Into, value: impl Into, on_decrement: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, on_increment: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, ) -> Self { Self { id: id.into(), value: value.into(), style: NumericStepperStyle::default(), on_decrement: Box::new(on_decrement), on_increment: Box::new(on_increment), reserve_space_for_reset: false, on_reset: None, } } pub fn style(mut self, style: NumericStepperStyle) -> Self { self.style = style; self } pub fn reserve_space_for_reset(mut self, reserve_space_for_reset: bool) -> Self { self.reserve_space_for_reset = reserve_space_for_reset; self } pub fn on_reset( mut self, on_reset: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, ) -> Self { self.on_reset = Some(Box::new(on_reset)); self } } impl RenderOnce for NumericStepper { fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { let shape = IconButtonShape::Square; let icon_size = IconSize::Small; let is_outlined = matches!(self.style, NumericStepperStyle::Outlined); h_flex() .id(self.id) .gap_1() .map(|element| { if let Some(on_reset) = self.on_reset { element.child( IconButton::new("reset", IconName::RotateCcw) .shape(shape) .icon_size(icon_size) .on_click(on_reset), ) } else if self.reserve_space_for_reset { element.child( h_flex() .size(icon_size.square(window, cx)) .flex_none() .into_any_element(), ) } else { element } }) .child( h_flex() .gap_1() .rounded_sm() .map(|this| { if is_outlined { this.overflow_hidden() .bg(cx.theme().colors().surface_background) .border_1() .border_color(cx.theme().colors().border) } else { this.px_1().bg(cx.theme().colors().editor_background) } }) .map(|decrement| { if is_outlined { decrement.child( h_flex() .id("decrement_button") .p_1p5() .size_full() .justify_center() .hover(|s| s.bg(cx.theme().colors().element_hover)) .border_r_1() .border_color(cx.theme().colors().border) .child(Icon::new(IconName::Dash).size(IconSize::Small)) .on_click(self.on_decrement), ) } else { decrement.child( IconButton::new("decrement", IconName::Dash) .shape(shape) .icon_size(icon_size) .on_click(self.on_decrement), ) } }) .when(is_outlined, |this| this) .child(Label::new(self.value).mx_3()) .map(|increment| { if is_outlined { increment.child( h_flex() .id("increment_button") .p_1p5() .size_full() .justify_center() .hover(|s| s.bg(cx.theme().colors().element_hover)) .border_l_1() .border_color(cx.theme().colors().border) .child(Icon::new(IconName::Plus).size(IconSize::Small)) .on_click(self.on_increment), ) } else { increment.child( IconButton::new("increment", IconName::Dash) .shape(shape) .icon_size(icon_size) .on_click(self.on_increment), ) } }), ) } } impl Component for NumericStepper { fn scope() -> ComponentScope { ComponentScope::Input } fn name() -> &'static str { "Numeric Stepper" } fn sort_name() -> &'static str { Self::name() } fn description() -> Option<&'static str> { Some("A button used to increment or decrement a numeric value.") } fn preview(_window: &mut Window, _cx: &mut App) -> Option { Some( v_flex() .gap_6() .children(vec![example_group_with_title( "Styles", vec![ single_example( "Default", NumericStepper::new( "numeric-stepper-component-preview", "10", move |_, _, _| {}, move |_, _, _| {}, ) .into_any_element(), ), single_example( "Outlined", NumericStepper::new( "numeric-stepper-with-border-component-preview", "10", move |_, _, _| {}, move |_, _, _| {}, ) .style(NumericStepperStyle::Outlined) .into_any_element(), ), ], )]) .into_any_element(), ) } }