Skip to main content

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    /// Set the `underlines` value.
295    ///
296    /// Note that the value is already updated automatically on layout, this method is for custom properties that
297    /// override the normal underlines.
298    pub fn set_underlines(&self, underlines: Vec<(PxPoint, Px)>) {
299        let mut l = self.layout();
300        if l.underlines != underlines {
301            WIDGET.render();
302        }
303        l.underlines = underlines;
304    }
305
306    pub(crate) fn resolve(&self) -> RwLockWriteGuardOwned<ResolvedText> {
307        RESOLVED_TEXT.write()
308    }
309
310    fn layout(&self) -> RwLockWriteGuardOwned<LaidoutText> {
311        LAIDOUT_TEXT.write()
312    }
313
314    pub(crate) fn take_rich_selection_started_by_alt(&self) -> bool {
315        std::mem::take(&mut RICH_TEXT.write().selection_started_by_alt)
316    }
317
318    pub(crate) fn flag_rich_selection_started_by_alt(&self) {
319        RICH_TEXT.write().selection_started_by_alt = true;
320    }
321}
322
323/// Defines the source of the current selection.
324///
325/// See [`ResolvedText::selection_by`] for more details.
326#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, bytemuck::NoUninit)]
327#[repr(u8)]
328pub enum SelectionBy {
329    /// Command or other programmatic selection.
330    Command = 0,
331    /// Key press.
332    Keyboard = 1,
333    /// Mouse drag.
334    Mouse = 2,
335    /// Touch drag.
336    Touch = 3,
337}
338impl SelectionBy {
339    /// Returns `true` if the interactive carets must be used for the current selection given the interactive caret mode.
340    pub fn matches_interactive_mode(self, mode: InteractiveCaretMode) -> bool {
341        match mode {
342            InteractiveCaretMode::TouchOnly => matches!(self, SelectionBy::Touch),
343            InteractiveCaretMode::Enabled => true,
344            InteractiveCaretMode::Disabled => false,
345        }
346    }
347}
348
349/// Represents the resolved fonts and the transformed, white space corrected and segmented text.
350///
351/// Use [`TEXT`] to get.
352#[non_exhaustive]
353pub struct ResolvedText {
354    /// The text source variable.
355    pub txt: Var<Txt>,
356    /// IME text edit that is not committed yet. Only the text in the segmented and shaped text is edited,
357    /// the text variable is not updated yet and undo is not tracking these changes.
358    pub ime_preview: Option<ImePreview>,
359
360    /// Text transformed, white space corrected and segmented.
361    pub segmented_text: SegmentedText,
362    /// Queried font faces.
363    pub faces: FontFaceList,
364    /// Font synthesis allowed by the text context and required to render the best font match.
365    pub synthesis: FontSynthesis,
366
367    /// Layout that needs to be recomputed as identified by the text resolver node.
368    ///
369    /// This is added to the layout invalidation by the layout node itself. When set a layout must
370    /// be requested for the widget.
371    pub pending_layout: PendingLayout,
372
373    /// Text modification is scheduled, caret info will only be valid after update.
374    pub pending_edit: bool,
375
376    /// Caret index and animation.
377    pub caret: CaretInfo,
378
379    /// Source of the current selection.
380    pub selection_by: SelectionBy,
381
382    /// If the selection toolbar is open.
383    pub selection_toolbar_is_open: bool,
384}
385impl fmt::Debug for ResolvedText {
386    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
387        f.debug_struct("ResolvedText")
388            .field("segmented_text", &self.segmented_text)
389            .field("faces", &self.faces)
390            .field("synthesis", &self.synthesis)
391            .field("pending_layout", &self.pending_layout)
392            .field("pending_edit", &self.pending_edit)
393            .field("caret", &self.caret)
394            .field("selection_by", &self.selection_by)
395            .field("selection_toolbar_is_open", &self.selection_toolbar_is_open)
396            .finish_non_exhaustive()
397    }
398}
399impl ResolvedText {
400    fn no_context() -> Self {
401        panic!("no `ResolvedText` in context, only available inside `resolve_text`")
402    }
403}
404
405/// Info about the last text render or render update.
406#[derive(Debug, Clone)]
407#[non_exhaustive]
408pub struct RenderInfo {
409    /// Render transform of the text, in the window space.
410    pub transform: PxTransform,
411    /// Render scale factor of the text.
412    pub scale_factor: Factor,
413}
414impl Default for RenderInfo {
415    /// Identify, 1.fct()
416    fn default() -> Self {
417        Self {
418            transform: PxTransform::identity(),
419            scale_factor: 1.fct(),
420        }
421    }
422}
423
424/// Represents the laidout text.
425///
426/// Use [`TEXT`] to get.
427#[derive(Debug)]
428#[non_exhaustive]
429pub struct LaidoutText {
430    /// Sized [`faces`].
431    ///
432    /// [`faces`]: ResolvedText::faces
433    pub fonts: FontList,
434
435    /// Layout text.
436    pub shaped_text: ShapedText,
437
438    /// Shaped text overflow info.
439    pub overflow: Option<TextOverflowInfo>,
440
441    /// Shaped text used as suffix when `shaped_text` overflows.
442    pub overflow_suffix: Option<ShapedText>,
443
444    /// Version updated every time the `shaped_text` is reshaped.
445    pub shaped_text_version: u32,
446
447    /// List of overline segments, defining origin and width of each line.
448    ///
449    /// Note that overlines are only computed if the `overline_thickness` is more than `0`.
450    ///
451    /// Default overlines are rendered by [`render_overlines`].
452    pub overlines: Vec<(PxPoint, Px)>,
453
454    /// Computed [`OVERLINE_THICKNESS_VAR`].
455    pub overline_thickness: Px,
456
457    /// List of strikethrough segments, defining origin and width of each line.
458    ///
459    /// Note that strikethroughs are only computed if the `strikethrough_thickness` is more than `0`.
460    ///
461    /// Default overlines are rendered by [`render_strikethroughs`].
462    pub strikethroughs: Vec<(PxPoint, Px)>,
463    /// Computed [`STRIKETHROUGH_THICKNESS_VAR`].
464    pub strikethrough_thickness: Px,
465
466    /// List of underline segments, defining origin and width of each line.
467    ///
468    /// Note that underlines are only computed if the `underline_thickness` is more than `0`. These
469    /// underlines never cover the IME preview text range.
470    ///
471    /// Default underlines are rendered by [`render_underlines`].
472    pub underlines: Vec<(PxPoint, Px)>,
473    /// Computed [`UNDERLINE_THICKNESS_VAR`].
474    pub underline_thickness: Px,
475
476    /// List of underline segments for IME preview text, defining origin and width of each line.
477    ///
478    /// Note that underlines are only computed if the `ime_underline_thickness` is more than `0`.
479    ///
480    /// Default underlines are rendered by [`render_underlines`].
481    pub ime_underlines: Vec<(PxPoint, Px)>,
482    /// Computed [`IME_UNDERLINE_THICKNESS_VAR`].
483    pub ime_underline_thickness: Px,
484
485    /// Top-middle offset of the caret index in the shaped text.
486    pub caret_origin: Option<PxPoint>,
487
488    /// Top-middle offset of the caret selection_index in the shaped text.
489    pub caret_selection_origin: Option<PxPoint>,
490
491    /// The x offset used when pressing up or down.
492    pub caret_retained_x: Px,
493
494    /// Info about the last text render or render update.
495    pub render_info: RenderInfo,
496
497    /// Latest layout viewport.
498    pub viewport: PxSize,
499}
500impl LaidoutText {
501    fn no_context() -> Self {
502        panic!("no `LaidoutText` in context, only available inside `layout_text`")
503    }
504}
505
506/// Represents the rich text context.
507///
508/// Use [`TEXT`] to get.
509#[non_exhaustive]
510pub struct RichText {
511    /// Widget that defines the rich text context.
512    pub root_id: WidgetId,
513
514    /// Widgets that define the caret and selection indexes.
515    pub caret: RichCaretInfo,
516
517    selection_started_by_alt: bool,
518}
519impl RichText {
520    fn no_context() -> Self {
521        panic!("no `RichText` in context, only available inside `rich_text`")
522    }
523}
524
525context_local! {
526    /// Represents the contextual [`RichText`] setup by the [`rich_text`] property.
527    static RICH_TEXT: RwLock<RichText> = RwLock::new(RichText::no_context());
528    /// Represents the contextual [`ResolvedText`] setup by the [`resolve_text`] node.
529    static RESOLVED_TEXT: RwLock<ResolvedText> = RwLock::new(ResolvedText::no_context());
530    /// Represents the contextual [`LaidoutText`] setup by the [`layout_text`] node.
531    static LAIDOUT_TEXT: RwLock<LaidoutText> = RwLock::new(LaidoutText::no_context());
532}
533
534bitflags! {
535    /// Text layout parts that need rebuild.
536    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
537    pub struct PendingLayout: u8 {
538        /// Underline size and position.
539        const UNDERLINE = 0b0000_0001;
540        /// Strikethrough size and position.
541        const STRIKETHROUGH = 0b0000_0010;
542        /// Overline size and position.
543        const OVERLINE = 0b0000_0100;
544        /// Caret origin.
545        const CARET = 0b0000_1000;
546        /// Overflow.
547        const OVERFLOW = 0b0001_0000;
548        /// Text lines position, retains line glyphs but reposition for new align and outer box.
549        const RESHAPE_LINES = 0b0111_1111;
550        /// Full reshape, re-compute all glyphs.
551        const RESHAPE = 0b1111_1111;
552    }
553}
554
555/// Create a node that is sized one text line height by `width`.
556///
557/// This node can be used to reserve space for a full text in lazy initing contexts.
558///
559/// The contextual variables affect the layout size.
560pub fn line_placeholder(width: impl IntoVar<Length>) -> UiNode {
561    let child = layout_text(FillUiNode);
562    let child = resolve_text(child, " ");
563    zng_wgt_size_offset::width(child, width)
564}
565
566pub(super) fn get_caret_index(child: impl IntoUiNode, index: impl IntoVar<Option<CaretIndex>>) -> UiNode {
567    let index = index.into_var();
568    match_node(child, move |c, op| {
569        let mut u = false;
570        match op {
571            UiNodeOp::Init => {
572                c.init();
573                index.set(TEXT.resolved().caret.index);
574            }
575            UiNodeOp::Deinit => {
576                index.set(None);
577            }
578            UiNodeOp::Update { updates } => {
579                c.update(updates);
580                u = true;
581            }
582            _ => {}
583        }
584        if u {
585            let t = TEXT.resolved();
586            let idx = t.caret.index;
587            if !t.pending_edit && index.get() != idx {
588                index.set(idx);
589            }
590        }
591    })
592}
593
594pub(super) fn get_caret_status(child: impl IntoUiNode, status: impl IntoVar<CaretStatus>) -> UiNode {
595    let status = status.into_var();
596    match_node(child, move |c, op| {
597        let mut u = false;
598        match op {
599            UiNodeOp::Init => {
600                c.init();
601                let t = TEXT.resolved();
602                status.set(match t.caret.index {
603                    None => CaretStatus::none(),
604                    Some(i) => CaretStatus::new(i.index, &t.segmented_text),
605                });
606            }
607            UiNodeOp::Deinit => {
608                status.set(CaretStatus::none());
609            }
610            UiNodeOp::Update { updates } => {
611                c.update(updates);
612                u = true;
613            }
614            _ => {}
615        }
616        if u {
617            let t = TEXT.resolved();
618            let idx = t.caret.index;
619            if !t.pending_edit && status.get().index() != idx.map(|ci| ci.index) {
620                status.set(match idx {
621                    None => CaretStatus::none(),
622                    Some(i) => CaretStatus::new(i.index, &t.segmented_text),
623                });
624            }
625        }
626    })
627}
628
629pub(super) fn get_lines_len(child: impl IntoUiNode, len: impl IntoVar<usize>) -> UiNode {
630    let len = len.into_var();
631    match_node(child, move |c, op| match op {
632        UiNodeOp::Deinit => {
633            len.set(0usize);
634        }
635        UiNodeOp::Layout { wl, final_size } => {
636            *final_size = c.layout(wl);
637            let t = TEXT.laidout();
638            let l = t.shaped_text.lines_len();
639            if l != len.get() {
640                len.set(t.shaped_text.lines_len());
641            }
642        }
643        _ => {}
644    })
645}
646
647pub(super) fn get_lines_wrap_count(child: impl IntoUiNode, lines: impl IntoVar<super::LinesWrapCount>) -> UiNode {
648    let lines = lines.into_var();
649    let mut version = 0;
650    match_node(child, move |c, op| match op {
651        UiNodeOp::Deinit => {
652            lines.set(super::LinesWrapCount::NoWrap(0));
653        }
654        UiNodeOp::Layout { wl, final_size } => {
655            *final_size = c.layout(wl);
656            let t = TEXT.laidout();
657            if t.shaped_text_version != version {
658                version = t.shaped_text_version;
659                if let Some(update) = lines.with(|l| lines_wrap_count(l, &t.shaped_text)) {
660                    lines.set(update);
661                }
662            }
663        }
664        _ => {}
665    })
666}
667// Returns `Some(_)` if the current wrap count changed from `prev`. Only allocates if new count has wrapped lines.
668fn lines_wrap_count(prev: &super::LinesWrapCount, txt: &ShapedText) -> Option<super::LinesWrapCount> {
669    match prev {
670        super::LinesWrapCount::NoWrap(len) => {
671            let mut counter = lines_wrap_counter(txt);
672            let mut l = 0;
673            for c in &mut counter {
674                if c != 1 {
675                    // at least one line wraps now
676                    let mut wrap = vec![1; l];
677                    wrap.push(c);
678                    wrap.extend(&mut counter);
679                    return Some(super::LinesWrapCount::Wrap(wrap));
680                }
681                l += 1;
682            }
683            if l != *len {
684                // no line wraps, but changed line count.
685                Some(super::LinesWrapCount::NoWrap(l))
686            } else {
687                None
688            }
689        }
690        super::LinesWrapCount::Wrap(counts) => {
691            // find `counts[i]` that diverges from counts, OR
692            // find if all new counts is now NoWrap
693            let mut prev_counts = counts.iter();
694            let mut new_counts = lines_wrap_counter(txt);
695            let mut eq_l = 0;
696            let mut eq_wrap = false;
697            for c in &mut new_counts {
698                if prev_counts.next() == Some(&c) {
699                    eq_l += 1;
700                    eq_wrap |= c != 1;
701                } else if eq_wrap || c != 1 {
702                    // not eq, and already found a wrap line
703                    let mut wrap = counts[..eq_l].to_vec();
704                    wrap.push(c);
705                    wrap.extend(&mut new_counts);
706                    return Some(super::LinesWrapCount::Wrap(wrap));
707                } else {
708                    // not eq, but maybe no wrap
709                    let mut l = eq_l + 1; // +1 is +c
710                    for c in &mut new_counts {
711                        if c != 1 {
712                            // nope, found a line wrap
713                            let mut wrap = vec![1; l];
714                            wrap.push(c);
715                            wrap.extend(&mut new_counts);
716                            return Some(super::LinesWrapCount::Wrap(wrap));
717                        }
718                        l += 1;
719                    }
720                    // changed to no wrap
721                    return Some(super::LinesWrapCount::NoWrap(l));
722                }
723            }
724            if prev_counts.next().is_some() {
725                Some(super::LinesWrapCount::Wrap(counts[..eq_l].to_vec()))
726            } else {
727                None
728            }
729        }
730    }
731}
732fn lines_wrap_counter(txt: &ShapedText) -> impl Iterator<Item = u32> + '_ {
733    struct Counter<I> {
734        lines: I,
735        count: u32,
736    }
737    impl<'a, I: Iterator<Item = ShapedLine<'a>>> Iterator for Counter<I> {
738        type Item = u32;
739
740        fn next(&mut self) -> Option<u32> {
741            loop {
742                let line = self.lines.next()?;
743                if line.ended_by_wrap() {
744                    self.count += 1;
745                    continue;
746                }
747
748                let c = self.count;
749                self.count = 1;
750
751                return Some(c);
752            }
753        }
754    }
755    Counter {
756        lines: txt.lines(),
757        count: 1,
758    }
759}
760
761pub(super) fn parse_text<T>(child: impl IntoUiNode, value: impl IntoVar<T>) -> UiNode
762where
763    T: super::TxtParseValue,
764{
765    let value = value.into_var();
766
767    let error = var(Txt::from_static(""));
768    let mut _error_note = DataNoteHandle::dummy();
769
770    #[derive(Clone, Copy, bytemuck::NoUninit)]
771    #[repr(u8)]
772    enum State {
773        Sync,
774        Requested,
775        Pending,
776    }
777    let state = Arc::new(Atomic::new(State::Sync));
778
779    match_node(child, move |_, op| match op {
780        UiNodeOp::Init => {
781            let ctx = TEXT.resolved();
782
783            // initial T -> Txt sync
784            ctx.txt.set_from_map(&value, |val| val.to_txt());
785
786            // bind `TXT_PARSE_LIVE_VAR` <-> `value` using `bind_filter_map_bidi`:
787            // - in case of parse error, it is set in `error` variable, that is held by the binding.
788            // - on error update the DATA note is updated.
789            // - in case parse is not live, ignores updates (Txt -> None), sets `state` to `Pending`.
790            // - in case of Pending and `PARSE_CMD` state is set to `Requested` and `TXT_PARSE_LIVE_VAR.update()`.
791            // - the pending state is also tracked in `TXT_PARSE_PENDING_VAR` and the `PARSE_CMD` handle.
792
793            let live = TXT_PARSE_LIVE_VAR.current_context();
794            let is_pending = TXT_PARSE_PENDING_VAR.current_context();
795            let cmd_handle = Arc::new(super::cmd::PARSE_CMD.scoped(WIDGET.id()).subscribe(false));
796
797            let binding = ctx.txt.bind_filter_map_bidi(
798                &value,
799                clmv!(state, error, is_pending, cmd_handle, |txt| {
800                    if live.get() || matches!(state.load(Ordering::Relaxed), State::Requested) {
801                        // can try parse
802
803                        if !matches!(state.swap(State::Sync, Ordering::Relaxed), State::Sync) {
804                            // exit pending state, even if it parse fails
805                            is_pending.set(false);
806                            cmd_handle.enabled().set(false);
807                        }
808
809                        // try parse
810                        match T::from_txt(txt) {
811                            Ok(val) => {
812                                error.set(Txt::from_static(""));
813                                Some(val)
814                            }
815                            Err(e) => {
816                                error.set(e);
817                                None
818                            }
819                        }
820                    } else {
821                        // cannot try parse
822
823                        if !matches!(state.swap(State::Pending, Ordering::Relaxed), State::Pending) {
824                            // enter pending state
825                            is_pending.set(true);
826                            cmd_handle.enabled().set(true);
827                        }
828
829                        // does not update the value
830                        None
831                    }
832                }),
833                clmv!(state, error, |val| {
834                    // value updated externally, exit error, exit pending.
835
836                    error.set(Txt::from_static(""));
837
838                    if !matches!(state.swap(State::Sync, Ordering::Relaxed), State::Sync) {
839                        is_pending.set(false);
840                        cmd_handle.enabled().set(false);
841                    }
842
843                    Some(val.to_txt())
844                }),
845            );
846
847            // cmd_handle is held by the binding
848
849            WIDGET.sub_var(&TXT_PARSE_LIVE_VAR).sub_var(&error).push_var_handles(binding);
850        }
851        UiNodeOp::Deinit => {
852            _error_note = DataNoteHandle::dummy();
853        }
854        UiNodeOp::Update { .. } => {
855            if matches!(state.load(Ordering::Relaxed), State::Pending) {
856                super::cmd::PARSE_CMD.scoped(WIDGET.id()).each_update(true, false, |args| {
857                    // requested parse and parse is pending
858
859                    state.store(State::Requested, Ordering::Relaxed);
860                    TEXT.resolved().txt.update();
861                    args.propagation.stop();
862                });
863            }
864
865            if let Some(true) = TXT_PARSE_LIVE_VAR.get_new()
866                && matches!(state.load(Ordering::Relaxed), State::Pending)
867            {
868                // enabled live parse and parse is pending
869
870                TEXT.resolved().txt.update();
871            }
872
873            if let Some(error) = error.get_new() {
874                // remove or replace the error
875
876                _error_note = if error.is_empty() {
877                    DataNoteHandle::dummy()
878                } else {
879                    DATA.invalidate(error)
880                };
881            }
882        }
883        _ => {}
884    })
885}
886
887pub(super) fn on_change_stop(child: impl IntoUiNode, handler: Handler<ChangeStopArgs>) -> UiNode {
888    let mut handler = handler.into_wgt_runner();
889    let mut pending = None;
890    match_node(child, move |c, op| match op {
891        UiNodeOp::Deinit => {
892            handler.deinit();
893        }
894        UiNodeOp::Update { updates } => {
895            if TEXT.resolved().txt.is_new() {
896                let deadline = TIMERS.deadline(CHANGE_STOP_DELAY_VAR.get());
897                deadline.subscribe(UpdateOp::Update, WIDGET.id()).perm();
898                pending = Some(deadline);
899            } else if let Some(p) = &pending
900                && p.get().has_elapsed()
901            {
902                pending = None;
903
904                handler.event(&ChangeStopArgs {
905                    cause: ChangeStopCause::DelayElapsed,
906                });
907            }
908
909            c.update(updates);
910            handler.update();
911
912            if pending.is_none() {
913                return;
914            }
915
916            KEY_INPUT_EVENT.each_update(false, |args| {
917                if let KeyState::Pressed = args.state
918                    && let Key::Enter = &args.key
919                    && !ACCEPTS_ENTER_VAR.get()
920                {
921                    pending = None;
922                    handler.event(&ChangeStopArgs {
923                        cause: ChangeStopCause::Enter,
924                    });
925                }
926            });
927            FOCUS_CHANGED_EVENT.each_update(true, |args| {
928                let target = WIDGET.id();
929                if args.is_blur(target) {
930                    pending = None;
931                    handler.event(&ChangeStopArgs {
932                        cause: ChangeStopCause::Blur,
933                    });
934                }
935            });
936        }
937        _ => {}
938    })
939}
940
941/// Implements the selection toolbar.
942pub fn selection_toolbar_node(child: impl IntoUiNode) -> UiNode {
943    use super::node::*;
944
945    let mut selection_range = None;
946    let mut popup_state = None::<Var<PopupState>>;
947    match_node(child, move |c, op| {
948        let mut open = false;
949        let mut open_long_press = false;
950        let mut close = false;
951        match op {
952            UiNodeOp::Init => {
953                WIDGET.sub_var(&SELECTION_TOOLBAR_FN_VAR);
954            }
955            UiNodeOp::Deinit => {
956                close = true;
957            }
958            UiNodeOp::Update { updates } => {
959                if SELECTION_TOOLBAR_FN_VAR.is_new() {
960                    close = true;
961                }
962                if let Some(state) = popup_state.as_ref().and_then(|s| s.get_new()) {
963                    let is_open = !matches!(state, PopupState::Closed);
964                    let mut r_txt = TEXT.resolve();
965                    if r_txt.selection_toolbar_is_open != is_open {
966                        r_txt.selection_toolbar_is_open = is_open;
967                        WIDGET.layout().render();
968
969                        if !is_open {
970                            popup_state = None;
971                        }
972                    }
973                }
974
975                c.update(updates);
976
977                let open_id = || {
978                    if let Some(popup_state) = &popup_state
979                        && let PopupState::Open(id) = popup_state.get()
980                    {
981                        return Some(id);
982                    }
983                    None
984                };
985
986                MOUSE_INPUT_EVENT.each_update(true, |args| {
987                    if open_id().map(|id| !args.target.contains(id)).unwrap_or(false) {
988                        close = true;
989                    }
990                    if args.state == ButtonState::Released {
991                        open = true;
992                    }
993                });
994
995                if TOUCH_LONG_PRESS_EVENT.has_update(true) {
996                    open = true;
997                    open_long_press = true;
998                }
999                if KEY_INPUT_EVENT.has_update(true) {
1000                    close = true;
1001                }
1002
1003                FOCUS_CHANGED_EVENT.each_update(true, |args| {
1004                    if args.is_blur(WIDGET.id())
1005                        && open_id()
1006                            .map(|id| args.new_focus.as_ref().map(|p| !p.contains(id)).unwrap_or(true))
1007                            .unwrap_or(false)
1008                    {
1009                        close = true;
1010                    }
1011                });
1012                TOUCH_INPUT_EVENT.each_update(true, |args| {
1013                    if matches!(args.phase, TouchPhase::Start | TouchPhase::Move)
1014                        && open_id().map(|id| !args.target.contains(id)).unwrap_or(false)
1015                    {
1016                        close = true;
1017                    }
1018                });
1019
1020                if popup_state.is_some() {
1021                    let r_txt = TEXT.resolved();
1022                    if selection_range != r_txt.caret.selection_range() {
1023                        close = true;
1024                    }
1025                }
1026            }
1027            _ => {}
1028        }
1029        if close && let Some(state) = &popup_state.take() {
1030            selection_range = None;
1031            POPUP.close(state);
1032            TEXT.resolve().selection_toolbar_is_open = false;
1033            WIDGET.layout().render();
1034        }
1035        if open {
1036            let r_txt = TEXT.resolved();
1037
1038            let range = r_txt.caret.selection_range();
1039            if open_long_press || range.is_some() {
1040                selection_range = range;
1041
1042                let toolbar_fn = SELECTION_TOOLBAR_FN_VAR.get();
1043                if let Some(node) = toolbar_fn.call_checked(SelectionToolbarArgs {
1044                    anchor_id: WIDGET.id(),
1045                    is_touch: matches!(r_txt.selection_by, SelectionBy::Touch),
1046                }) {
1047                    let (node, _) = node.init_widget();
1048
1049                    let mut translate = PxVector::zero();
1050                    let transform_key = FrameValueKey::new_unique();
1051                    let node = match_widget(node, move |c, op| match op {
1052                        UiNodeOp::Init => {
1053                            c.init();
1054                            if let Some(mut wgt) = c.node().as_widget() {
1055                                wgt.with_context(WidgetUpdateMode::Bubble, || WIDGET.sub_var_layout(&SELECTION_TOOLBAR_ANCHOR_VAR));
1056                            }
1057                        }
1058                        UiNodeOp::Layout { wl, final_size } => {
1059                            let r_txt = TEXT.resolved();
1060
1061                            let range = if open_long_press {
1062                                Some(r_txt.caret.selection_range().unwrap_or_else(|| {
1063                                    let i = r_txt.caret.index.unwrap_or(CaretIndex::ZERO);
1064                                    i..i
1065                                }))
1066                            } else {
1067                                r_txt.caret.selection_range()
1068                            };
1069
1070                            if let Some(range) = range {
1071                                let l_txt = TEXT.laidout();
1072                                let r_txt = r_txt.segmented_text.text();
1073
1074                                let mut bounds = PxBox::new(PxPoint::splat(Px::MAX), PxPoint::splat(Px::MIN));
1075                                for line_rect in l_txt.shaped_text.highlight_rects(range, r_txt) {
1076                                    let line_box = line_rect.to_box2d();
1077                                    bounds.min = bounds.min.min(line_box.min);
1078                                    bounds.max = bounds.max.max(line_box.max);
1079                                }
1080                                let selection_bounds = bounds.to_rect();
1081
1082                                *final_size = c.layout(wl);
1083
1084                                let offset = SELECTION_TOOLBAR_ANCHOR_VAR.get();
1085
1086                                fn layout_offset(size: PxSize, point: Point) -> PxVector {
1087                                    LAYOUT
1088                                        .with_constraints(PxConstraints2d::new_exact_size(size), || point.layout())
1089                                        .to_vector()
1090                                }
1091                                let place = layout_offset(selection_bounds.size, offset.place);
1092                                let origin = layout_offset(*final_size, offset.origin);
1093
1094                                translate = selection_bounds.origin.to_vector() + place - origin;
1095                            } else {
1096                                // no selection, must be closing
1097                                wl.collapse();
1098                                *final_size = PxSize::zero();
1099                            };
1100                        }
1101                        UiNodeOp::Render { frame } => {
1102                            let l_txt = TEXT.laidout();
1103                            let transform = l_txt.render_info.transform.then_translate(translate.cast());
1104                            let transform = adjust_viewport_bound(transform, c.node());
1105
1106                            frame.push_reference_frame(transform_key.into(), FrameValue::Value(transform), true, false, |frame| {
1107                                c.render(frame)
1108                            });
1109                        }
1110                        UiNodeOp::RenderUpdate { update } => {
1111                            let l_txt = TEXT.laidout();
1112                            let transform = l_txt.render_info.transform.then_translate(translate.cast());
1113                            let transform = adjust_viewport_bound(transform, c.node());
1114
1115                            update.with_transform(transform_key.update(transform, true), false, |update| c.render_update(update));
1116                        }
1117                        _ => {}
1118                    });
1119
1120                    // capture all context including LaidoutText, exclude text style properties.
1121                    let capture = ContextCapture::CaptureBlend {
1122                        filter: CaptureFilter::Exclude({
1123                            let mut exclude = ContextValueSet::new();
1124                            super::Text::context_vars_set(&mut exclude);
1125
1126                            let mut allow = ContextValueSet::new();
1127                            super::LangMix::<()>::context_vars_set(&mut allow);
1128                            exclude.remove_all(&allow);
1129                            exclude.remove(&SELECTION_TOOLBAR_ANCHOR_VAR);
1130
1131                            exclude
1132                        }),
1133                        over: false,
1134                    };
1135
1136                    let mut anchor_mode = AnchorMode::tooltip();
1137                    anchor_mode.transform = AnchorTransform::None;
1138                    let state = POPUP.open_config(node, anchor_mode, capture);
1139                    state.subscribe(UpdateOp::Update, WIDGET.id()).perm();
1140                    popup_state = Some(state);
1141                    drop(r_txt);
1142                    TEXT.resolve().selection_toolbar_is_open = true;
1143                    WIDGET.layout().render();
1144                }
1145            };
1146        }
1147    })
1148}
1149fn adjust_viewport_bound(transform: PxTransform, widget: &mut UiNode) -> PxTransform {
1150    let window_bounds = WINDOW.vars().actual_size_px().get();
1151    let wgt_bounds = PxBox::from(match widget.as_widget() {
1152        Some(mut w) => w.with_context(WidgetUpdateMode::Ignore, || WIDGET.bounds().outer_size()),
1153        None => PxSize::zero(),
1154    });
1155    let wgt_bounds = transform.outer_transformed(wgt_bounds).unwrap_or_default();
1156
1157    let x_underflow = -wgt_bounds.min.x.min(Px(0));
1158    let x_overflow = (wgt_bounds.max.x - window_bounds.width).max(Px(0));
1159    let y_underflow = -wgt_bounds.min.y.min(Px(0));
1160    let y_overflow = (wgt_bounds.max.y - window_bounds.height).max(Px(0));
1161
1162    let x = x_underflow - x_overflow;
1163    let y = y_underflow - y_overflow;
1164
1165    let correction = PxVector::new(x, y);
1166
1167    transform.then_translate(correction.cast())
1168}