Implemented `txt_parse`, real time only.

This commit is contained in:
Samuel Guerra 2023-10-31 12:24:34 -03:00
parent 7874213cbc
commit 91000c0feb
6 changed files with 124 additions and 55 deletions

View File

@ -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.

View File

@ -864,52 +864,29 @@ fn form_editor_window(is_open: ArcVar<bool>) -> 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)

View File

@ -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::*;

View File

@ -119,7 +119,7 @@ pub fn has_data_error(child: impl UiNode, any: impl IntoVar<bool>) -> 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<bool>) -> 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

View File

@ -94,15 +94,56 @@ pub struct Text(
#[property(CHILD, capture, default(""), widget_impl(Text))]
pub fn txt(txt: impl IntoVar<Txt>) {}
/// 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<T>(child: impl UiNode, value: impl IntoVar<T>) -> 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<Self, Txt>;
/// 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<T> TxtParseValue for T
where
T: VarValue + std::str::FromStr + std::fmt::Display,
<Self as std::str::FromStr>::Err: std::fmt::Display,
{
fn from_txt(txt: &Txt) -> Result<Self, Txt> {
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| {

View File

@ -2668,14 +2668,53 @@ fn lines_wrap_counter(txt: &ShapedText) -> impl Iterator<Item = u32> + '_ {
pub(super) fn parse_text<T>(child: impl UiNode, value: impl IntoVar<T>) -> 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<Txt>) -> impl UiNode {
child
}
with_context_var(child, PARSE_TXT_VAR, text)
}