Fine tune UX for new numeric stepper

This commit is contained in:
Mikayla Maki 2025-08-19 21:04:57 -07:00
parent b775a2e3cb
commit e47b9d3b38
4 changed files with 193 additions and 140 deletions

View file

@ -69,6 +69,20 @@ impl<'a, T: 'static> Context<'a, T> {
})
}
/// Observe changes to ourselves
pub fn observe_self(
&mut self,
mut on_event: impl FnMut(&mut T, &mut Context<T>) + 'static,
) -> Subscription
where
T: 'static,
{
let this = self.entity();
self.app.observe(&this, move |this, cx| {
this.update(cx, |this, cx| on_event(this, cx))
})
}
/// Subscribe to an event type from another entity
pub fn subscribe<T2, Evt>(
&mut self,

View file

@ -4838,6 +4838,12 @@ impl<T: Into<SharedString>> From<(ElementId, T)> for ElementId {
}
}
impl From<core::panic::Location<'static>> for ElementId {
fn from(value: core::panic::Location<'static>) -> Self {
Self::CodeLocation(value)
}
}
/// A rectangle to be rendered in the window at the given position and size.
/// Passed as an argument [`Window::paint_quad`].
#[derive(Clone)]

View file

@ -15,6 +15,7 @@ use ui::{
ButtonLike, ListItem, ListItemSpacing, PopoverMenu, SwitchField, ToggleButtonGroup,
ToggleButtonGroupStyle, ToggleButtonSimple, ToggleState, Tooltip, prelude::*,
};
use ui_input::NumericStepper;
use crate::{ImportCursorSettings, ImportVsCodeSettings, SettingsImportState};
@ -109,7 +110,7 @@ fn write_ui_font_family(font: SharedString, cx: &mut App) {
});
}
fn _write_ui_font_size(size: Pixels, cx: &mut App) {
fn write_ui_font_size(size: Pixels, cx: &mut App) {
let fs = <dyn Fs>::global(cx);
update_settings_file::<ThemeSettings>(fs, cx, move |theme_settings, _| {
@ -117,7 +118,7 @@ fn _write_ui_font_size(size: Pixels, cx: &mut App) {
});
}
fn _write_buffer_font_size(size: Pixels, cx: &mut App) {
fn write_buffer_font_size(size: Pixels, cx: &mut App) {
let fs = <dyn Fs>::global(cx);
update_settings_file::<ThemeSettings>(fs, cx, move |theme_settings, _| {
@ -277,10 +278,10 @@ fn render_font_customization_section(
cx: &mut App,
) -> impl IntoElement {
let theme_settings = ThemeSettings::get_global(cx);
// let ui_font_size = theme_settings.ui_font_size(cx);
let ui_font_size = theme_settings.ui_font_size(cx);
let ui_font_family = theme_settings.ui_font.family.clone();
let buffer_font_family = theme_settings.buffer_font.family.clone();
// let buffer_font_size = theme_settings.buffer_font_size(cx);
let buffer_font_size = theme_settings.buffer_font_size(cx);
let ui_font_picker =
cx.new(|cx| font_picker(ui_font_family.clone(), write_ui_font_family, window, cx));
@ -306,62 +307,53 @@ fn render_font_customization_section(
.gap_1()
.child(Label::new("UI Font"))
.child(
h_flex().w_full().justify_between().gap_2().child(
PopoverMenu::new("ui-font-picker")
.menu({
let ui_font_picker = ui_font_picker.clone();
move |_window, _cx| Some(ui_font_picker.clone())
})
.trigger(
ButtonLike::new("ui-font-family-button")
.style(ButtonStyle::Outlined)
.size(ButtonSize::Medium)
.full_width()
.tab_index({
*tab_index += 1;
*tab_index - 1
})
.child(
h_flex()
.w_full()
.justify_between()
.child(Label::new(ui_font_family))
.child(
Icon::new(IconName::ChevronUpDown)
.color(Color::Muted)
.size(IconSize::XSmall),
),
),
)
.full_width(true)
.anchor(gpui::Corner::TopLeft)
.offset(gpui::Point {
x: px(0.0),
y: px(4.0),
})
.with_handle(ui_font_handle),
), // .child(
// NumericStepper::new(
// "ui-font-size",
// ui_font_size.to_string(),
// move |size, cx| {
// write_ui_font_size(Pixels::from(size), cx);
// },
// move |_, _, cx| {
// write_ui_font_size(ui_font_size - px(1.), cx);
// },
// move |_, _, cx| {
// write_ui_font_size(ui_font_size + px(1.), cx);
// },
// window,
// cx,
// )
// .style(ui_input::NumericStepperStyle::Outlined)
// .tab_index({
// *tab_index += 2;
// *tab_index - 2
// }),
// ),
h_flex()
.w_full()
.justify_between()
.gap_2()
.child(
PopoverMenu::new("ui-font-picker")
.menu({
let ui_font_picker = ui_font_picker.clone();
move |_window, _cx| Some(ui_font_picker.clone())
})
.trigger(
ButtonLike::new("ui-font-family-button")
.style(ButtonStyle::Outlined)
.size(ButtonSize::Medium)
.full_width()
.tab_index({
*tab_index += 1;
*tab_index - 1
})
.child(
h_flex()
.w_full()
.justify_between()
.child(Label::new(ui_font_family))
.child(
Icon::new(IconName::ChevronUpDown)
.color(Color::Muted)
.size(IconSize::XSmall),
),
),
)
.full_width(true)
.anchor(gpui::Corner::TopLeft)
.offset(gpui::Point {
x: px(0.0),
y: px(4.0),
})
.with_handle(ui_font_handle),
)
.child(font_picker_stepper(
"ui-font-size",
&ui_font_size,
tab_index,
write_ui_font_size,
window,
cx,
)),
),
)
.child(
@ -370,66 +362,99 @@ fn render_font_customization_section(
.gap_1()
.child(Label::new("Editor Font"))
.child(
h_flex().w_full().justify_between().gap_2().child(
PopoverMenu::new("buffer-font-picker")
.menu({
let buffer_font_picker = buffer_font_picker.clone();
move |_window, _cx| Some(buffer_font_picker.clone())
})
.trigger(
ButtonLike::new("buffer-font-family-button")
.style(ButtonStyle::Outlined)
.size(ButtonSize::Medium)
.full_width()
.tab_index({
*tab_index += 1;
*tab_index - 1
})
.child(
h_flex()
.w_full()
.justify_between()
.child(Label::new(buffer_font_family))
.child(
Icon::new(IconName::ChevronUpDown)
.color(Color::Muted)
.size(IconSize::XSmall),
),
),
)
.full_width(true)
.anchor(gpui::Corner::TopLeft)
.offset(gpui::Point {
x: px(0.0),
y: px(4.0),
})
.with_handle(buffer_font_handle),
), // .child( todo!
// NumericStepper::new(
// "buffer-font-size",
// buffer_font_size.to_string(),
// move |size, cx| {
// write_buffer_font_size(Pixels::from(size), cx);
// },
// move |_, _, cx| {
// write_buffer_font_size(buffer_font_size - px(1.), cx);
// },
// move |_, _, cx| {
// write_buffer_font_size(buffer_font_size + px(1.), cx);
// },
// window,
// cx,
// )
// .style(ui_input::NumericStepperStyle::Outlined)
// .tab_index({
// *tab_index += 2;
// *tab_index - 2
// }),
// ),
h_flex()
.w_full()
.justify_between()
.gap_2()
.child(
PopoverMenu::new("buffer-font-picker")
.menu({
let buffer_font_picker = buffer_font_picker.clone();
move |_window, _cx| Some(buffer_font_picker.clone())
})
.trigger(
ButtonLike::new("buffer-font-family-button")
.style(ButtonStyle::Outlined)
.size(ButtonSize::Medium)
.full_width()
.tab_index({
*tab_index += 1;
*tab_index - 1
})
.child(
h_flex()
.w_full()
.justify_between()
.child(Label::new(buffer_font_family))
.child(
Icon::new(IconName::ChevronUpDown)
.color(Color::Muted)
.size(IconSize::XSmall),
),
),
)
.full_width(true)
.anchor(gpui::Corner::TopLeft)
.offset(gpui::Point {
x: px(0.0),
y: px(4.0),
})
.with_handle(buffer_font_handle),
)
.child(font_picker_stepper(
"buffer-font-size",
&buffer_font_size,
tab_index,
write_buffer_font_size,
window,
cx,
)),
),
)
}
fn font_picker_stepper(
id: &'static str,
font_size: &Pixels,
tab_index: &mut isize,
write_font_size: fn(Pixels, &mut App),
window: &mut Window,
cx: &mut App,
) -> NumericStepper<u32> {
window.with_id(id, |window| {
let optimistic_font_size: gpui::Entity<Option<u32>> = window.use_state(cx, |_, _| None);
optimistic_font_size.update(cx, |optimistic_font_size, _| {
if let Some(optimistic_font_size_val) = optimistic_font_size {
if *optimistic_font_size_val == font_size.0 as u32 {
*optimistic_font_size = None;
}
}
});
let stepper_font_size = optimistic_font_size
.read(cx)
.unwrap_or_else(|| font_size.0 as u32);
NumericStepper::new(
SharedString::new(format!("{}-stepper", id)),
stepper_font_size,
window,
cx,
)
.on_change(move |new_value, _, cx| {
optimistic_font_size.write(cx, Some(*new_value));
write_font_size(Pixels::from(*new_value), cx);
})
.style(ui_input::NumericStepperStyle::Outlined)
.tab_index({
*tab_index += 2;
*tab_index - 2
})
.min(6)
.max(32)
})
}
type FontPicker = Picker<FontPickerDelegate>;
pub struct FontPickerDelegate {

View file

@ -1,6 +1,7 @@
use std::{
fmt::Display,
ops::{Add, Sub},
rc::Rc,
str::FromStr,
};
@ -115,7 +116,7 @@ impl_numeric_stepper_int!(u64);
#[derive(RegisterComponent)]
pub struct NumericStepper<T = usize> {
id: ElementId,
value: Entity<T>,
value: T,
style: NumericStepperStyle,
focus_handle: FocusHandle,
mode: Entity<NumericStepperMode>,
@ -126,16 +127,12 @@ pub struct NumericStepper<T = usize> {
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: NumericStepperType> NumericStepper<T> {
pub fn new(
id: impl Into<ElementId>,
value: Entity<T>,
window: &mut Window,
cx: &mut App,
) -> Self {
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| {
@ -157,6 +154,7 @@ impl<T: NumericStepperType> NumericStepper<T> {
min_value: T::min_value(),
max_value: T::max_value(),
on_reset: None,
on_change: Rc::new(|_, _, _| {}),
tab_index: None,
}
}
@ -208,6 +206,11 @@ impl<T: NumericStepperType> NumericStepper<T> {
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: NumericStepperType> IntoElement for NumericStepper<T> {
@ -274,14 +277,14 @@ impl<T: NumericStepperType> RenderOnce for NumericStepper<T> {
.map(|decrement| {
let decrement_handler = {
let value = self.value.clone();
let on_change = self.on_change.clone();
let focus = self.focus_handle.clone();
let min = self.min_value;
move |click: &ClickEvent, window: &mut Window, cx: &mut App| {
let step = get_step(click.modifiers());
let current_value = *value.read(cx);
let new_value = current_value - step;
let new_value = value - step;
let new_value = if new_value < min { min } else { new_value };
value.write(cx, new_value);
on_change(&new_value, window, cx);
window.focus(&focus);
}
};
@ -328,7 +331,7 @@ impl<T: NumericStepperType> RenderOnce for NumericStepper<T> {
.child(match *self.mode.read(cx) {
NumericStepperMode::Read => div()
.id("numeric_stepper_label")
.child(Label::new((self.format)(self.value.read(cx))).mx_3())
.child(Label::new((self.format)(&self.value)).mx_3())
.on_click({
let mode = self.mode.clone();
@ -347,17 +350,13 @@ impl<T: NumericStepperType> RenderOnce for NumericStepper<T> {
|window, cx| {
let mut editor = Editor::single_line(window, cx);
editor.set_text(
format!("{}", self.value.read(cx)),
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 value = self.value.clone();
let min = self.min_value;
let max = self.max_value;
move |this, _, _window, cx| {
let on_change = self.on_change.clone();
move |this, _, window, cx| {
if let Ok(new_value) =
this.text(cx).parse::<T>()
{
@ -368,7 +367,8 @@ impl<T: NumericStepperType> RenderOnce for NumericStepper<T> {
} else {
new_value
};
value.write(cx, new_value);
on_change(&new_value, window, cx);
};
mode.write(cx, NumericStepperMode::Read);
}
@ -397,13 +397,13 @@ impl<T: NumericStepperType> RenderOnce for NumericStepper<T> {
let increment_handler = {
let value = self.value.clone();
let focus = self.focus_handle.clone();
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 current_value = *value.read(cx);
let new_value = current_value + step;
let new_value = value + step;
let new_value = if new_value > max { max } else { new_value };
value.write(cx, new_value);
on_change(&new_value, window, cx);
window.focus(&focus);
}
};
@ -474,20 +474,28 @@ impl Component for NumericStepper<usize> {
"Default",
NumericStepper::new(
"numeric-stepper-component-preview",
first_stepper,
*first_stepper.read(cx),
window,
cx,
)
.on_change({
let first_stepper = first_stepper.clone();
move |value, _, cx| first_stepper.write(cx, *value)
})
.into_any_element(),
),
single_example(
"Outlined",
NumericStepper::new(
"numeric-stepper-with-border-component-preview",
second_stepper,
*second_stepper.read(cx),
window,
cx,
)
.on_change({
let second_stepper = second_stepper.clone();
move |value, _, cx| second_stepper.write(cx, *value)
})
.min(1.0)
.max(100.0)
.style(NumericStepperStyle::Outlined)