diff --git a/src/forms/number.rs b/src/forms/number.rs index 6b9a67f..f51bd29 100644 --- a/src/forms/number.rs +++ b/src/forms/number.rs @@ -62,6 +62,7 @@ fn on_grid(bound: f64, scale: f64, inward: fn(f64) -> f64) -> f64 { struct Numeric { input: Entity, on_change: Option, + on_commit: Option, value: f64, limits: (f64, f64, f64, usize), _events: Subscription, @@ -77,7 +78,7 @@ impl Numeric { /// Shows `value` in the field, then tells the owner. fn show( input: &Entity, - on_change: Option<&OnChange>, + (on_change, on_commit): (Option<&OnChange>, Option<&OnChange>), value: f64, precision: usize, window: &mut Window, @@ -89,21 +90,26 @@ fn show( if let Some(on_change) = on_change { on_change(value, window, cx); } + if let Some(on_commit) = on_commit { + on_commit(value, window, cx); + } } /// Settles `next` into the limits and shows it. fn commit(state: &Entity, next: f64, window: &mut Window, cx: &mut App) { - let (input, on_change, (min, max, _, precision)) = { + let (input, on_change, on_commit, (min, max, _, precision)) = { let numeric = state.read(cx); ( numeric.input.clone(), numeric.on_change.clone(), + numeric.on_commit.clone(), numeric.limits, ) }; let value = settle(next, min, max, precision); state.update(cx, |numeric, _| numeric.value = value); - show(&input, on_change.as_ref(), value, precision, window, cx); + let handlers = (on_change.as_ref(), on_commit.as_ref()); + show(&input, handlers, value, precision, window, cx); } fn numeric(id: &ElementId, window: &mut Window, cx: &mut App) -> Entity { @@ -120,7 +126,7 @@ fn numeric(id: &ElementId, window: &mut Window, cx: &mut App) -> Entity numeric.value = value; show( input, - numeric.on_change.as_ref(), + (numeric.on_change.as_ref(), numeric.on_commit.as_ref()), value, precision, window, @@ -147,6 +153,7 @@ fn numeric(id: &ElementId, window: &mut Window, cx: &mut App) -> Entity Numeric { input, on_change: None, + on_commit: None, value: 0.0, limits: (f64::MIN, f64::MAX, 1.0, 0), _events: events, @@ -215,6 +222,7 @@ pub struct NumberInput { suffix: Option, size: ControlSize, on_change: Option, + on_commit: Option, scrub_label: Option, } @@ -231,6 +239,7 @@ impl NumberInput { suffix: None, size: ControlSize::default(), on_change: None, + on_commit: None, scrub_label: None, } } @@ -288,6 +297,12 @@ impl NumberInput { self.on_change = Some(Rc::new(handler)); self } + + /// Runs on Enter, blur, steps and scrubs; never while typing. + pub fn on_commit(mut self, handler: impl Fn(f64, &mut Window, &mut App) + 'static) -> Self { + self.on_commit = Some(Rc::new(handler)); + self + } } impl RenderOnce for NumberInput { @@ -296,6 +311,7 @@ impl RenderOnce for NumberInput { let (value, min, max, step) = (self.value, self.min, self.max, self.step); state.update(cx, |numeric, _| { numeric.on_change = self.on_change.clone(); + numeric.on_commit = self.on_commit.clone(); numeric.value = value; numeric.limits = (min, max, step, self.precision); }); @@ -465,36 +481,4 @@ impl RenderOnce for ScrubInput { } #[cfg(test)] -mod tests { - use super::{parse, settle}; - - #[test] - fn parses_grouped_and_typographic_minus_and_refuses_junk() { - assert_eq!(parse("1,234.5"), Some(1234.5)); - assert_eq!(parse("\u{2212}3"), Some(-3.0)); - assert_eq!(parse("abc"), None); - assert_eq!(parse("1e999"), None); - } - - #[test] - fn settle_clamps_then_rounds() { - assert_eq!(settle(1.23456, 0.0, 10.0, 2), 1.23); - assert_eq!(settle(-5.0, 0.0, 10.0, 0), 0.0); - assert_eq!(settle(12.6, 0.0, 10.0, 0), 10.0); - } - - #[test] - fn settle_rounds_before_it_clamps() { - assert_eq!(settle(0.2 + 0.1, 0.0, 0.25, 1), 0.2); - assert_eq!(settle(-0.26, -0.25, 1.0, 1), -0.2); - assert_eq!(settle(0.3, 0.0, 0.29, 2), 0.29); - assert_eq!(settle(0.29, 0.29, 0.29, 2), 0.29); - assert_eq!(settle(10000000.01, 0.0, 10000000.005, 2), 10000000.0); - } - - #[test] - #[should_panic(expected = "no 1-place number lies in 0.21..=0.29")] - fn settle_refuses_a_range_without_a_number_at_its_precision() { - settle(0.25, 0.21, 0.29, 1); - } -} +mod tests; diff --git a/src/forms/number/tests.rs b/src/forms/number/tests.rs new file mode 100644 index 0000000..3df8de0 --- /dev/null +++ b/src/forms/number/tests.rs @@ -0,0 +1,31 @@ +use super::{parse, settle}; + +#[test] +fn parses_grouped_and_typographic_minus_and_refuses_junk() { + assert_eq!(parse("1,234.5"), Some(1234.5)); + assert_eq!(parse("\u{2212}3"), Some(-3.0)); + assert_eq!(parse("abc"), None); + assert_eq!(parse("1e999"), None); +} + +#[test] +fn settle_clamps_then_rounds() { + assert_eq!(settle(1.23456, 0.0, 10.0, 2), 1.23); + assert_eq!(settle(-5.0, 0.0, 10.0, 0), 0.0); + assert_eq!(settle(12.6, 0.0, 10.0, 0), 10.0); +} + +#[test] +fn settle_rounds_before_it_clamps() { + assert_eq!(settle(0.2 + 0.1, 0.0, 0.25, 1), 0.2); + assert_eq!(settle(-0.26, -0.25, 1.0, 1), -0.2); + assert_eq!(settle(0.3, 0.0, 0.29, 2), 0.29); + assert_eq!(settle(0.29, 0.29, 0.29, 2), 0.29); + assert_eq!(settle(10000000.01, 0.0, 10000000.005, 2), 10000000.0); +} + +#[test] +#[should_panic(expected = "no 1-place number lies in 0.21..=0.29")] +fn settle_refuses_a_range_without_a_number_at_its_precision() { + settle(0.25, 0.21, 0.29, 1); +} diff --git a/src/forms/tests/mod.rs b/src/forms/tests/mod.rs index 44bbf45..dea7b05 100644 --- a/src/forms/tests/mod.rs +++ b/src/forms/tests/mod.rs @@ -220,6 +220,37 @@ fn a_typed_number_reaches_the_owner_before_any_blur(cx: &mut TestAppContext) { ); } +struct Committed { + values: Vec, +} + +impl Render for Committed { + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + let view = cx.entity(); + NumberInput::new("committed", 4.0) + .on_commit(move |value, _, cx| view.update(cx, |view, _| view.values.push(value))) + } +} + +#[gpui::test] +fn typing_commits_only_on_enter_and_steps(cx: &mut TestAppContext) { + setup(cx); + let (view, cx) = cx.add_window_view(|_, _| Committed { values: Vec::new() }); + cx.update(|window, cx| window.focus_next(cx)); + cx.simulate_keystrokes("cmd-a"); + cx.simulate_input("12"); + assert!( + view.read_with(cx, |view, _| view.values.is_empty()), + "typing waits" + ); + cx.simulate_keystrokes("enter"); + cx.simulate_keystrokes("up"); + assert_eq!( + view.read_with(cx, |view, _| view.values.clone()), + [12.0, 13.0] + ); +} + struct Pin { code: String, }