use std::{ fmt::Display, num::{NonZero, NonZeroU32, NonZeroU64}, rc::Rc, str::FromStr, }; use editor::{Editor, EditorStyle}; use gpui::{ClickEvent, Entity, FocusHandle, Focusable, FontWeight, Modifiers}; use settings::{CenteredPaddingSettings, CodeFade, DelayMs, InactiveOpacity, MinimumContrast}; use ui::prelude::*; #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] pub enum NumberFieldMode { #[default] Read, Edit, } pub trait NumberFieldType: Display + Copy + Clone + Sized + PartialOrd + FromStr + 'static { fn default_format(value: &Self) -> String { format!("{}", value) } fn default_step() -> Self; fn large_step() -> Self; fn small_step() -> Self; fn min_value() -> Self; fn max_value() -> Self; fn saturating_add(self, rhs: Self) -> Self; fn saturating_sub(self, rhs: Self) -> Self; } macro_rules! impl_newtype_numeric_stepper_float { ($type:ident, $default:expr, $large:expr, $small:expr, $min:expr, $max:expr) => { impl NumberFieldType for $type { fn default_step() -> Self { $default.into() } fn large_step() -> Self { $large.into() } fn small_step() -> Self { $small.into() } fn min_value() -> Self { $min.into() } fn max_value() -> Self { $max.into() } fn saturating_add(self, rhs: Self) -> Self { $type((self.0 + rhs.0).min(Self::max_value().0)) } fn saturating_sub(self, rhs: Self) -> Self { $type((self.0 - rhs.0).max(Self::min_value().0)) } } }; } macro_rules! impl_newtype_numeric_stepper_int { ($type:ident, $default:expr, $large:expr, $small:expr, $min:expr, $max:expr) => { impl NumberFieldType for $type { fn default_step() -> Self { $default.into() } fn large_step() -> Self { $large.into() } fn small_step() -> Self { $small.into() } fn min_value() -> Self { $min.into() } fn max_value() -> Self { $max.into() } fn saturating_add(self, rhs: Self) -> Self { $type(self.0.saturating_add(rhs.0).min(Self::max_value().0)) } fn saturating_sub(self, rhs: Self) -> Self { $type(self.0.saturating_sub(rhs.0).max(Self::min_value().0)) } } }; } #[rustfmt::skip] impl_newtype_numeric_stepper_float!(FontWeight, 50., 100., 10., FontWeight::THIN, FontWeight::BLACK); impl_newtype_numeric_stepper_float!(CodeFade, 0.1, 0.2, 0.05, 0.0, 0.9); impl_newtype_numeric_stepper_float!(InactiveOpacity, 0.1, 0.2, 0.05, 0.0, 1.0); impl_newtype_numeric_stepper_float!(MinimumContrast, 1., 10., 0.5, 0.0, 106.0); impl_newtype_numeric_stepper_int!(DelayMs, 100, 500, 10, 0, 2000); impl_newtype_numeric_stepper_float!( CenteredPaddingSettings, 0.05, 0.2, 0.1, CenteredPaddingSettings::MIN_PADDING, CenteredPaddingSettings::MAX_PADDING ); macro_rules! impl_numeric_stepper_int { ($type:ident) => { impl NumberFieldType for $type { fn default_step() -> Self { 1 } fn large_step() -> Self { 10 } fn small_step() -> Self { 1 } fn min_value() -> Self { <$type>::MIN } fn max_value() -> Self { <$type>::MAX } fn saturating_add(self, rhs: Self) -> Self { self.saturating_add(rhs) } fn saturating_sub(self, rhs: Self) -> Self { self.saturating_sub(rhs) } } }; } macro_rules! impl_numeric_stepper_nonzero_int { ($nonzero:ty, $inner:ty) => { impl NumberFieldType for $nonzero { fn default_step() -> Self { <$nonzero>::new(1).unwrap() } fn large_step() -> Self { <$nonzero>::new(10).unwrap() } fn small_step() -> Self { <$nonzero>::new(1).unwrap() } fn min_value() -> Self { <$nonzero>::MIN } fn max_value() -> Self { <$nonzero>::MAX } fn saturating_add(self, rhs: Self) -> Self { let result = self.get().saturating_add(rhs.get()); <$nonzero>::new(result.max(1)).unwrap() } fn saturating_sub(self, rhs: Self) -> Self { let result = self.get().saturating_sub(rhs.get()).max(1); <$nonzero>::new(result).unwrap() } } }; } macro_rules! impl_numeric_stepper_float { ($type:ident) => { impl NumberFieldType for $type { fn default_format(value: &Self) -> String { format!("{:.2}", value) } fn default_step() -> Self { 1.0 } fn large_step() -> Self { 10.0 } fn small_step() -> Self { 0.1 } fn min_value() -> Self { <$type>::MIN } fn max_value() -> Self { <$type>::MAX } fn saturating_add(self, rhs: Self) -> Self { (self + rhs).clamp(Self::min_value(), Self::max_value()) } fn saturating_sub(self, rhs: Self) -> Self { (self - rhs).clamp(Self::min_value(), Self::max_value()) } } }; } impl_numeric_stepper_float!(f32); impl_numeric_stepper_float!(f64); impl_numeric_stepper_int!(isize); impl_numeric_stepper_int!(usize); impl_numeric_stepper_int!(i32); impl_numeric_stepper_int!(u32); impl_numeric_stepper_int!(i64); impl_numeric_stepper_int!(u64); impl_numeric_stepper_nonzero_int!(NonZeroU32, u32); impl_numeric_stepper_nonzero_int!(NonZeroU64, u64); impl_numeric_stepper_nonzero_int!(NonZero, usize); #[derive(RegisterComponent)] pub struct NumberField { id: ElementId, value: T, focus_handle: FocusHandle, mode: Entity, format: Box String>, large_step: T, small_step: T, step: T, min_value: T, max_value: T, on_reset: Option>, on_change: Rc, tab_index: Option, } impl NumberField { pub fn new(id: impl Into, value: T, window: &mut Window, cx: &mut App) -> Self { let id = id.into(); let (mode, focus_handle) = window.with_id(id.clone(), |window| { let mode = window.use_state(cx, |_, _| NumberFieldMode::default()); let focus_handle = window.use_state(cx, |_, cx| cx.focus_handle()); (mode, focus_handle) }); Self { id, mode, value, focus_handle: focus_handle.read(cx).clone(), format: Box::new(T::default_format), large_step: T::large_step(), step: T::default_step(), small_step: T::small_step(), min_value: T::min_value(), max_value: T::max_value(), on_reset: None, on_change: Rc::new(|_, _, _| {}), tab_index: None, } } pub fn format(mut self, format: impl FnOnce(&T) -> String + 'static) -> Self { self.format = Box::new(format); self } pub fn small_step(mut self, step: T) -> Self { self.small_step = step; self } pub fn normal_step(mut self, step: T) -> Self { self.step = step; self } pub fn large_step(mut self, step: T) -> Self { self.large_step = step; self } pub fn min(mut self, min: T) -> Self { self.min_value = min; self } pub fn max(mut self, max: T) -> Self { self.max_value = max; 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 } pub fn on_change(mut self, on_change: impl Fn(&T, &mut Window, &mut App) + 'static) -> Self { self.on_change = Rc::new(on_change); self } } impl IntoElement for NumberField { type Element = gpui::Component; fn into_element(self) -> Self::Element { gpui::Component::new(self) } } impl RenderOnce for NumberField { fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { let mut tab_index = self.tab_index; let get_step = { let large_step = self.large_step; let step = self.step; let small_step = self.small_step; move |modifiers: Modifiers| -> T { if modifiers.shift { large_step } else if modifiers.alt { small_step } else { step } } }; let bg_color = cx.theme().colors().surface_background; let hover_bg_color = cx.theme().colors().element_hover; let border_color = cx.theme().colors().border_variant; let focus_border_color = cx.theme().colors().border_focused; let base_button = |icon: IconName| { h_flex() .cursor_pointer() .p_1p5() .size_full() .justify_center() .overflow_hidden() .border_1() .border_color(border_color) .bg(bg_color) .hover(|s| s.bg(hover_bg_color)) .focus_visible(|s| s.border_color(focus_border_color).bg(hover_bg_color)) .child(Icon::new(icon).size(IconSize::Small)) }; h_flex() .id(self.id.clone()) .track_focus(&self.focus_handle) .gap_1() .when_some(self.on_reset, |this, on_reset| { this.child( IconButton::new("reset", IconName::RotateCcw) .icon_size(IconSize::Small) .when_some(tab_index.as_mut(), |this, tab_index| { *tab_index += 1; this.tab_index(*tab_index - 1) }) .on_click(on_reset), ) }) .child( h_flex() .map(|decrement| { let decrement_handler = { let value = self.value; let on_change = self.on_change.clone(); let min = self.min_value; move |click: &ClickEvent, window: &mut Window, cx: &mut App| { let step = get_step(click.modifiers()); let new_value = value.saturating_sub(step); let new_value = if new_value < min { min } else { new_value }; on_change(&new_value, window, cx); } }; decrement.child( base_button(IconName::Dash) .id("decrement_button") .rounded_tl_sm() .rounded_bl_sm() .tab_index( tab_index .as_mut() .map(|tab_index| { *tab_index += 1; *tab_index - 1 }) .unwrap_or(0), ) .on_click(decrement_handler), ) }) .child( h_flex() .min_w_16() .size_full() .border_y_1() .border_color(border_color) .bg(bg_color) .in_focus(|this| this.border_color(focus_border_color)) .child(match *self.mode.read(cx) { NumberFieldMode::Read => h_flex() .px_1() .flex_1() .justify_center() .child(Label::new((self.format)(&self.value))) .into_any_element(), // Edit mode is disabled until we implement center text alignment for editor // mode.write(cx, NumberFieldMode::Edit); // // When we get to making Edit mode work, we shouldn't even focus the decrement/increment buttons. // Focus should go instead straight to the editor, avoiding any double-step focus. // In this world, the buttons become a mouse-only interaction, given users should be able // to do everything they'd do with the buttons straight in the editor anyway. NumberFieldMode::Edit => h_flex() .flex_1() .child(window.use_state(cx, { |window, cx| { let previous_focus_handle = window.focused(cx); let mut editor = Editor::single_line(window, cx); let mut style = EditorStyle::default(); style.text.text_align = gpui::TextAlign::Right; editor.set_style(style, window, cx); editor.set_text(format!("{}", self.value), window, cx); cx.on_focus_out(&editor.focus_handle(cx), window, { let mode = self.mode.clone(); let min = self.min_value; let max = self.max_value; let on_change = self.on_change.clone(); move |this, _, window, cx| { if let Ok(new_value) = this.text(cx).parse::() { let new_value = if new_value < min { min } else if new_value > max { max } else { new_value }; if let Some(previous) = previous_focus_handle.as_ref() { window.focus(previous); } on_change(&new_value, window, cx); }; mode.write(cx, NumberFieldMode::Read); } }) .detach(); window.focus(&editor.focus_handle(cx)); editor } })) .on_action::({ move |_, window, _| { window.blur(); } }) .into_any_element(), }), ) .map(|increment| { let increment_handler = { let value = self.value; let on_change = self.on_change.clone(); let max = self.max_value; move |click: &ClickEvent, window: &mut Window, cx: &mut App| { let step = get_step(click.modifiers()); let new_value = value.saturating_add(step); let new_value = if new_value > max { max } else { new_value }; on_change(&new_value, window, cx); } }; increment.child( base_button(IconName::Plus) .id("increment_button") .rounded_tr_sm() .rounded_br_sm() .tab_index( tab_index .as_mut() .map(|tab_index| { *tab_index += 1; *tab_index - 1 }) .unwrap_or(0), ) .on_click(increment_handler), ) }), ) } } impl Component for NumberField { fn scope() -> ComponentScope { ComponentScope::Input } fn name() -> &'static str { "Number Field" } fn sort_name() -> &'static str { Self::name() } fn description() -> Option<&'static str> { Some("A numeric input element with increment and decrement buttons.") } fn preview(window: &mut Window, cx: &mut App) -> Option { let stepper_example = window.use_state(cx, |_, _| 100.0); Some( v_flex() .gap_6() .children(vec![single_example( "Default Numeric Stepper", NumberField::new( "numeric-stepper-component-preview", *stepper_example.read(cx), window, cx, ) .on_change({ let stepper_example = stepper_example.clone(); move |value, _, cx| stepper_example.write(cx, *value) }) .min(1.0) .max(100.0) .into_any_element(), )]) .into_any_element(), ) } }