From 91000c0feb34b08734981f4443ea53b20b205a1c Mon Sep 17 00:00:00 2001 From: Samuel Guerra Date: Tue, 31 Oct 2023 12:24:34 -0300 Subject: [PATCH] Implemented `txt_parse`, real time only. --- TODO/_current.md | 7 +-- examples/text.rs | 74 +++++++++++--------------- zero-ui/src/lib.rs | 2 + zero-ui/src/properties/data_context.rs | 4 +- zero-ui/src/widgets/text.rs | 45 +++++++++++++++- zero-ui/src/widgets/text/nodes.rs | 47 ++++++++++++++-- 6 files changed, 124 insertions(+), 55 deletions(-) diff --git a/TODO/_current.md b/TODO/_current.md index 88d47ffd9..9440a2f36 100644 --- a/TODO/_current.md +++ b/TODO/_current.md @@ -46,10 +46,11 @@ # DATA Notes * Implement `txt_parse`. - - It must use `DATA.invalidate`. - Configurable parse moment, real time, with delay or on focus loss. - - Capture node, but use it? Because of generic. - - Node must use resolved text as source. + - Can't use a binding for this? + - How to avoid infinite update loop AND delay update? + - When the value var updates there is no delay, only Txt->T has potential delay. + * Implement `required`. - It must set error, but not from the start. - Some mechanism triggers a validation even if the field was never touched. diff --git a/examples/text.rs b/examples/text.rs index fabdb4402..f92b51508 100644 --- a/examples/text.rs +++ b/examples/text.rs @@ -864,52 +864,29 @@ fn form_editor_window(is_open: ArcVar) -> WindowRoot { target = "field-version"; }, { - let version = var(Version::default()); - - let version_err = var(Txt::from_str("")); - let version_ok = version.filter_map_bidi( - clmv!(version_err, |ver| { - version_err.set(Txt::from_str("")); - Some(ver.to_text()) - }), - clmv!(version_err, |txt| { - match txt.parse() { - Ok(ver) => { - version_err.set(Txt::from_str("")); - Some(ver) - }, - Err(e) => { - version_err.set(e); - None - } - } - }), - || Version::default().to_text() - ); - - Stack! { + let error = var(Txt::from_static("")); + Container! { grid::cell::row = 2; grid::cell::column = 1; - direction = StackDirection::top_to_bottom(); - children = ui_vec![ - TextInput! { - id = "field-version"; - txt = version_ok; + child = TextInput! { + id = "field-version"; + txt_parse = var(Version::default()); - when !#{version_err.clone()}.is_empty() { - border = { - widths: 1, - sides: colors::ROSE, - }; - } - }, - Text! { - txt = version_err; - font_color = colors::ROSE; - font_size = 0.8.em(); + when #has_data_error { + border = { + widths: 1, + sides: colors::ROSE, + }; } - ] + }; + + get_data_error_txt = error.clone(); + child_insert_below = Text! { + txt = error; + font_color = colors::ROSE; + font_size = 0.8.em(); + }, 0; } }, ]; @@ -954,13 +931,22 @@ impl std::str::FromStr for Version { let mut split = s.split('.'); if let Some(major) = split.next() { - r.major = u32::from_str(major).map_err(|e| e.to_text())?; + if !major.is_empty() { + r.major = u32::from_str(major).map_err(|e| e.to_text())?; + } } if let Some(minor) = split.next() { - r.minor = u32::from_str(minor).map_err(|e| e.to_text())?; + if !minor.is_empty() { + r.minor = u32::from_str(minor).map_err(|e| e.to_text())?; + } } if let Some(rev) = split.next() { - r.rev = u32::from_str(rev).map_err(|e| e.to_text())?; + if !rev.is_empty() { + r.rev = u32::from_str(rev).map_err(|e| e.to_text())?; + } + } + if split.next().is_some() { + return Err("expected maximum of 3 version numbers".into()) } Ok(r) diff --git a/zero-ui/src/lib.rs b/zero-ui/src/lib.rs index 58baeda59..4d2b665ac 100644 --- a/zero-ui/src/lib.rs +++ b/zero-ui/src/lib.rs @@ -341,6 +341,8 @@ pub mod prelude { #[doc(no_inline)] pub use crate::properties::access::{access_role, accessible}; #[doc(no_inline)] + pub use crate::properties::data_context::{DataNoteHandle, DataNoteLevel}; + #[doc(no_inline)] pub use crate::properties::events::{self, gesture::*, keyboard::*}; #[doc(no_inline)] pub use crate::properties::filters::*; diff --git a/zero-ui/src/properties/data_context.rs b/zero-ui/src/properties/data_context.rs index 237996680..ae9ceace8 100644 --- a/zero-ui/src/properties/data_context.rs +++ b/zero-ui/src/properties/data_context.rs @@ -119,7 +119,7 @@ pub fn has_data_error(child: impl UiNode, any: impl IntoVar) -> impl UiNod /// /// This service enables data flow from a context to descendants, a little like an anonymous context var, and /// from descendants up-to contexts. -/// +/// /// Arbitrary data can be set on a context using the [`data`] property and retrieved using [`DATA.get`] or [`DATA.req`], /// behaving a little like an anonymous context var. Only one data entry and type can exist in a context, nested /// [`data`] properties override the parent data and type in their context. @@ -130,7 +130,7 @@ pub fn has_data_error(child: impl UiNode, any: impl IntoVar) -> impl UiNod /// property gets the error formatted for display. Data notes are aggregated from descendants up-to the context, continuing /// up to outer nested contexts too, this means that you can get data errors for a form field by setting [`get_data_error_txt`] on /// the field widget, and get all form errors from that field and others by also setting [`get_data_error_txt`] in the form widget. -/// +/// /// [`data`]: fn@data /// [`get_data_notes`]: fn@get_data_notes /// [`get_data_error_txt`]: fn@get_data_error_txt diff --git a/zero-ui/src/widgets/text.rs b/zero-ui/src/widgets/text.rs index f2a711585..b83cefd68 100644 --- a/zero-ui/src/widgets/text.rs +++ b/zero-ui/src/widgets/text.rs @@ -94,15 +94,56 @@ pub struct Text( #[property(CHILD, capture, default(""), widget_impl(Text))] pub fn txt(txt: impl IntoVar) {} -/// The value that is converted to and from text. +/// Value that is parsed from the text and displayed as the text. +/// +/// This is an alternative to [`txt`] that converts to and from `T`. If `T: VarValue + Display + FromStr where FromStr::Err: Display` +/// the type is compatible with this property. +/// +/// If the parse operation fails the value variable is not updated and the error display text is set in [`DATA.invalidate`], you +/// can use [`has_data_error`] and [`get_data_error_txt`] to display the error. +/// +/// [`txt`]: fn@txt +/// [`DATA.invalidate`]: crate::properties::data_context::DATA::invalidate +/// [`has_data_error`]: fn@crate::properties::data_context::has_data_error +/// [`get_data_error_txt`]: fn@crate::properties::data_context::get_data_error_txt #[property(CHILD, widget_impl(Text))] pub fn txt_parse(child: impl UiNode, value: impl IntoVar) -> impl UiNode where - T: VarValue + std::str::FromStr + std::fmt::Display, + T: TxtParseValue, { nodes::parse_text(child, value) } +/// A type that can be a var value, parse and display. +/// +/// This trait is used by [`txt_parse`]. It is implemented for all types that are +/// `VarValue + FromStr + Display where FromStr::Err: Display`. +/// +/// [`txt_parse`]: fn@txt_parse +pub trait TxtParseValue: VarValue { + /// Try parse `Self` from `txt`, formats the error for display. + /// + /// Note that the widget context is not available here as this method is called in the app context. + fn from_txt(txt: &Txt) -> Result; + /// Display the value, the returned text can be parsed back to an equal value. + /// + /// Note that the widget context is not available here as this method is called in the app context. + fn to_txt(&self) -> Txt; +} +impl TxtParseValue for T +where + T: VarValue + std::str::FromStr + std::fmt::Display, + ::Err: std::fmt::Display, +{ + fn from_txt(txt: &Txt) -> Result { + T::from_str(txt).map_err(|e| e.to_text()) + } + + fn to_txt(&self) -> Txt { + self.to_text() + } +} + impl Text { fn widget_intrinsic(&mut self) { self.widget_builder().push_build_action(|wgt| { diff --git a/zero-ui/src/widgets/text/nodes.rs b/zero-ui/src/widgets/text/nodes.rs index e7d947ca1..e2963d7ed 100644 --- a/zero-ui/src/widgets/text/nodes.rs +++ b/zero-ui/src/widgets/text/nodes.rs @@ -2668,14 +2668,53 @@ fn lines_wrap_counter(txt: &ShapedText) -> impl Iterator + '_ { pub(super) fn parse_text(child: impl UiNode, value: impl IntoVar) -> impl UiNode where - T: VarValue + std::str::FromStr + std::fmt::Display, + T: super::TxtParseValue, { let value = value.into_var(); - match_node(child, move |c, op| match op { + let error = var(Txt::from_static("")); + let mut _error_note = DataNoteHandle::dummy(); + match_node(child, move |_, op| match op { + UiNodeOp::Init => { + let _ = PARSE_TXT_VAR.set_from_map(&value, |val| val.to_txt()); + let binding = PARSE_TXT_VAR.bind_filter_map_bidi( + &value, + clmv!(error, |txt| match T::from_txt(txt) { + Ok(val) => { + error.set(Txt::from_static("")); + Some(val) + } + Err(e) => { + error.set(e); + None + } + }), + clmv!(error, |val| { + error.set(Txt::from_static("")); + Some(val.to_txt()) + }), + ); + WIDGET.sub_var(&error).push_var_handles(binding); + } + UiNodeOp::Deinit => { + _error_note = DataNoteHandle::dummy(); + } + UiNodeOp::Update { .. } => { + if let Some(error) = error.get_new() { + _error_note = if error.is_empty() { + DataNoteHandle::dummy() + } else { + DATA.invalidate(error) + }; + } + } _ => {} }) } +context_var! { + static PARSE_TXT_VAR: Txt = Txt::from_static(""); +} + pub(super) fn parse_text_ctx(child: impl UiNode, text: BoxedVar) -> impl UiNode { - child -} \ No newline at end of file + with_context_var(child, PARSE_TXT_VAR, text) +}