Implemented `txt_parse_live` and `PARSE_CMD`.
This commit is contained in:
parent
91000c0feb
commit
445e2122c7
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue