Files
zed/crates/ui/src/components/numeric_stepper.rs
T
Ben KunkleandMrSubidubi 33c64935ea onboarding: Wire up tab index (#35659)
Closes #ISSUE

Allows tabbing through everything in all three pages. Until #35075 is
merged it is not possible to actually "click" tab focused buttons with
the keyboard.

Additionally adds an action `onboarding::Finish` and displays the
keybind. The action corresponds to both the "Skip all" and "Start
Building" buttons, with the keybind displayed similar to how it is for
the page nav buttons

Release Notes:

- N/A *or* Added/Fixed/Improved ...

---------

Co-authored-by: MrSubidubi <finn@zed.dev>
2025-08-05 19:48:15 +00:00

238 lines
9.1 KiB
Rust

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<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
on_increment: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
/// Whether to reserve space for the reset button.
reserve_space_for_reset: bool,
on_reset: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
tab_index: Option<isize>,
}
impl NumericStepper {
pub fn new(
id: impl Into<ElementId>,
value: impl Into<SharedString>,
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,
tab_index: 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
}
pub fn tab_index(mut self, tab_index: isize) -> Self {
self.tab_index = Some(tab_index);
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);
let mut tab_index = self.tab_index;
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)
.when_some(tab_index.as_mut(), |this, tab_index| {
*tab_index += 1;
this.tab_index(*tab_index - 1)
})
.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_variant)
} 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_variant)
.child(Icon::new(IconName::Dash).size(IconSize::Small))
.when_some(tab_index.as_mut(), |this, tab_index| {
*tab_index += 1;
this.tab_index(*tab_index - 1).focus(|style| {
style.bg(cx.theme().colors().element_hover)
})
})
.on_click(self.on_decrement),
)
} else {
decrement.child(
IconButton::new("decrement", IconName::Dash)
.shape(shape)
.icon_size(icon_size)
.when_some(tab_index.as_mut(), |this, tab_index| {
*tab_index += 1;
this.tab_index(*tab_index - 1)
})
.on_click(self.on_decrement),
)
}
})
.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_variant)
.child(Icon::new(IconName::Plus).size(IconSize::Small))
.when_some(tab_index.as_mut(), |this, tab_index| {
*tab_index += 1;
this.tab_index(*tab_index - 1).focus(|style| {
style.bg(cx.theme().colors().element_hover)
})
})
.on_click(self.on_increment),
)
} else {
increment.child(
IconButton::new("increment", IconName::Dash)
.shape(shape)
.icon_size(icon_size)
.when_some(tab_index.as_mut(), |this, tab_index| {
*tab_index += 1;
this.tab_index(*tab_index - 1)
})
.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<AnyElement> {
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(),
)
}
}