zng_wgt_text/
node.rs

1//! UI nodes used for building a text widget.
2
3use std::{fmt, num::Wrapping, ops, sync::Arc};
4
5use super::text_properties::*;
6use atomic::{Atomic, Ordering};
7use parking_lot::RwLock;
8use zng_app::render::FontSynthesis;
9use zng_app_context::{MappedRwLockWriteGuardOwned, RwLockReadGuardOwned, RwLockWriteGuardOwned};
10use zng_ext_font::{CaretIndex, FontFaceList, FontList, SegmentedText, ShapedLine, ShapedText, TextOverflowInfo};
11use zng_ext_input::{
12    focus::FOCUS_CHANGED_EVENT,
13    keyboard::{KEY_INPUT_EVENT, Key, KeyState},
14    mouse::MOUSE_INPUT_EVENT,
15    touch::{TOUCH_INPUT_EVENT, TOUCH_LONG_PRESS_EVENT},
16};
17use zng_ext_window::WINDOW_Ext as _;
18use zng_view_api::{mouse::ButtonState, touch::TouchPhase};
19use zng_wgt::prelude::*;
20use zng_wgt_data::{DATA, DataNoteHandle};
21use zng_wgt_layer::{
22    AnchorMode, AnchorTransform,
23    popup::{ContextCapture, POPUP, PopupState},
24};
25
26mod resolve;
27pub use resolve::*;
28
29mod layout;
30pub use layout::*;
31
32mod render;
33pub use render::*;
34
35mod caret;
36pub use caret::*;
37
38/// Represents the caret position at the [`ResolvedText`] level.
39#[derive(Clone)]
40pub struct CaretInfo {
41    /// Caret opacity.
42    ///
43    /// This variable is replaced often, the text resolver subscribes to it for
44    /// [`UpdateOp::RenderUpdate`] automatically.
45    ///
46    /// [`UpdateOp::RenderUpdate`]: zng_wgt::prelude::UpdateOp::RenderUpdate
47    pub opacity: ReadOnlyArcVar<Factor>,
48
49    /// Caret byte offset in the text string.
50    ///
51    /// This is the insertion offset on the text, it can be the text length.
52    pub index: Option<CaretIndex>,
53
54    /// Second index that defines the start or end of a selection range.
55    pub selection_index: Option<CaretIndex>,
56
57    /// Selection by word or line sets this value, selection extend by word or line
58    /// grows from this central selection. The value is `(selection, is_word)`.
59    pub initial_selection: Option<(ops::Range<CaretIndex>, bool)>,
60
61    /// Value incremented by one every time the `index` is set.
62    ///
63    /// This is used to signal interaction with the `index` value by [`TextEditOp`]
64    /// even if the interaction only sets-it to the index same value.
65    ///
66    /// [`TextEditOp`]: crate::cmd::TextEditOp
67    pub index_version: Wrapping<u8>,
68
69    /// If the index was set by using the [`caret_retained_x`].
70    ///
71    /// [`caret_retained_x`]: LaidoutText::caret_retained_x
72    pub used_retained_x: bool,
73
74    /// Don't scroll to new caret position on the next update.
75    ///
76    /// If this is set to `true` the next time `index` or `index_version` changes auto-scroll is skipped once.
77    pub skip_next_scroll: bool,
78}
79impl fmt::Debug for CaretInfo {
80    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81        f.debug_struct("CaretInfo")
82            .field("opacity", &self.opacity.debug())
83            .field("index", &self.index)
84            .field("index_version", &self.index_version)
85            .field("used_retained_x", &self.used_retained_x)
86            .finish()
87    }
88}
89impl CaretInfo {
90    /// Set the index and update the index version.
91    pub fn set_index(&mut self, index: CaretIndex) {
92        self.index = Some(index);
93        self.index_version += 1;
94    }
95
96    /// Sets the selection start, end and update the index version.
97    ///
98    /// The `end` is the caret position.
99    pub fn set_selection(&mut self, start: CaretIndex, end: CaretIndex) {
100        self.selection_index = Some(start);
101        self.set_index(end);
102    }
103
104    /// Clears selection.
105    pub fn clear_selection(&mut self) {
106        self.selection_index = None;
107        self.initial_selection = None;
108        self.index_version += 1;
109    }
110
111    /// Set the char byte index and update the index version.
112    ///
113    /// The caret line is always snapped when the caret changes, so the line value will be updated.
114    pub fn set_char_index(&mut self, index: usize) {
115        if let Some(i) = &mut self.index {
116            i.index = index;
117        } else {
118            self.index = Some(CaretIndex { index, line: 0 });
119        }
120        self.index_version += 1;
121    }
122
123    /// Set the char byte index of the selection start, end and update the index version.
124    ///
125    /// The `end` is the caret position.
126    ///
127    /// The caret and selection lines are always snapped when the caret changes, so the line values will be updated.
128    pub fn set_char_selection(&mut self, start: usize, end: usize) {
129        if let Some(s) = &mut self.selection_index {
130            s.index = start;
131        } else {
132            self.selection_index = Some(CaretIndex { index: start, line: 0 });
133        }
134        self.set_char_index(end);
135    }
136
137    /// Gets the selection range if both [`index`] and [`selection_index`] are set.
138    ///
139    /// [`index`]: Self::index
140    /// [`selection_index`]: Self::selection_index
141    pub fn selection_range(&self) -> Option<ops::Range<CaretIndex>> {
142        let a = self.index?;
143        let b = self.selection_index?;
144
145        use std::cmp::Ordering;
146        match a.index.cmp(&b.index) {
147            Ordering::Less => Some(a..b),
148            Ordering::Equal => None,
149            Ordering::Greater => Some(b..a),
150        }
151    }
152
153    /// Gets the character range of the selection if both [`index`] and [`selection_index`] are set.
154    ///
155    /// [`index`]: Self::index
156    /// [`selection_index`]: Self::selection_index
157    pub fn selection_char_range(&self) -> Option<ops::Range<usize>> {
158        self.selection_range().map(|r| r.start.index..r.end.index)
159    }
160}
161
162/// IME text edit that is not committed yet.
163#[derive(Clone)]
164pub struct ImePreview {
165    /// The inserted text.
166    pub txt: Txt,
167
168    /// Caret index when IME started.
169    pub prev_caret: CaretIndex,
170    /// Selection index when IME started.
171    ///
172    /// If set defines a selection of the text variable that is replaced with the `txt`.
173    pub prev_selection: Option<CaretIndex>,
174}
175
176/// Text internals used by text implementer nodes and properties.
177///
178/// The text implementation is split between two contexts, [`resolve_text`] and [`layout_text`], this service
179/// provides access to data produced y these two contexts.
180pub struct TEXT;
181
182impl TEXT {
183    /// Read lock the current contextual resolved text if called in a node inside [`resolve_text`].
184    ///
185    /// Note that this will block until a read lock can be acquired.
186    pub fn try_resolved(&self) -> Option<RwLockReadGuardOwned<ResolvedText>> {
187        if RESOLVED_TEXT.is_default() {
188            None
189        } else {
190            Some(RESOLVED_TEXT.read_recursive())
191        }
192    }
193
194    /// Read lock the current contextual resolved text.
195    ///
196    /// # Panics
197    ///
198    /// Panics if requested in a node outside [`resolve_text`].
199    pub fn resolved(&self) -> RwLockReadGuardOwned<ResolvedText> {
200        RESOLVED_TEXT.read_recursive()
201    }
202
203    /// Read lock the current contextual laidout text if called in a node inside [`layout_text`].
204    ///
205    /// Note that this will block until a read lock can be acquired.
206    pub fn try_laidout(&self) -> Option<RwLockReadGuardOwned<LaidoutText>> {
207        if LAIDOUT_TEXT.is_default() {
208            None
209        } else {
210            Some(LAIDOUT_TEXT.read_recursive())
211        }
212    }
213
214    /// Read lock the current contextual laidout text.
215    ///
216    /// # Panics
217    ///
218    /// Panics if not available in context. Is only available inside [`layout_text`] after the first layout.
219    pub fn laidout(&self) -> RwLockReadGuardOwned<LaidoutText> {
220        LAIDOUT_TEXT.read_recursive()
221    }
222
223    /// Write lock the current contextual resolved text to edit the caret.
224    ///     
225    /// # Panics
226    ///
227    /// Panics if requested in a node outside [`resolve_text`].
228    pub fn resolve_caret(&self) -> MappedRwLockWriteGuardOwned<ResolvedText, CaretInfo> {
229        RwLockWriteGuardOwned::map(self.resolve(), |ctx| &mut ctx.caret)
230    }
231
232    pub(crate) fn resolve(&self) -> RwLockWriteGuardOwned<ResolvedText> {
233        RESOLVED_TEXT.write()
234    }
235
236    fn layout(&self) -> RwLockWriteGuardOwned<LaidoutText> {
237        LAIDOUT_TEXT.write()
238    }
239}
240
241/// Defines the source of the current selection.
242///
243/// See [`ResolvedText::selection_by`] for more details.
244#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, bytemuck::NoUninit)]
245#[repr(u8)]
246pub enum SelectionBy {
247    /// Command or other programmatic selection.
248    Command = 0,
249    /// Key press.
250    Keyboard = 1,
251    /// Mouse drag.
252    Mouse = 2,
253    /// Touch drag.
254    Touch = 3,
255}
256impl SelectionBy {
257    /// Returns `true` if the interactive carets must be used for the current selection given the interactive caret mode.
258    pub fn matches_interactive_mode(self, mode: InteractiveCaretMode) -> bool {
259        match mode {
260            InteractiveCaretMode::TouchOnly => matches!(self, SelectionBy::Touch),
261            InteractiveCaretMode::Enabled => true,
262            InteractiveCaretMode::Disabled => false,
263        }
264    }
265}
266
267/// Represents the resolved fonts and the transformed, white space corrected and segmented text.
268///
269/// Use [`TEXT`] to get.
270pub struct ResolvedText {
271    /// The text source variable.
272    pub txt: BoxedVar<Txt>,
273    /// IME text edit that is not committed yet. Only the text in the segmented and shaped text is edited,
274    /// the text variable is not updated yet and undo is not tracking these changes.
275    pub ime_preview: Option<ImePreview>,
276
277    /// Text transformed, white space corrected and segmented.
278    pub segmented_text: SegmentedText,
279    /// Queried font faces.
280    pub faces: FontFaceList,
281    /// Font synthesis allowed by the text context and required to render the best font match.
282    pub synthesis: FontSynthesis,
283
284    /// Layout that needs to be recomputed as identified by the text resolver node.
285    ///
286    /// This is added to the layout invalidation by the layout node itself. When set a layout must
287    /// be requested for the widget.
288    pub pending_layout: PendingLayout,
289
290    /// Text modification is scheduled, caret info will only be valid after update.
291    pub pending_edit: bool,
292
293    /// Caret index and animation.
294    pub caret: CaretInfo,
295
296    /// Source of the current selection.
297    pub selection_by: SelectionBy,
298
299    /// If the selection toolbar is open.
300    pub selection_toolbar_is_open: bool,
301}
302impl fmt::Debug for ResolvedText {
303    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
304        f.debug_struct("ResolvedText")
305            .field("segmented_text", &self.segmented_text)
306            .field("faces", &self.faces)
307            .field("synthesis", &self.synthesis)
308            .field("pending_layout", &self.pending_layout)
309            .field("pending_edit", &self.pending_edit)
310            .field("caret", &self.caret)
311            .field("selection_by", &self.selection_by)
312            .field("selection_toolbar_is_open", &self.selection_toolbar_is_open)
313            .finish_non_exhaustive()
314    }
315}
316impl ResolvedText {
317    fn no_context() -> Self {
318        panic!("no `ResolvedText` in context, only available inside `resolve_text`")
319    }
320}
321
322/// Info about the last text render or render update.
323#[derive(Debug, Clone)]
324pub struct RenderInfo {
325    /// Render transform of the text, in the window space.
326    pub transform: PxTransform,
327    /// Render scale factor of the text.
328    pub scale_factor: Factor,
329}
330impl Default for RenderInfo {
331    /// Identify, 1.fct()
332    fn default() -> Self {
333        Self {
334            transform: PxTransform::identity(),
335            scale_factor: 1.fct(),
336        }
337    }
338}
339
340/// Represents the laidout text.
341///
342/// Use [`TEXT`] to get.
343#[derive(Debug)]
344pub struct LaidoutText {
345    /// Sized [`faces`].
346    ///
347    /// [`faces`]: ResolvedText::faces
348    pub fonts: FontList,
349
350    /// Layout text.
351    pub shaped_text: ShapedText,
352
353    /// Shaped text overflow info.
354    pub overflow: Option<TextOverflowInfo>,
355
356    /// Shaped text used as suffix when `shaped_text` overflows.
357    pub overflow_suffix: Option<ShapedText>,
358
359    /// Version updated every time the `shaped_text` is reshaped.
360    pub shaped_text_version: u32,
361
362    /// List of overline segments, defining origin and width of each line.
363    ///
364    /// Note that overlines are only computed if the `overline_thickness` is more than `0`.
365    ///
366    /// Default overlines are rendered by [`render_overlines`].
367    pub overlines: Vec<(PxPoint, Px)>,
368
369    /// Computed [`OVERLINE_THICKNESS_VAR`].
370    pub overline_thickness: Px,
371
372    /// List of strikethrough segments, defining origin and width of each line.
373    ///
374    /// Note that strikethroughs are only computed if the `strikethrough_thickness` is more than `0`.
375    ///
376    /// Default overlines are rendered by [`render_strikethroughs`].
377    pub strikethroughs: Vec<(PxPoint, Px)>,
378    /// Computed [`STRIKETHROUGH_THICKNESS_VAR`].
379    pub strikethrough_thickness: Px,
380
381    /// List of underline segments, defining origin and width of each line.
382    ///
383    /// Note that underlines are only computed if the `underline_thickness` is more than `0`. These
384    /// underlines never cover the IME review text range.
385    ///
386    /// Default underlines are rendered by [`render_underlines`].
387    pub underlines: Vec<(PxPoint, Px)>,
388    /// Computed [`UNDERLINE_THICKNESS_VAR`].
389    pub underline_thickness: Px,
390
391    /// List of underline segments for IME preview text, defining origin and width of each line.
392    ///
393    /// Note that underlines are only computed if the `ime_underline_thickness` is more than `0`.
394    ///
395    /// Default underlines are rendered by [`render_underlines`].
396    pub ime_underlines: Vec<(PxPoint, Px)>,
397    /// Computed [`IME_UNDERLINE_THICKNESS_VAR`].
398    pub ime_underline_thickness: Px,
399
400    /// Top-middle offset of the caret index in the shaped text.
401    pub caret_origin: Option<PxPoint>,
402
403    /// Top-middle offset of the caret selection_index in the shaped text.
404    pub caret_selection_origin: Option<PxPoint>,
405
406    /// The x offset used when pressing up or down.
407    pub caret_retained_x: Px,
408
409    /// Info about the last text render or render update.
410    pub render_info: RenderInfo,
411
412    /// Latest layout viewport.
413    pub viewport: PxSize,
414}
415
416impl LaidoutText {
417    fn no_context() -> Self {
418        panic!("no `LaidoutText` in context, only available inside `layout_text`")
419    }
420}
421
422context_local! {
423    /// Represents the contextual [`ResolvedText`] setup by the [`resolve_text`] node.
424    static RESOLVED_TEXT: RwLock<ResolvedText> = RwLock::new(ResolvedText::no_context());
425    /// Represents the contextual [`LaidoutText`] setup by the [`layout_text`] node.
426    static LAIDOUT_TEXT: RwLock<LaidoutText> = RwLock::new(LaidoutText::no_context());
427}
428
429bitflags! {
430    /// Text layout parts that need rebuild.
431    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
432    pub struct PendingLayout: u8 {
433        /// Underline size and position.
434        const UNDERLINE     = 0b0000_0001;
435        /// Strikethrough size and position.
436        const STRIKETHROUGH = 0b0000_0010;
437        /// Overline size and position.
438        const OVERLINE      = 0b0000_0100;
439        /// Caret origin.
440        const CARET         = 0b0000_1000;
441        /// Overflow.
442        const OVERFLOW      = 0b0001_0000;
443        /// Text lines position, retains line glyphs but reposition for new align and outer box.
444        const RESHAPE_LINES = 0b0111_1111;
445        /// Full reshape, re-compute all glyphs.
446        const RESHAPE       = 0b1111_1111;
447    }
448}
449
450/// Create a node that is sized one text line height by `width`.
451///
452/// This node can be used to reserve space for a full text in lazy loading contexts.
453///
454/// The contextual variables affect the layout size.
455pub fn line_placeholder(width: impl IntoVar<Length>) -> impl UiNode {
456    let child = layout_text(FillUiNode);
457    let child = resolve_text(child, " ");
458    zng_wgt_size_offset::width(child, width)
459}
460
461pub(super) fn get_caret_index(child: impl UiNode, index: impl IntoVar<Option<CaretIndex>>) -> impl UiNode {
462    let index = index.into_var();
463    match_node(child, move |c, op| {
464        let mut u = false;
465        match op {
466            UiNodeOp::Init => {
467                c.init();
468                let _ = index.set(TEXT.resolved().caret.index);
469            }
470            UiNodeOp::Deinit => {
471                let _ = index.set(None);
472            }
473            UiNodeOp::Event { update } => {
474                c.event(update);
475                u = true;
476            }
477            UiNodeOp::Update { updates } => {
478                c.update(updates);
479                u = true;
480            }
481            _ => {}
482        }
483        if u {
484            let t = TEXT.resolved();
485            let idx = t.caret.index;
486            if !t.pending_edit && index.get() != idx {
487                let _ = index.set(idx);
488            }
489        }
490    })
491}
492
493pub(super) fn get_caret_status(child: impl UiNode, status: impl IntoVar<CaretStatus>) -> impl UiNode {
494    let status = status.into_var();
495    match_node(child, move |c, op| {
496        let mut u = false;
497        match op {
498            UiNodeOp::Init => {
499                c.init();
500                let t = TEXT.resolved();
501                let _ = status.set(match t.caret.index {
502                    None => CaretStatus::none(),
503                    Some(i) => CaretStatus::new(i.index, &t.segmented_text),
504                });
505            }
506            UiNodeOp::Deinit => {
507                let _ = status.set(CaretStatus::none());
508            }
509            UiNodeOp::Event { update } => {
510                c.event(update);
511                u = true;
512            }
513            UiNodeOp::Update { updates } => {
514                c.update(updates);
515                u = true;
516            }
517            _ => {}
518        }
519        if u {
520            let t = TEXT.resolved();
521            let idx = t.caret.index;
522            if !t.pending_edit && status.get().index() != idx.map(|ci| ci.index) {
523                let _ = status.set(match idx {
524                    None => CaretStatus::none(),
525                    Some(i) => CaretStatus::new(i.index, &t.segmented_text),
526                });
527            }
528        }
529    })
530}
531
532pub(super) fn get_lines_len(child: impl UiNode, len: impl IntoVar<usize>) -> impl UiNode {
533    let len = len.into_var();
534    match_node(child, move |c, op| match op {
535        UiNodeOp::Deinit => {
536            let _ = len.set(0usize);
537        }
538        UiNodeOp::Layout { wl, final_size } => {
539            *final_size = c.layout(wl);
540            let t = TEXT.laidout();
541            let l = t.shaped_text.lines_len();
542            if l != len.get() {
543                let _ = len.set(t.shaped_text.lines_len());
544            }
545        }
546        _ => {}
547    })
548}
549
550pub(super) fn get_lines_wrap_count(child: impl UiNode, lines: impl IntoVar<super::LinesWrapCount>) -> impl UiNode {
551    let lines = lines.into_var();
552    let mut version = 0;
553    match_node(child, move |c, op| match op {
554        UiNodeOp::Deinit => {
555            let _ = lines.set(super::LinesWrapCount::NoWrap(0));
556        }
557        UiNodeOp::Layout { wl, final_size } => {
558            *final_size = c.layout(wl);
559            let t = TEXT.laidout();
560            if t.shaped_text_version != version {
561                version = t.shaped_text_version;
562                if let Some(update) = lines.with(|l| lines_wrap_count(l, &t.shaped_text)) {
563                    let _ = lines.set(update);
564                }
565            }
566        }
567        _ => {}
568    })
569}
570// Returns `Some(_)` if the current wrap count changed from `prev`. Only allocates if new count has wrapped lines.
571fn lines_wrap_count(prev: &super::LinesWrapCount, txt: &ShapedText) -> Option<super::LinesWrapCount> {
572    match prev {
573        super::LinesWrapCount::NoWrap(len) => {
574            let mut counter = lines_wrap_counter(txt);
575            let mut l = 0;
576            for c in &mut counter {
577                if c != 1 {
578                    // at least one line wraps now
579                    let mut wrap = vec![1; l];
580                    wrap.push(c);
581                    wrap.extend(&mut counter);
582                    return Some(super::LinesWrapCount::Wrap(wrap));
583                }
584                l += 1;
585            }
586            if l != *len {
587                // no line wraps, but changed line count.
588                Some(super::LinesWrapCount::NoWrap(l))
589            } else {
590                None
591            }
592        }
593        super::LinesWrapCount::Wrap(counts) => {
594            // find `counts[i]` that diverges from counts, OR
595            // find if all new counts is now NoWrap
596            let mut prev_counts = counts.iter();
597            let mut new_counts = lines_wrap_counter(txt);
598            let mut eq_l = 0;
599            let mut eq_wrap = false;
600            for c in &mut new_counts {
601                if prev_counts.next() == Some(&c) {
602                    eq_l += 1;
603                    eq_wrap |= c != 1;
604                } else if eq_wrap || c != 1 {
605                    // not eq, and already found a wrap line
606                    let mut wrap = counts[..eq_l].to_vec();
607                    wrap.push(c);
608                    wrap.extend(&mut new_counts);
609                    return Some(super::LinesWrapCount::Wrap(wrap));
610                } else {
611                    // not eq, but maybe no wrap
612                    let mut l = eq_l + 1; // +1 is +c
613                    for c in &mut new_counts {
614                        if c != 1 {
615                            // nope, found a line wrap
616                            let mut wrap = vec![1; l];
617                            wrap.push(c);
618                            wrap.extend(&mut new_counts);
619                            return Some(super::LinesWrapCount::Wrap(wrap));
620                        }
621                        l += 1;
622                    }
623                    // changed to no wrap
624                    return Some(super::LinesWrapCount::NoWrap(l));
625                }
626            }
627            if prev_counts.next().is_some() {
628                Some(super::LinesWrapCount::Wrap(counts[..eq_l].to_vec()))
629            } else {
630                None
631            }
632        }
633    }
634}
635fn lines_wrap_counter(txt: &ShapedText) -> impl Iterator<Item = u32> + '_ {
636    struct Counter<I> {
637        lines: I,
638        count: u32,
639    }
640    impl<'a, I: Iterator<Item = ShapedLine<'a>>> Iterator for Counter<I> {
641        type Item = u32;
642
643        fn next(&mut self) -> Option<u32> {
644            loop {
645                let line = self.lines.next()?;
646                if line.ended_by_wrap() {
647                    self.count += 1;
648                    continue;
649                }
650
651                let c = self.count;
652                self.count = 1;
653
654                return Some(c);
655            }
656        }
657    }
658    Counter {
659        lines: txt.lines(),
660        count: 1,
661    }
662}
663
664pub(super) fn parse_text<T>(child: impl UiNode, value: impl IntoVar<T>) -> impl UiNode
665where
666    T: super::TxtParseValue,
667{
668    let value = value.into_var();
669
670    let error = var(Txt::from_static(""));
671    let mut _error_note = DataNoteHandle::dummy();
672
673    #[derive(Clone, Copy, bytemuck::NoUninit)]
674    #[repr(u8)]
675    enum State {
676        Sync,
677        Requested,
678        Pending,
679    }
680    let state = Arc::new(Atomic::new(State::Sync));
681
682    match_node(child, move |_, op| match op {
683        UiNodeOp::Init => {
684            let ctx = TEXT.resolved();
685
686            // initial T -> Txt sync
687            let _ = ctx.txt.set_from_map(&value, |val| val.to_txt());
688
689            // bind `TXT_PARSE_LIVE_VAR` <-> `value` using `bind_filter_map_bidi`:
690            // - in case of parse error, it is set in `error` variable, that is held by the binding.
691            // - on error update the DATA note is updated.
692            // - in case parse is not live, ignores updates (Txt -> None), sets `state` to `Pending`.
693            // - in case of Pending and `PARSE_CMD` state is set to `Requested` and `TXT_PARSE_LIVE_VAR.update()`.
694            // - the pending state is also tracked in `TXT_PARSE_PENDING_VAR` and the `PARSE_CMD` handle.
695
696            let live = TXT_PARSE_LIVE_VAR.actual_var();
697            let is_pending = TXT_PARSE_PENDING_VAR.actual_var();
698            let cmd_handle = Arc::new(super::cmd::PARSE_CMD.scoped(WIDGET.id()).subscribe(false));
699
700            let binding = ctx.txt.bind_filter_map_bidi(
701                &value,
702                clmv!(state, error, is_pending, cmd_handle, |txt| {
703                    if live.get() || matches!(state.load(Ordering::Relaxed), State::Requested) {
704                        // can try parse
705
706                        if !matches!(state.swap(State::Sync, Ordering::Relaxed), State::Sync) {
707                            // exit pending state, even if it parse fails
708                            let _ = is_pending.set(false);
709                            cmd_handle.set_enabled(false);
710                        }
711
712                        // try parse
713                        match T::from_txt(txt) {
714                            Ok(val) => {
715                                error.set(Txt::from_static(""));
716                                Some(val)
717                            }
718                            Err(e) => {
719                                error.set(e);
720                                None
721                            }
722                        }
723                    } else {
724                        // cannot try parse
725
726                        if !matches!(state.swap(State::Pending, Ordering::Relaxed), State::Pending) {
727                            // enter pending state
728                            let _ = is_pending.set(true);
729                            cmd_handle.set_enabled(true);
730                        }
731
732                        // does not update the value
733                        None
734                    }
735                }),
736                clmv!(state, error, |val| {
737                    // value updated externally, exit error, exit pending.
738
739                    error.set(Txt::from_static(""));
740
741                    if !matches!(state.swap(State::Sync, Ordering::Relaxed), State::Sync) {
742                        let _ = is_pending.set(false);
743                        cmd_handle.set_enabled(false);
744                    }
745
746                    Some(val.to_txt())
747                }),
748            );
749
750            // cmd_handle is held by the binding
751
752            WIDGET.sub_var(&TXT_PARSE_LIVE_VAR).sub_var(&error).push_var_handles(binding);
753        }
754        UiNodeOp::Deinit => {
755            _error_note = DataNoteHandle::dummy();
756        }
757        UiNodeOp::Event { update } => {
758            if let Some(args) = super::cmd::PARSE_CMD.scoped(WIDGET.id()).on_unhandled(update) {
759                if matches!(state.load(Ordering::Relaxed), State::Pending) {
760                    // requested parse and parse is pending
761
762                    state.store(State::Requested, Ordering::Relaxed);
763                    let _ = TEXT.resolved().txt.update();
764                    args.propagation().stop();
765                }
766            }
767        }
768        UiNodeOp::Update { .. } => {
769            if let Some(true) = TXT_PARSE_LIVE_VAR.get_new() {
770                if matches!(state.load(Ordering::Relaxed), State::Pending) {
771                    // enabled live parse and parse is pending
772
773                    let _ = TEXT.resolved().txt.update();
774                }
775            }
776
777            if let Some(error) = error.get_new() {
778                // remove or replace the error
779
780                _error_note = if error.is_empty() {
781                    DataNoteHandle::dummy()
782                } else {
783                    DATA.invalidate(error)
784                };
785            }
786        }
787        _ => {}
788    })
789}
790
791pub(super) fn on_change_stop(child: impl UiNode, mut handler: impl WidgetHandler<ChangeStopArgs>) -> impl UiNode {
792    let mut pending = None;
793    match_node(child, move |c, op| match op {
794        UiNodeOp::Event { update } => {
795            if pending.is_none() {
796                return;
797            }
798
799            if let Some(args) = KEY_INPUT_EVENT.on_unhandled(update) {
800                if let (KeyState::Pressed, Key::Enter) = (args.state, &args.key) {
801                    if !ACCEPTS_ENTER_VAR.get() {
802                        pending = None;
803                        handler.event(&ChangeStopArgs {
804                            cause: ChangeStopCause::Enter,
805                        });
806                    }
807                }
808            } else if let Some(args) = FOCUS_CHANGED_EVENT.on(update) {
809                let target = WIDGET.id();
810                if args.is_blur(target) {
811                    pending = None;
812                    handler.event(&ChangeStopArgs {
813                        cause: ChangeStopCause::Blur,
814                    });
815                }
816            }
817        }
818        UiNodeOp::Update { updates } => {
819            if TEXT.resolved().txt.is_new() {
820                let deadline = TIMERS.deadline(CHANGE_STOP_DELAY_VAR.get());
821                deadline.subscribe(UpdateOp::Update, WIDGET.id()).perm();
822                pending = Some(deadline);
823            } else if let Some(p) = &pending {
824                if p.get().has_elapsed() {
825                    pending = None;
826
827                    handler.event(&ChangeStopArgs {
828                        cause: ChangeStopCause::DelayElapsed,
829                    });
830                }
831            }
832
833            c.update(updates);
834            handler.update();
835        }
836        _ => {}
837    })
838}
839
840/// Implements the selection toolbar.
841pub fn selection_toolbar_node(child: impl UiNode) -> impl UiNode {
842    use super::node::*;
843
844    let mut selection_range = None;
845    let mut popup_state = None::<ReadOnlyArcVar<PopupState>>;
846    match_node(child, move |c, op| {
847        let mut open = false;
848        let mut open_long_press = false;
849        let mut close = false;
850        match op {
851            UiNodeOp::Init => {
852                WIDGET.sub_var(&SELECTION_TOOLBAR_FN_VAR);
853            }
854            UiNodeOp::Deinit => {
855                close = true;
856            }
857            UiNodeOp::Event { update } => {
858                c.event(update);
859
860                let open_id = || {
861                    if let Some(popup_state) = &popup_state {
862                        if let PopupState::Open(id) = popup_state.get() {
863                            return Some(id);
864                        }
865                    }
866                    None
867                };
868
869                if let Some(args) = MOUSE_INPUT_EVENT.on(update) {
870                    if open_id().map(|id| !args.target.contains(id)).unwrap_or(false) {
871                        close = true;
872                    }
873                    if args.state == ButtonState::Released {
874                        open = true;
875                    }
876                } else if TOUCH_LONG_PRESS_EVENT.has(update) {
877                    open = true;
878                    open_long_press = true;
879                } else if KEY_INPUT_EVENT.has(update) {
880                    close = true;
881                } else if let Some(args) = FOCUS_CHANGED_EVENT.on(update) {
882                    if args.is_blur(WIDGET.id())
883                        && open_id()
884                            .map(|id| args.new_focus.as_ref().map(|p| !p.contains(id)).unwrap_or(true))
885                            .unwrap_or(false)
886                    {
887                        close = true;
888                    }
889                } else if let Some(args) = TOUCH_INPUT_EVENT.on(update) {
890                    if matches!(args.phase, TouchPhase::Start | TouchPhase::Move)
891                        && open_id().map(|id| !args.target.contains(id)).unwrap_or(false)
892                    {
893                        close = true;
894                    }
895                }
896
897                if popup_state.is_some() {
898                    let r_txt = TEXT.resolved();
899                    if selection_range != r_txt.caret.selection_range() {
900                        close = true;
901                    }
902                }
903            }
904            UiNodeOp::Update { .. } => {
905                if SELECTION_TOOLBAR_FN_VAR.is_new() {
906                    close = true;
907                }
908                if let Some(state) = popup_state.as_ref().and_then(|s| s.get_new()) {
909                    let is_open = !matches!(state, PopupState::Closed);
910                    let mut r_txt = TEXT.resolve();
911                    if r_txt.selection_toolbar_is_open != is_open {
912                        r_txt.selection_toolbar_is_open = is_open;
913                        WIDGET.layout().render();
914
915                        if !is_open {
916                            popup_state = None;
917                        }
918                    }
919                }
920            }
921            _ => {}
922        }
923        if close {
924            if let Some(state) = &popup_state.take() {
925                selection_range = None;
926                POPUP.close(state);
927                TEXT.resolve().selection_toolbar_is_open = false;
928                WIDGET.layout().render();
929            }
930        }
931        if open {
932            let r_txt = TEXT.resolved();
933
934            let range = r_txt.caret.selection_range();
935            if open_long_press || range.is_some() {
936                selection_range = range;
937
938                let toolbar_fn = SELECTION_TOOLBAR_FN_VAR.get();
939                if let Some(node) = toolbar_fn.call_checked(SelectionToolbarArgs {
940                    anchor_id: WIDGET.id(),
941                    is_touch: matches!(r_txt.selection_by, SelectionBy::Touch),
942                }) {
943                    let (node, _) = node.init_widget();
944
945                    let mut translate = PxVector::zero();
946                    let transform_key = FrameValueKey::new_unique();
947                    let node = match_widget(node, move |c, op| match op {
948                        UiNodeOp::Init => {
949                            c.init();
950                            c.with_context(WidgetUpdateMode::Bubble, || WIDGET.sub_var_layout(&SELECTION_TOOLBAR_ANCHOR_VAR));
951                        }
952                        UiNodeOp::Layout { wl, final_size } => {
953                            let r_txt = TEXT.resolved();
954
955                            let range = if open_long_press {
956                                Some(r_txt.caret.selection_range().unwrap_or_else(|| {
957                                    let i = r_txt.caret.index.unwrap_or(CaretIndex::ZERO);
958                                    i..i
959                                }))
960                            } else {
961                                r_txt.caret.selection_range()
962                            };
963
964                            if let Some(range) = range {
965                                let l_txt = TEXT.laidout();
966                                let r_txt = r_txt.segmented_text.text();
967
968                                let mut bounds = PxBox::new(PxPoint::splat(Px::MAX), PxPoint::splat(Px::MIN));
969                                for line_rect in l_txt.shaped_text.highlight_rects(range, r_txt) {
970                                    let line_box = line_rect.to_box2d();
971                                    bounds.min = bounds.min.min(line_box.min);
972                                    bounds.max = bounds.max.max(line_box.max);
973                                }
974                                let selection_bounds = bounds.to_rect();
975
976                                *final_size = c.layout(wl);
977
978                                let offset = SELECTION_TOOLBAR_ANCHOR_VAR.get();
979
980                                fn layout_offset(size: PxSize, point: Point) -> PxVector {
981                                    LAYOUT
982                                        .with_constraints(PxConstraints2d::new_exact_size(size), || point.layout())
983                                        .to_vector()
984                                }
985                                let place = layout_offset(selection_bounds.size, offset.place);
986                                let origin = layout_offset(*final_size, offset.origin);
987
988                                translate = selection_bounds.origin.to_vector() + place - origin;
989                            } else {
990                                // no selection, must be closing
991                                wl.collapse();
992                                *final_size = PxSize::zero();
993                            };
994                        }
995                        UiNodeOp::Render { frame } => {
996                            let l_txt = TEXT.laidout();
997                            let transform = l_txt.render_info.transform.then_translate(translate.cast());
998                            let transform = adjust_viewport_bound(transform, c);
999
1000                            frame.push_reference_frame(transform_key.into(), FrameValue::Value(transform), true, false, |frame| {
1001                                c.render(frame)
1002                            });
1003                        }
1004                        UiNodeOp::RenderUpdate { update } => {
1005                            let l_txt = TEXT.laidout();
1006                            let transform = l_txt.render_info.transform.then_translate(translate.cast());
1007                            let transform = adjust_viewport_bound(transform, c);
1008
1009                            update.with_transform(transform_key.update(transform, true), false, |update| c.render_update(update));
1010                        }
1011                        _ => {}
1012                    });
1013
1014                    // capture all context including LaidoutText, exclude text style properties.
1015                    let capture = ContextCapture::CaptureBlend {
1016                        filter: CaptureFilter::Exclude({
1017                            let mut exclude = ContextValueSet::new();
1018                            super::Text::context_vars_set(&mut exclude);
1019
1020                            let mut allow = ContextValueSet::new();
1021                            super::LangMix::<()>::context_vars_set(&mut allow);
1022                            exclude.remove_all(&allow);
1023                            exclude.remove(&SELECTION_TOOLBAR_ANCHOR_VAR);
1024
1025                            exclude
1026                        }),
1027                        over: false,
1028                    };
1029
1030                    let mut anchor_mode = AnchorMode::tooltip();
1031                    anchor_mode.transform = AnchorTransform::None;
1032                    let state = POPUP.open_config(node, anchor_mode, capture);
1033                    state.subscribe(UpdateOp::Update, WIDGET.id()).perm();
1034                    popup_state = Some(state);
1035                    drop(r_txt);
1036                    TEXT.resolve().selection_toolbar_is_open = true;
1037                    WIDGET.layout().render();
1038                }
1039            };
1040        }
1041    })
1042}
1043fn adjust_viewport_bound(transform: PxTransform, widget: &mut impl UiNode) -> PxTransform {
1044    let window_bounds = WINDOW.vars().actual_size_px().get();
1045    let wgt_bounds = PxBox::from(
1046        widget
1047            .with_context(WidgetUpdateMode::Ignore, || WIDGET.bounds().outer_size())
1048            .unwrap_or_else(PxSize::zero),
1049    );
1050    let wgt_bounds = transform.outer_transformed(wgt_bounds).unwrap_or_default();
1051
1052    let x_underflow = -wgt_bounds.min.x.min(Px(0));
1053    let x_overflow = (wgt_bounds.max.x - window_bounds.width).max(Px(0));
1054    let y_underflow = -wgt_bounds.min.y.min(Px(0));
1055    let y_overflow = (wgt_bounds.max.y - window_bounds.height).max(Px(0));
1056
1057    let x = x_underflow - x_overflow;
1058    let y = y_underflow - y_overflow;
1059
1060    let correction = PxVector::new(x, y);
1061
1062    transform.then_translate(correction.cast())
1063}