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}
512
513context_local! {
514    /// Represents the contextual [`RichText`] setup by the [`rich_text`] property.
515    static RICH_TEXT: RwLock<RichText> = RwLock::new(RichText::no_context());
516    /// Represents the contextual [`ResolvedText`] setup by the [`resolve_text`] node.
517    static RESOLVED_TEXT: RwLock<ResolvedText> = RwLock::new(ResolvedText::no_context());
518    /// Represents the contextual [`LaidoutText`] setup by the [`layout_text`] node.
519    static LAIDOUT_TEXT: RwLock<LaidoutText> = RwLock::new(LaidoutText::no_context());
520}
521
522bitflags! {
523    /// Text layout parts that need rebuild.
524    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
525    pub struct PendingLayout: u8 {
526        /// Underline size and position.
527        const UNDERLINE = 0b0000_0001;
528        /// Strikethrough size and position.
529        const STRIKETHROUGH = 0b0000_0010;
530        /// Overline size and position.
531        const OVERLINE = 0b0000_0100;
532        /// Caret origin.
533        const CARET = 0b0000_1000;
534        /// Overflow.
535        const OVERFLOW = 0b0001_0000;
536        /// Text lines position, retains line glyphs but reposition for new align and outer box.
537        const RESHAPE_LINES = 0b0111_1111;
538        /// Full reshape, re-compute all glyphs.
539        const RESHAPE = 0b1111_1111;
540    }
541}
542
543/// Create a node that is sized one text line height by `width`.
544///
545/// This node can be used to reserve space for a full text in lazy initing contexts.
546///
547/// The contextual variables affect the layout size.
548pub fn line_placeholder(width: impl IntoVar<Length>) -> UiNode {
549    let child = layout_text(FillUiNode);
550    let child = resolve_text(child, " ");
551    zng_wgt_size_offset::width(child, width)
552}
553
554pub(super) fn get_caret_index(child: impl IntoUiNode, index: impl IntoVar<Option<CaretIndex>>) -> UiNode {
555    let index = index.into_var();
556    match_node(child, move |c, op| {
557        let mut u = false;
558        match op {
559            UiNodeOp::Init => {
560                c.init();
561                index.set(TEXT.resolved().caret.index);
562            }
563            UiNodeOp::Deinit => {
564                index.set(None);
565            }
566            UiNodeOp::Update { updates } => {
567                c.update(updates);
568                u = true;
569            }
570            _ => {}
571        }
572        if u {
573            let t = TEXT.resolved();
574            let idx = t.caret.index;
575            if !t.pending_edit && index.get() != idx {
576                index.set(idx);
577            }
578        }
579    })
580}
581
582pub(super) fn get_caret_status(child: impl IntoUiNode, status: impl IntoVar<CaretStatus>) -> UiNode {
583    let status = status.into_var();
584    match_node(child, move |c, op| {
585        let mut u = false;
586        match op {
587            UiNodeOp::Init => {
588                c.init();
589                let t = TEXT.resolved();
590                status.set(match t.caret.index {
591                    None => CaretStatus::none(),
592                    Some(i) => CaretStatus::new(i.index, &t.segmented_text),
593                });
594            }
595            UiNodeOp::Deinit => {
596                status.set(CaretStatus::none());
597            }
598            UiNodeOp::Update { updates } => {
599                c.update(updates);
600                u = true;
601            }
602            _ => {}
603        }
604        if u {
605            let t = TEXT.resolved();
606            let idx = t.caret.index;
607            if !t.pending_edit && status.get().index() != idx.map(|ci| ci.index) {
608                status.set(match idx {
609                    None => CaretStatus::none(),
610                    Some(i) => CaretStatus::new(i.index, &t.segmented_text),
611                });
612            }
613        }
614    })
615}
616
617pub(super) fn get_lines_len(child: impl IntoUiNode, len: impl IntoVar<usize>) -> UiNode {
618    let len = len.into_var();
619    match_node(child, move |c, op| match op {
620        UiNodeOp::Deinit => {
621            len.set(0usize);
622        }
623        UiNodeOp::Layout { wl, final_size } => {
624            *final_size = c.layout(wl);
625            let t = TEXT.laidout();
626            let l = t.shaped_text.lines_len();
627            if l != len.get() {
628                len.set(t.shaped_text.lines_len());
629            }
630        }
631        _ => {}
632    })
633}
634
635pub(super) fn get_lines_wrap_count(child: impl IntoUiNode, lines: impl IntoVar<super::LinesWrapCount>) -> UiNode {
636    let lines = lines.into_var();
637    let mut version = 0;
638    match_node(child, move |c, op| match op {
639        UiNodeOp::Deinit => {
640            lines.set(super::LinesWrapCount::NoWrap(0));
641        }
642        UiNodeOp::Layout { wl, final_size } => {
643            *final_size = c.layout(wl);
644            let t = TEXT.laidout();
645            if t.shaped_text_version != version {
646                version = t.shaped_text_version;
647                if let Some(update) = lines.with(|l| lines_wrap_count(l, &t.shaped_text)) {
648                    lines.set(update);
649                }
650            }
651        }
652        _ => {}
653    })
654}
655// Returns `Some(_)` if the current wrap count changed from `prev`. Only allocates if new count has wrapped lines.
656fn lines_wrap_count(prev: &super::LinesWrapCount, txt: &ShapedText) -> Option<super::LinesWrapCount> {
657    match prev {
658        super::LinesWrapCount::NoWrap(len) => {
659            let mut counter = lines_wrap_counter(txt);
660            let mut l = 0;
661            for c in &mut counter {
662                if c != 1 {
663                    // at least one line wraps now
664                    let mut wrap = vec![1; l];
665                    wrap.push(c);
666                    wrap.extend(&mut counter);
667                    return Some(super::LinesWrapCount::Wrap(wrap));
668                }
669                l += 1;
670            }
671            if l != *len {
672                // no line wraps, but changed line count.
673                Some(super::LinesWrapCount::NoWrap(l))
674            } else {
675                None
676            }
677        }
678        super::LinesWrapCount::Wrap(counts) => {
679            // find `counts[i]` that diverges from counts, OR
680            // find if all new counts is now NoWrap
681            let mut prev_counts = counts.iter();
682            let mut new_counts = lines_wrap_counter(txt);
683            let mut eq_l = 0;
684            let mut eq_wrap = false;
685            for c in &mut new_counts {
686                if prev_counts.next() == Some(&c) {
687                    eq_l += 1;
688                    eq_wrap |= c != 1;
689                } else if eq_wrap || c != 1 {
690                    // not eq, and already found a wrap line
691                    let mut wrap = counts[..eq_l].to_vec();
692                    wrap.push(c);
693                    wrap.extend(&mut new_counts);
694                    return Some(super::LinesWrapCount::Wrap(wrap));
695                } else {
696                    // not eq, but maybe no wrap
697                    let mut l = eq_l + 1; // +1 is +c
698                    for c in &mut new_counts {
699                        if c != 1 {
700                            // nope, found a line wrap
701                            let mut wrap = vec![1; l];
702                            wrap.push(c);
703                            wrap.extend(&mut new_counts);
704                            return Some(super::LinesWrapCount::Wrap(wrap));
705                        }
706                        l += 1;
707                    }
708                    // changed to no wrap
709                    return Some(super::LinesWrapCount::NoWrap(l));
710                }
711            }
712            if prev_counts.next().is_some() {
713                Some(super::LinesWrapCount::Wrap(counts[..eq_l].to_vec()))
714            } else {
715                None
716            }
717        }
718    }
719}
720fn lines_wrap_counter(txt: &ShapedText) -> impl Iterator<Item = u32> + '_ {
721    struct Counter<I> {
722        lines: I,
723        count: u32,
724    }
725    impl<'a, I: Iterator<Item = ShapedLine<'a>>> Iterator for Counter<I> {
726        type Item = u32;
727
728        fn next(&mut self) -> Option<u32> {
729            loop {
730                let line = self.lines.next()?;
731                if line.ended_by_wrap() {
732                    self.count += 1;
733                    continue;
734                }
735
736                let c = self.count;
737                self.count = 1;
738
739                return Some(c);
740            }
741        }
742    }
743    Counter {
744        lines: txt.lines(),
745        count: 1,
746    }
747}
748
749pub(super) fn parse_text<T>(child: impl IntoUiNode, value: impl IntoVar<T>) -> UiNode
750where
751    T: super::TxtParseValue,
752{
753    let value = value.into_var();
754
755    let error = var(Txt::from_static(""));
756    let mut _error_note = DataNoteHandle::dummy();
757
758    #[derive(Clone, Copy, bytemuck::NoUninit)]
759    #[repr(u8)]
760    enum State {
761        Sync,
762        Requested,
763        Pending,
764    }
765    let state = Arc::new(Atomic::new(State::Sync));
766
767    match_node(child, move |_, op| match op {
768        UiNodeOp::Init => {
769            let ctx = TEXT.resolved();
770
771            // initial T -> Txt sync
772            ctx.txt.set_from_map(&value, |val| val.to_txt());
773
774            // bind `TXT_PARSE_LIVE_VAR` <-> `value` using `bind_filter_map_bidi`:
775            // - in case of parse error, it is set in `error` variable, that is held by the binding.
776            // - on error update the DATA note is updated.
777            // - in case parse is not live, ignores updates (Txt -> None), sets `state` to `Pending`.
778            // - in case of Pending and `PARSE_CMD` state is set to `Requested` and `TXT_PARSE_LIVE_VAR.update()`.
779            // - the pending state is also tracked in `TXT_PARSE_PENDING_VAR` and the `PARSE_CMD` handle.
780
781            let live = TXT_PARSE_LIVE_VAR.current_context();
782            let is_pending = TXT_PARSE_PENDING_VAR.current_context();
783            let cmd_handle = Arc::new(super::cmd::PARSE_CMD.scoped(WIDGET.id()).subscribe(false));
784
785            let binding = ctx.txt.bind_filter_map_bidi(
786                &value,
787                clmv!(state, error, is_pending, cmd_handle, |txt| {
788                    if live.get() || matches!(state.load(Ordering::Relaxed), State::Requested) {
789                        // can try parse
790
791                        if !matches!(state.swap(State::Sync, Ordering::Relaxed), State::Sync) {
792                            // exit pending state, even if it parse fails
793                            is_pending.set(false);
794                            cmd_handle.enabled().set(false);
795                        }
796
797                        // try parse
798                        match T::from_txt(txt) {
799                            Ok(val) => {
800                                error.set(Txt::from_static(""));
801                                Some(val)
802                            }
803                            Err(e) => {
804                                error.set(e);
805                                None
806                            }
807                        }
808                    } else {
809                        // cannot try parse
810
811                        if !matches!(state.swap(State::Pending, Ordering::Relaxed), State::Pending) {
812                            // enter pending state
813                            is_pending.set(true);
814                            cmd_handle.enabled().set(true);
815                        }
816
817                        // does not update the value
818                        None
819                    }
820                }),
821                clmv!(state, error, |val| {
822                    // value updated externally, exit error, exit pending.
823
824                    error.set(Txt::from_static(""));
825
826                    if !matches!(state.swap(State::Sync, Ordering::Relaxed), State::Sync) {
827                        is_pending.set(false);
828                        cmd_handle.enabled().set(false);
829                    }
830
831                    Some(val.to_txt())
832                }),
833            );
834
835            // cmd_handle is held by the binding
836
837            WIDGET.sub_var(&TXT_PARSE_LIVE_VAR).sub_var(&error).push_var_handles(binding);
838        }
839        UiNodeOp::Deinit => {
840            _error_note = DataNoteHandle::dummy();
841        }
842        UiNodeOp::Update { .. } => {
843            if matches!(state.load(Ordering::Relaxed), State::Pending) {
844                super::cmd::PARSE_CMD.scoped(WIDGET.id()).each_update(true, false, |args| {
845                    // requested parse and parse is pending
846
847                    state.store(State::Requested, Ordering::Relaxed);
848                    TEXT.resolved().txt.update();
849                    args.propagation.stop();
850                });
851            }
852
853            if let Some(true) = TXT_PARSE_LIVE_VAR.get_new()
854                && matches!(state.load(Ordering::Relaxed), State::Pending)
855            {
856                // enabled live parse and parse is pending
857
858                TEXT.resolved().txt.update();
859            }
860
861            if let Some(error) = error.get_new() {
862                // remove or replace the error
863
864                _error_note = if error.is_empty() {
865                    DataNoteHandle::dummy()
866                } else {
867                    DATA.invalidate(error)
868                };
869            }
870        }
871        _ => {}
872    })
873}
874
875pub(super) fn on_change_stop(child: impl IntoUiNode, handler: Handler<ChangeStopArgs>) -> UiNode {
876    let mut handler = handler.into_wgt_runner();
877    let mut pending = None;
878    match_node(child, move |c, op| match op {
879        UiNodeOp::Deinit => {
880            handler.deinit();
881        }
882        UiNodeOp::Update { updates } => {
883            if TEXT.resolved().txt.is_new() {
884                let deadline = TIMERS.deadline(CHANGE_STOP_DELAY_VAR.get());
885                deadline.subscribe(UpdateOp::Update, WIDGET.id()).perm();
886                pending = Some(deadline);
887            } else if let Some(p) = &pending
888                && p.get().has_elapsed()
889            {
890                pending = None;
891
892                handler.event(&ChangeStopArgs {
893                    cause: ChangeStopCause::DelayElapsed,
894                });
895            }
896
897            c.update(updates);
898            handler.update();
899
900            if pending.is_none() {
901                return;
902            }
903
904            KEY_INPUT_EVENT.each_update(false, |args| {
905                if let KeyState::Pressed = args.state
906                    && let Key::Enter = &args.key
907                    && !ACCEPTS_ENTER_VAR.get()
908                {
909                    pending = None;
910                    handler.event(&ChangeStopArgs {
911                        cause: ChangeStopCause::Enter,
912                    });
913                }
914            });
915            FOCUS_CHANGED_EVENT.each_update(true, |args| {
916                let target = WIDGET.id();
917                if args.is_blur(target) {
918                    pending = None;
919                    handler.event(&ChangeStopArgs {
920                        cause: ChangeStopCause::Blur,
921                    });
922                }
923            });
924        }
925        _ => {}
926    })
927}
928
929/// Implements the selection toolbar.
930pub fn selection_toolbar_node(child: impl IntoUiNode) -> UiNode {
931    use super::node::*;
932
933    let mut selection_range = None;
934    let mut popup_state = None::<Var<PopupState>>;
935    match_node(child, move |c, op| {
936        let mut open = false;
937        let mut open_long_press = false;
938        let mut close = false;
939        match op {
940            UiNodeOp::Init => {
941                WIDGET.sub_var(&SELECTION_TOOLBAR_FN_VAR);
942            }
943            UiNodeOp::Deinit => {
944                close = true;
945            }
946            UiNodeOp::Update { updates } => {
947                if SELECTION_TOOLBAR_FN_VAR.is_new() {
948                    close = true;
949                }
950                if let Some(state) = popup_state.as_ref().and_then(|s| s.get_new()) {
951                    let is_open = !matches!(state, PopupState::Closed);
952                    let mut r_txt = TEXT.resolve();
953                    if r_txt.selection_toolbar_is_open != is_open {
954                        r_txt.selection_toolbar_is_open = is_open;
955                        WIDGET.layout().render();
956
957                        if !is_open {
958                            popup_state = None;
959                        }
960                    }
961                }
962
963                c.update(updates);
964
965                let open_id = || {
966                    if let Some(popup_state) = &popup_state
967                        && let PopupState::Open(id) = popup_state.get()
968                    {
969                        return Some(id);
970                    }
971                    None
972                };
973
974                MOUSE_INPUT_EVENT.each_update(true, |args| {
975                    if open_id().map(|id| !args.target.contains(id)).unwrap_or(false) {
976                        close = true;
977                    }
978                    if args.state == ButtonState::Released {
979                        open = true;
980                    }
981                });
982
983                if TOUCH_LONG_PRESS_EVENT.has_update(true) {
984                    open = true;
985                    open_long_press = true;
986                }
987                if KEY_INPUT_EVENT.has_update(true) {
988                    close = true;
989                }
990
991                FOCUS_CHANGED_EVENT.each_update(true, |args| {
992                    if args.is_blur(WIDGET.id())
993                        && open_id()
994                            .map(|id| args.new_focus.as_ref().map(|p| !p.contains(id)).unwrap_or(true))
995                            .unwrap_or(false)
996                    {
997                        close = true;
998                    }
999                });
1000                TOUCH_INPUT_EVENT.each_update(true, |args| {
1001                    if matches!(args.phase, TouchPhase::Start | TouchPhase::Move)
1002                        && open_id().map(|id| !args.target.contains(id)).unwrap_or(false)
1003                    {
1004                        close = true;
1005                    }
1006                });
1007
1008                if popup_state.is_some() {
1009                    let r_txt = TEXT.resolved();
1010                    if selection_range != r_txt.caret.selection_range() {
1011                        close = true;
1012                    }
1013                }
1014            }
1015            _ => {}
1016        }
1017        if close && let Some(state) = &popup_state.take() {
1018            selection_range = None;
1019            POPUP.close(state);
1020            TEXT.resolve().selection_toolbar_is_open = false;
1021            WIDGET.layout().render();
1022        }
1023        if open {
1024            let r_txt = TEXT.resolved();
1025
1026            let range = r_txt.caret.selection_range();
1027            if open_long_press || range.is_some() {
1028                selection_range = range;
1029
1030                let toolbar_fn = SELECTION_TOOLBAR_FN_VAR.get();
1031                if let Some(node) = toolbar_fn.call_checked(SelectionToolbarArgs {
1032                    anchor_id: WIDGET.id(),
1033                    is_touch: matches!(r_txt.selection_by, SelectionBy::Touch),
1034                }) {
1035                    let (node, _) = node.init_widget();
1036
1037                    let mut translate = PxVector::zero();
1038                    let transform_key = FrameValueKey::new_unique();
1039                    let node = match_widget(node, move |c, op| match op {
1040                        UiNodeOp::Init => {
1041                            c.init();
1042                            if let Some(mut wgt) = c.node().as_widget() {
1043                                wgt.with_context(WidgetUpdateMode::Bubble, || WIDGET.sub_var_layout(&SELECTION_TOOLBAR_ANCHOR_VAR));
1044                            }
1045                        }
1046                        UiNodeOp::Layout { wl, final_size } => {
1047                            let r_txt = TEXT.resolved();
1048
1049                            let range = if open_long_press {
1050                                Some(r_txt.caret.selection_range().unwrap_or_else(|| {
1051                                    let i = r_txt.caret.index.unwrap_or(CaretIndex::ZERO);
1052                                    i..i
1053                                }))
1054                            } else {
1055                                r_txt.caret.selection_range()
1056                            };
1057
1058                            if let Some(range) = range {
1059                                let l_txt = TEXT.laidout();
1060                                let r_txt = r_txt.segmented_text.text();
1061
1062                                let mut bounds = PxBox::new(PxPoint::splat(Px::MAX), PxPoint::splat(Px::MIN));
1063                                for line_rect in l_txt.shaped_text.highlight_rects(range, r_txt) {
1064                                    let line_box = line_rect.to_box2d();
1065                                    bounds.min = bounds.min.min(line_box.min);
1066                                    bounds.max = bounds.max.max(line_box.max);
1067                                }
1068                                let selection_bounds = bounds.to_rect();
1069
1070                                *final_size = c.layout(wl);
1071
1072                                let offset = SELECTION_TOOLBAR_ANCHOR_VAR.get();
1073
1074                                fn layout_offset(size: PxSize, point: Point) -> PxVector {
1075                                    LAYOUT
1076                                        .with_constraints(PxConstraints2d::new_exact_size(size), || point.layout())
1077                                        .to_vector()
1078                                }
1079                                let place = layout_offset(selection_bounds.size, offset.place);
1080                                let origin = layout_offset(*final_size, offset.origin);
1081
1082                                translate = selection_bounds.origin.to_vector() + place - origin;
1083                            } else {
1084                                // no selection, must be closing
1085                                wl.collapse();
1086                                *final_size = PxSize::zero();
1087                            };
1088                        }
1089                        UiNodeOp::Render { frame } => {
1090                            let l_txt = TEXT.laidout();
1091                            let transform = l_txt.render_info.transform.then_translate(translate.cast());
1092                            let transform = adjust_viewport_bound(transform, c.node());
1093
1094                            frame.push_reference_frame(transform_key.into(), FrameValue::Value(transform), true, false, |frame| {
1095                                c.render(frame)
1096                            });
1097                        }
1098                        UiNodeOp::RenderUpdate { update } => {
1099                            let l_txt = TEXT.laidout();
1100                            let transform = l_txt.render_info.transform.then_translate(translate.cast());
1101                            let transform = adjust_viewport_bound(transform, c.node());
1102
1103                            update.with_transform(transform_key.update(transform, true), false, |update| c.render_update(update));
1104                        }
1105                        _ => {}
1106                    });
1107
1108                    // capture all context including LaidoutText, exclude text style properties.
1109                    let capture = ContextCapture::CaptureBlend {
1110                        filter: CaptureFilter::Exclude({
1111                            let mut exclude = ContextValueSet::new();
1112                            super::Text::context_vars_set(&mut exclude);
1113
1114                            let mut allow = ContextValueSet::new();
1115                            super::LangMix::<()>::context_vars_set(&mut allow);
1116                            exclude.remove_all(&allow);
1117                            exclude.remove(&SELECTION_TOOLBAR_ANCHOR_VAR);
1118
1119                            exclude
1120                        }),
1121                        over: false,
1122                    };
1123
1124                    let mut anchor_mode = AnchorMode::tooltip();
1125                    anchor_mode.transform = AnchorTransform::None;
1126                    let state = POPUP.open_config(node, anchor_mode, capture);
1127                    state.subscribe(UpdateOp::Update, WIDGET.id()).perm();
1128                    popup_state = Some(state);
1129                    drop(r_txt);
1130                    TEXT.resolve().selection_toolbar_is_open = true;
1131                    WIDGET.layout().render();
1132                }
1133            };
1134        }
1135    })
1136}
1137fn adjust_viewport_bound(transform: PxTransform, widget: &mut UiNode) -> PxTransform {
1138    let window_bounds = WINDOW.vars().actual_size_px().get();
1139    let wgt_bounds = PxBox::from(match widget.as_widget() {
1140        Some(mut w) => w.with_context(WidgetUpdateMode::Ignore, || WIDGET.bounds().outer_size()),
1141        None => PxSize::zero(),
1142    });
1143    let wgt_bounds = transform.outer_transformed(wgt_bounds).unwrap_or_default();
1144
1145    let x_underflow = -wgt_bounds.min.x.min(Px(0));
1146    let x_overflow = (wgt_bounds.max.x - window_bounds.width).max(Px(0));
1147    let y_underflow = -wgt_bounds.min.y.min(Px(0));
1148    let y_overflow = (wgt_bounds.max.y - window_bounds.height).max(Px(0));
1149
1150    let x = x_underflow - x_overflow;
1151    let y = y_underflow - y_overflow;
1152
1153    let correction = PxVector::new(x, y);
1154
1155    transform.then_translate(correction.cast())
1156}