Forms · NumberInput on_commit runs on Enter, blur, steps and scrubs, never while typing; number tests move to number/tests.rs
This commit is contained in:
3 files changed
+83
-37
No files matched your search
+21
-37
@@ -62,6 +62,7 @@ fn on_grid(bound: f64, scale: f64, inward: fn(f64) -> f64) -> f64 {
|
|||||||
struct Numeric {
|
struct Numeric {
|
||||||
input: Entity<TextInput>,
|
input: Entity<TextInput>,
|
||||||
on_change: Option<OnChange>,
|
on_change: Option<OnChange>,
|
||||||
|
on_commit: Option<OnChange>,
|
||||||
value: f64,
|
value: f64,
|
||||||
limits: (f64, f64, f64, usize),
|
limits: (f64, f64, f64, usize),
|
||||||
_events: Subscription,
|
_events: Subscription,
|
||||||
@@ -77,7 +78,7 @@ impl Numeric {
|
|||||||
/// Shows `value` in the field, then tells the owner.
|
/// Shows `value` in the field, then tells the owner.
|
||||||
fn show(
|
fn show(
|
||||||
input: &Entity<TextInput>,
|
input: &Entity<TextInput>,
|
||||||
on_change: Option<&OnChange>,
|
(on_change, on_commit): (Option<&OnChange>, Option<&OnChange>),
|
||||||
value: f64,
|
value: f64,
|
||||||
precision: usize,
|
precision: usize,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
@@ -89,21 +90,26 @@ fn show(
|
|||||||
if let Some(on_change) = on_change {
|
if let Some(on_change) = on_change {
|
||||||
on_change(value, window, cx);
|
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.
|
/// Settles `next` into the limits and shows it.
|
||||||
fn commit(state: &Entity<Numeric>, next: f64, window: &mut Window, cx: &mut App) {
|
fn commit(state: &Entity<Numeric>, 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);
|
let numeric = state.read(cx);
|
||||||
(
|
(
|
||||||
numeric.input.clone(),
|
numeric.input.clone(),
|
||||||
numeric.on_change.clone(),
|
numeric.on_change.clone(),
|
||||||
|
numeric.on_commit.clone(),
|
||||||
numeric.limits,
|
numeric.limits,
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
let value = settle(next, min, max, precision);
|
let value = settle(next, min, max, precision);
|
||||||
state.update(cx, |numeric, _| numeric.value = value);
|
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<Numeric> {
|
fn numeric(id: &ElementId, window: &mut Window, cx: &mut App) -> Entity<Numeric> {
|
||||||
@@ -120,7 +126,7 @@ fn numeric(id: &ElementId, window: &mut Window, cx: &mut App) -> Entity<Numeric>
|
|||||||
numeric.value = value;
|
numeric.value = value;
|
||||||
show(
|
show(
|
||||||
input,
|
input,
|
||||||
numeric.on_change.as_ref(),
|
(numeric.on_change.as_ref(), numeric.on_commit.as_ref()),
|
||||||
value,
|
value,
|
||||||
precision,
|
precision,
|
||||||
window,
|
window,
|
||||||
@@ -147,6 +153,7 @@ fn numeric(id: &ElementId, window: &mut Window, cx: &mut App) -> Entity<Numeric>
|
|||||||
Numeric {
|
Numeric {
|
||||||
input,
|
input,
|
||||||
on_change: None,
|
on_change: None,
|
||||||
|
on_commit: None,
|
||||||
value: 0.0,
|
value: 0.0,
|
||||||
limits: (f64::MIN, f64::MAX, 1.0, 0),
|
limits: (f64::MIN, f64::MAX, 1.0, 0),
|
||||||
_events: events,
|
_events: events,
|
||||||
@@ -215,6 +222,7 @@ pub struct NumberInput {
|
|||||||
suffix: Option<AnyElement>,
|
suffix: Option<AnyElement>,
|
||||||
size: ControlSize,
|
size: ControlSize,
|
||||||
on_change: Option<OnChange>,
|
on_change: Option<OnChange>,
|
||||||
|
on_commit: Option<OnChange>,
|
||||||
scrub_label: Option<SharedString>,
|
scrub_label: Option<SharedString>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,6 +239,7 @@ impl NumberInput {
|
|||||||
suffix: None,
|
suffix: None,
|
||||||
size: ControlSize::default(),
|
size: ControlSize::default(),
|
||||||
on_change: None,
|
on_change: None,
|
||||||
|
on_commit: None,
|
||||||
scrub_label: None,
|
scrub_label: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -288,6 +297,12 @@ impl NumberInput {
|
|||||||
self.on_change = Some(Rc::new(handler));
|
self.on_change = Some(Rc::new(handler));
|
||||||
self
|
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 {
|
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);
|
let (value, min, max, step) = (self.value, self.min, self.max, self.step);
|
||||||
state.update(cx, |numeric, _| {
|
state.update(cx, |numeric, _| {
|
||||||
numeric.on_change = self.on_change.clone();
|
numeric.on_change = self.on_change.clone();
|
||||||
|
numeric.on_commit = self.on_commit.clone();
|
||||||
numeric.value = value;
|
numeric.value = value;
|
||||||
numeric.limits = (min, max, step, self.precision);
|
numeric.limits = (min, max, step, self.precision);
|
||||||
});
|
});
|
||||||
@@ -465,36 +481,4 @@ impl RenderOnce for ScrubInput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -220,6 +220,37 @@ fn a_typed_number_reaches_the_owner_before_any_blur(cx: &mut TestAppContext) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct Committed {
|
||||||
|
values: Vec<f64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for Committed {
|
||||||
|
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> 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 {
|
struct Pin {
|
||||||
code: String,
|
code: String,
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user