From 0b1e9fd6407f2f1a884441ba734f634f802edf84 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Tue, 12 Dec 2023 12:48:38 +0000 Subject: [PATCH] Add kas-widgets::edit::InstantParseGuard --- crates/kas-widgets/src/edit.rs | 142 ++++++++++++++++++++++++++++++--- 1 file changed, 131 insertions(+), 11 deletions(-) diff --git a/crates/kas-widgets/src/edit.rs b/crates/kas-widgets/src/edit.rs index 17a1dd17d..852b33a63 100644 --- a/crates/kas-widgets/src/edit.rs +++ b/crates/kas-widgets/src/edit.rs @@ -124,7 +124,11 @@ impl EditGuard for DefaultGuard { } impl_scope! { - /// An [`EditGuard`] impl for string input + /// An [`EditGuard`] for read-only strings + /// + /// This may be used with read-only edit fields, essentially resulting in a + /// fancier version of [`Text`](crate::Text) or + /// [`ScrollText`](crate::ScrollText). #[autoimpl(Debug ignore self.value_fn, self.on_afl)] pub struct StringGuard { value_fn: Box String>, @@ -199,7 +203,11 @@ impl_scope! { } impl_scope! { - /// An [`EditGuard`] impl for simple parsable types (e.g. numbers) + /// An [`EditGuard`] for parsable types + /// + /// This guard displays a value formatted from input data, updates the error + /// state according to parse success on each keystroke, and sends a message + /// on focus loss (where successful parsing occurred). #[autoimpl(Debug ignore self.value_fn, self.on_afl)] pub struct ParseGuard { parsed: Option, @@ -265,6 +273,68 @@ impl_scope! { } } +impl_scope! { + /// An as-you-type [`EditGuard`] for parsable types + /// + /// This guard displays a value formatted from input data, updates the error + /// state according to parse success on each keystroke, and sends a message + /// immediately (where successful parsing occurred). + #[autoimpl(Debug ignore self.value_fn, self.on_afl)] + pub struct InstantParseGuard { + value_fn: Box T>, + on_afl: Box, + } + + impl Self { + /// Construct + /// + /// On update, `value_fn` is used to extract a value from input data + /// which is then formatted as a string via [`Display`]. + /// If, however, the input field has focus, the update is ignored. + /// + /// On every edit, the guard attempts to parse the field's input as type + /// `T` via [`FromStr`]. On success, the result is converted to a + /// message via `on_afl` then emitted via [`EventCx::push`]. + pub fn new( + value_fn: impl Fn(&A) -> T + 'static, + on_afl: impl Fn(T) -> M + 'static, + ) -> Self { + InstantParseGuard { + value_fn: Box::new(value_fn), + on_afl: Box::new(move |cx, value| cx.push(on_afl(value))), + } + } + } + + impl EditGuard for Self { + type Data = A; + + fn focus_lost(edit: &mut EditField, cx: &mut EventCx, data: &A) { + // Always reset data on focus loss + let value = (edit.guard.value_fn)(data); + let action = edit.set_string(format!("{}", value)); + cx.action(edit, action); + } + + fn edit(edit: &mut EditField, cx: &mut EventCx, _: &A) { + let result = edit.get_str().parse(); + let action = edit.set_error_state(result.is_err()); + cx.action(edit.id(), action); + if let Ok(value) = result { + (edit.guard.on_afl)(cx, value); + } + } + + fn update(edit: &mut EditField, cx: &mut ConfigCx, data: &A) { + if !edit.has_edit_focus() { + let value = (edit.guard.value_fn)(data); + let action = edit.set_string(format!("{}", value)); + cx.action(&edit, action); + } + } + } +} + impl_scope! { /// A text-edit box /// @@ -418,16 +488,25 @@ impl EditBox> { } } - /// Construct an `EditField` displaying some `String` value - /// - /// The field is read-only. To make it read-write call [`Self::with_msg`] - /// or [`Self::with_editable`]. + /// Construct a read-only `EditBox` displaying some `String` value #[inline] pub fn string(value_fn: impl Fn(&A) -> String + 'static) -> EditBox> { EditBox::new(StringGuard::new(value_fn)).with_editable(false) } /// Construct an `EditBox` for a parsable value (e.g. a number) + /// + /// On update, `value_fn` is used to extract a value from input data + /// which is then formatted as a string via [`Display`]. + /// If, however, the input field has focus, the update is ignored. + /// + /// On every edit, the guard attempts to parse the field's input as type + /// `T` via [`FromStr`], caching the result and setting the error state. + /// + /// On field activation and focus loss when a `T` value is cached (see + /// previous paragraph), `on_afl` is used to construct a message to be + /// emitted via [`EventCx::push`]. The cached value is then cleared to + /// avoid sending duplicate messages. #[inline] pub fn parser( value_fn: impl Fn(&A) -> T + 'static, @@ -435,6 +514,22 @@ impl EditBox> { ) -> EditBox> { EditBox::new(ParseGuard::new(value_fn, msg_fn)) } + + /// Construct an `EditBox` for a parsable value (e.g. a number) + /// + /// On update, `value_fn` is used to extract a value from input data + /// which is then formatted as a string via [`Display`]. + /// If, however, the input field has focus, the update is ignored. + /// + /// On every edit, the guard attempts to parse the field's input as type + /// `T` via [`FromStr`]. On success, the result is converted to a + /// message via `on_afl` then emitted via [`EventCx::push`]. + pub fn instant_parser( + value_fn: impl Fn(&A) -> T + 'static, + msg_fn: impl Fn(T) -> M + 'static, + ) -> EditBox> { + EditBox::new(InstantParseGuard::new(value_fn, msg_fn)) + } } impl EditBox> { @@ -882,16 +977,25 @@ impl EditField> { } } - /// Construct an `EditField` displaying some `String` value - /// - /// The field is read-only. To make it read-write call [`Self::with_msg`] - /// or [`Self::with_editable`]. + /// Construct a read-only `EditField` displaying some `String` value #[inline] pub fn string(value_fn: impl Fn(&A) -> String + 'static) -> EditField> { EditField::new(StringGuard::new(value_fn)).with_editable(false) } /// Construct an `EditField` for a parsable value (e.g. a number) + /// + /// On update, `value_fn` is used to extract a value from input data + /// which is then formatted as a string via [`Display`]. + /// If, however, the input field has focus, the update is ignored. + /// + /// On every edit, the guard attempts to parse the field's input as type + /// `T` via [`FromStr`], caching the result and setting the error state. + /// + /// On field activation and focus loss when a `T` value is cached (see + /// previous paragraph), `on_afl` is used to construct a message to be + /// emitted via [`EventCx::push`]. The cached value is then cleared to + /// avoid sending duplicate messages. #[inline] pub fn parser( value_fn: impl Fn(&A) -> T + 'static, @@ -899,6 +1003,22 @@ impl EditField> { ) -> EditField> { EditField::new(ParseGuard::new(value_fn, msg_fn)) } + + /// Construct an `EditField` for a parsable value (e.g. a number) + /// + /// On update, `value_fn` is used to extract a value from input data + /// which is then formatted as a string via [`Display`]. + /// If, however, the input field has focus, the update is ignored. + /// + /// On every edit, the guard attempts to parse the field's input as type + /// `T` via [`FromStr`]. On success, the result is converted to a + /// message via `on_afl` then emitted via [`EventCx::push`]. + pub fn instant_parser( + value_fn: impl Fn(&A) -> T + 'static, + msg_fn: impl Fn(T) -> M + 'static, + ) -> EditField> { + EditField::new(InstantParseGuard::new(value_fn, msg_fn)) + } } impl EditField> { @@ -939,7 +1059,7 @@ impl EditField { /// Set the initial text (inline) /// - /// This method should only be used on a new `EditBox`. + /// This method should only be used on a new `EditField`. #[inline] #[must_use] pub fn with_text(mut self, text: impl ToString) -> Self {