Implemented `txt_parse_live` and `PARSE_CMD`.

This commit is contained in:
Samuel Guerra 2023-10-31 16:01:00 -03:00
parent 91000c0feb
commit 445e2122c7
5 changed files with 142 additions and 21 deletions

View File

@ -45,19 +45,25 @@
# DATA Notes
* Implement `txt_parse`.
- Configurable parse moment, real time, with delay or on focus loss.
- 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 control of when `txt_parse` updates.
- `txt_parse_live = false;`.
- We could provide `on_inactive`, `on_enter` and use `on_blur`, user can disable live and call the command in these handlers.
- `on_inactive` needs a time.
* Implement `required`.
- It must set error, but not from the start.
- Some mechanism triggers a validation even if the field was never touched.
- The initial value needs to display as an empty string?
* Define default colors for the 3 levels.
* Implement error style for `TextInput!`.
- Info and warning too.
- Implement `get_top_notes` to only get error/warn/info.
* Implement "What's new" info indicator.
- Blue dot on the top-end of widgets.
- Can lead users on a trail into sub-menus, until the new one is shown.
* Implement `RESTORE_CMD` or `CANCEL_CMD` to set the text back to the current value.
# Publish
* Publish if there is no missing component that could cause a core API refactor.

View File

@ -946,7 +946,7 @@ impl std::str::FromStr for Version {
}
}
if split.next().is_some() {
return Err("expected maximum of 3 version numbers".into())
return Err("expected maximum of 3 version numbers".into());
}
Ok(r)

View File

@ -36,7 +36,12 @@ command! {
name: "Select All",
shortcut: shortcut!(CTRL+'A'),
shortcut_filter: ShortcutFilter::FOCUSED | ShortcutFilter::CMD_ENABLED,
};;
};
/// Parse text and update value if [`txt_parse`] is pending.
///
/// [`txt_parse`]: fn@super::txt_parse
pub static PARSE_CMD;
}
struct SharedTextEditOp {

View File

@ -2671,35 +2671,115 @@ where
T: super::TxtParseValue,
{
let value = value.into_var();
let error = var(Txt::from_static(""));
let mut _error_note = DataNoteHandle::dummy();
#[derive(Clone, Copy, bytemuck::NoUninit)]
#[repr(u8)]
enum State {
Sync,
Requested,
Pending,
}
let state = Arc::new(Atomic::new(State::Sync));
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(
// initial T -> Txt sync
let _ = PARSE_TEXT_VAR.set_from_map(&value, |val| val.to_txt());
// bind `TXT_PARSE_LIVE_VAR` <-> `value` using `bind_filter_map_bidi`:
// - in case of parse error, it is set in `error` variable, that is held by the binding.
// - on error update the DATA note is updated.
// - in case parse is not live, ignores updates (Txt -> None), sets `state` to `Pending`.
// - in case of Pending and `PARSE_CMD` state is set to `Requested` and `TXT_PARSE_LIVE_VAR.update()`.
// - the pending state is also tracked in `TXT_PARSE_PENDING_VAR` and the `PARSE_CMD` handle.
let live = TXT_PARSE_LIVE_VAR.actual_var();
let is_pending = TXT_PARSE_PENDING_VAR.actual_var();
let cmd_handle = Arc::new(super::commands::PARSE_CMD.scoped(WIDGET.id()).subscribe(false));
let binding = PARSE_TEXT_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);
clmv!(state, error, is_pending, cmd_handle, |txt| {
if live.get() || matches!(state.load(Ordering::Relaxed), State::Requested) {
// can try parse
if !matches!(state.swap(State::Sync, Ordering::Relaxed), State::Sync) {
// exit pending state, even if it parse fails
let _ = is_pending.set(false);
cmd_handle.set_enabled(false);
}
// try parse
match T::from_txt(txt) {
Ok(val) => {
error.set(Txt::from_static(""));
Some(val)
}
Err(e) => {
error.set(e);
None
}
}
} else {
// cannot try parse
if !matches!(state.swap(State::Pending, Ordering::Relaxed), State::Pending) {
// enter pending state
let _ = is_pending.set(true);
cmd_handle.set_enabled(true);
}
// does not update the value
None
}
}),
clmv!(error, |val| {
clmv!(state, error, |val| {
// value updated externally, exit error, exit pending.
error.set(Txt::from_static(""));
if !matches!(state.swap(State::Sync, Ordering::Relaxed), State::Sync) {
let _ = is_pending.set(false);
cmd_handle.set_enabled(false);
}
Some(val.to_txt())
}),
);
WIDGET.sub_var(&error).push_var_handles(binding);
// cmd_handle is held by the binding
WIDGET.sub_var(&TXT_PARSE_LIVE_VAR).sub_var(&error).push_var_handles(binding);
}
UiNodeOp::Deinit => {
_error_note = DataNoteHandle::dummy();
}
UiNodeOp::Event { update } => {
if let Some(args) = super::commands::PARSE_CMD.scoped(WIDGET.id()).on_unhandled(update) {
if matches!(state.load(Ordering::Relaxed), State::Pending) {
// requested parse and parse is pending
state.store(State::Requested, Ordering::Relaxed);
let _ = PARSE_TEXT_VAR.update();
args.propagation().stop();
}
}
}
UiNodeOp::Update { .. } => {
if let Some(true) = TXT_PARSE_LIVE_VAR.get_new() {
if matches!(state.load(Ordering::Relaxed), State::Pending) {
// enabled live parse and parse is pending
let _ = PARSE_TEXT_VAR.update();
}
}
if let Some(error) = error.get_new() {
// remove or replace the error
_error_note = if error.is_empty() {
DataNoteHandle::dummy()
} else {
@ -2712,9 +2792,9 @@ where
}
context_var! {
static PARSE_TXT_VAR: Txt = Txt::from_static("");
static PARSE_TEXT_VAR: Txt = Txt::from_static("");
}
pub(super) fn parse_text_ctx(child: impl UiNode, text: BoxedVar<Txt>) -> impl UiNode {
with_context_var(child, PARSE_TXT_VAR, text)
with_context_var(child, PARSE_TEXT_VAR, text)
}

View File

@ -1092,6 +1092,11 @@ context_var! {
/// Selection background color.
pub static SELECTION_COLOR_VAR: Rgba = colors::BLUE.with_alpha(20.pct());
/// If text parse updated for every text change.
pub static TXT_PARSE_LIVE_VAR: bool = true;
pub(super) static TXT_PARSE_PENDING_VAR: bool = false;
}
/// Defines the position of a caret in relation to the selection.
@ -1201,6 +1206,31 @@ pub fn get_lines_wrap_count(child: impl UiNode, lines: impl IntoVar<LinesWrapCou
super::nodes::get_lines_wrap_count(child, lines)
}
/// If [`txt_parse`] tries to parse after any text change immediately.
///
/// This is enabled by default, if disabled the [`PARSE_CMD`] can be used to update pending parse.
///
/// This property sets the [`TXT_PARSE_LIVE_VAR`].
///
/// [`txt_parse`]: fn@super::txt_parse
/// [`PARSE_CMD`]: super::commands::PARSE_CMD
#[property(CONTEXT, default(TXT_PARSE_LIVE_VAR), widget_impl(TextEditMix<P>))]
pub fn txt_parse_live(child: impl UiNode, enabled: impl IntoVar<bool>) -> impl UiNode {
with_context_var(child, TXT_PARSE_LIVE_VAR, enabled)
}
/// If text has changed but [`txt_parse`] has not tried to parse the new text yet.
///
/// This can only be `true` if [`txt_parse_live`] is `false`.
///
/// [`txt_parse`]: fn@super::txt_parse
/// [`txt_parse_live`]: fn@txt_parse_live
#[property(CONTEXT, default(false), widget_impl(TextEditMix<P>))]
pub fn is_parse_pending(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
// reverse context, `txt_parse` sets `TXT_PARSE_PENDING_VAR`
with_context_var(child, TXT_PARSE_PENDING_VAR, state)
}
/// Display info of edit caret position.
#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone, serde::Serialize, serde::Deserialize)]
pub struct CaretStatus {