Closes #41774 Release Notes: - settings_ui: Fixed an integer underflow panic when attempting to hit the `-` sign on settings item that take delays in milliseconds
578 lines
20 KiB
Rust
578 lines
20 KiB
Rust
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>, usize);
|
|
|
|
#[derive(RegisterComponent)]
|
|
pub struct NumberField<T = usize> {
|
|
id: ElementId,
|
|
value: T,
|
|
focus_handle: FocusHandle,
|
|
mode: Entity<NumberFieldMode>,
|
|
format: Box<dyn FnOnce(&T) -> String>,
|
|
large_step: T,
|
|
small_step: T,
|
|
step: T,
|
|
min_value: T,
|
|
max_value: T,
|
|
on_reset: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
|
|
on_change: Rc<dyn Fn(&T, &mut Window, &mut App) + 'static>,
|
|
tab_index: Option<isize>,
|
|
}
|
|
|
|
impl<T: NumberFieldType> NumberField<T> {
|
|
pub fn new(id: impl Into<ElementId>, 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<T: NumberFieldType> IntoElement for NumberField<T> {
|
|
type Element = gpui::Component<Self>;
|
|
|
|
fn into_element(self) -> Self::Element {
|
|
gpui::Component::new(self)
|
|
}
|
|
}
|
|
|
|
impl<T: NumberFieldType> RenderOnce for NumberField<T> {
|
|
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::<T>()
|
|
{
|
|
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::<menu::Confirm>({
|
|
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<usize> {
|
|
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<AnyElement> {
|
|
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(),
|
|
)
|
|
}
|
|
}
|