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