Skip to main content

zng_wgt_text/node/
rich.rs

1use std::{borrow::Cow, fmt, sync::Arc};
2
3use parking_lot::{Mutex, RwLock};
4use zng_app::{event::CommandScope, widget::node::Z_INDEX};
5use zng_ext_clipboard::{CLIPBOARD, COPY_CMD};
6use zng_ext_font::CaretIndex;
7use zng_ext_input::{
8    focus::{FOCUS, FOCUS_CHANGED_EVENT},
9    mouse::MOUSE_INPUT_EVENT,
10    touch::{TOUCH_INPUT_EVENT, TOUCH_TAP_EVENT},
11};
12use zng_ext_window::WINDOWS;
13use zng_wgt::prelude::*;
14use zng_wgt_scroll::SCROLL;
15
16use crate::{
17    RICH_TEXT_FOCUSED_Z_VAR, TEXT_SELECTABLE_VAR,
18    cmd::{SELECT_ALL_CMD, SELECT_CMD, TextSelectOp},
19};
20
21use super::{RICH_TEXT, RichCaretInfo, RichText, TEXT};
22
23pub(crate) fn rich_text_node(child: impl IntoUiNode, enabled: impl IntoVar<bool>) -> UiNode {
24    let enabled = enabled.into_var();
25    let child = rich_text_cmds(child);
26    let child = rich_text_component(child, "rich_text");
27
28    let mut ctx = None;
29    match_node(child, move |child, op| match op {
30        UiNodeOp::Init => {
31            WIDGET.sub_var(&enabled);
32            if enabled.get() && TEXT.try_rich().is_none() {
33                ctx = Some(Arc::new(RwLock::new(RichText {
34                    root_id: WIDGET.id(),
35                    caret: RichCaretInfo {
36                        index: None,
37                        selection_index: None,
38                    },
39                    selection_started_by_alt: false,
40                })));
41
42                RICH_TEXT.with_context(&mut ctx, || child.init());
43            }
44        }
45        UiNodeOp::Update { updates } => {
46            if enabled.is_new() {
47                WIDGET.reinit();
48            } else if ctx.is_some() {
49                RICH_TEXT.with_context(&mut ctx, || child.update(updates));
50            }
51        }
52        UiNodeOp::Deinit => {
53            if ctx.is_some() {
54                RICH_TEXT.with_context(&mut ctx, || child.deinit());
55                ctx = None;
56            }
57        }
58        op => {
59            if ctx.is_some() {
60                RICH_TEXT.with_context(&mut ctx, || child.op(op));
61            }
62        }
63    })
64}
65
66fn rich_text_cmds(child: impl IntoUiNode) -> UiNode {
67    #[derive(Default)]
68    struct Cmds {
69        // cut: CommandHandle,
70        copy: CommandHandle,
71        // paste: CommandHandle,
72        // edit: CommandHandle,
73        select: CommandHandle,
74        select_all: CommandHandle,
75    }
76    let mut _cmds = Cmds::default();
77    match_node(child, move |_, op| match op {
78        UiNodeOp::Init if TEXT.try_rich().is_some() => {
79            let id = WIDGET.id();
80            // cmds.cut = CUT_CMD.scoped(id).subscribe(true);
81            _cmds.copy = COPY_CMD.scoped(id).subscribe(true);
82            // cmds.paste = PASTE_CMD.scoped(id).subscribe(true);
83            _cmds.select = SELECT_CMD.scoped(id).subscribe(true);
84            // cmds.edit = EDIT_CMD.scoped(id).subscribe(true);
85            _cmds.select_all = SELECT_ALL_CMD.scoped(id).subscribe(true);
86        }
87        UiNodeOp::Deinit => {
88            _cmds = Cmds::default();
89        }
90        UiNodeOp::Update { .. } => {
91            let ctx = match TEXT.try_rich() {
92                Some(c) => c,
93                None => return, // disabled
94            };
95            COPY_CMD.event().each_update(false, |args| {
96                if args.param.is_none()
97                    && let CommandScope::Widget(scope_id) = args.scope
98                    && (ctx.root_id == scope_id || ctx.leaf_info(scope_id).is_some())
99                {
100                    // is normal COPY_CMD request for the rich text or a leaf text.
101                    args.propagation.stop();
102
103                    let leaf_texts: Vec<_> = ctx
104                        .selection()
105                        .map(|leaf| {
106                            let rich_copy = RichTextCopyParam::default();
107                            COPY_CMD.scoped(leaf.id()).notify_param(rich_copy.clone());
108                            (rich_copy, leaf)
109                        })
110                        .collect();
111
112                    if !leaf_texts.is_empty() {
113                        // after all leafs set the text
114                        UPDATES.once_next_update("rich-text-copy", move || {
115                            let mut txt = String::new();
116
117                            for (leaf_text, leaf) in leaf_texts {
118                                if let Some(t_frag) = leaf_text.into_text() {
119                                    let line_info = leaf.rich_text_line_info();
120                                    if line_info.starts_new_line && !line_info.is_wrap_start && !txt.is_empty() {
121                                        txt.push('\n');
122                                    } else {
123                                        let spaced = matches!(t_frag.chars().next(), Some(c) if c.is_whitespace())
124                                            || matches!(txt.chars().last(), Some(c) if c.is_whitespace());
125                                        if !spaced {
126                                            txt.push(' ');
127                                        }
128                                    }
129
130                                    txt.push_str(&t_frag);
131                                }
132                            }
133
134                            let _ = CLIPBOARD.set_text(txt);
135                        });
136                    }
137                }
138            });
139
140            // actual leaf Text! handles rich text context for operations
141            SELECT_CMD.scoped(ctx.root_id).each_update(true, false, |args| {
142                args.propagation.stop();
143                if let Some(leaf) = ctx.caret_index_info().or_else(|| ctx.leaves().next()) {
144                    SELECT_CMD.scoped(leaf.id()).notify_param(args.param.clone());
145                }
146            });
147            SELECT_ALL_CMD.scoped(ctx.root_id).latest_update(true, false, |args| {
148                args.propagation.stop();
149                if let Some(leaf) = ctx.caret_index_info().or_else(|| ctx.leaves().next()) {
150                    SELECT_ALL_CMD.scoped(leaf.id()).notify_param(args.param.clone());
151                }
152            });
153        }
154        _ => {}
155    })
156}
157// some visuals (like selection background) depend on focused status of any rich leaf
158//
159// leaf texts also handle pointer input events for the rich text
160pub(crate) fn rich_text_events_broadcast(child: impl IntoUiNode) -> UiNode {
161    let mut handles = VarHandles::dummy();
162    match_node(child, move |c, op| {
163        match op {
164            UiNodeOp::Init if TEXT.try_rich().is_some() => {
165                WIDGET.sub_var(&TEXT_SELECTABLE_VAR);
166                if TEXT_SELECTABLE_VAR.get() {
167                    let id = WIDGET.id();
168                    handles.push(MOUSE_INPUT_EVENT.subscribe(UpdateOp::Update, id));
169                    handles.push(TOUCH_INPUT_EVENT.subscribe(UpdateOp::Update, id));
170                    handles.push(TOUCH_TAP_EVENT.subscribe(UpdateOp::Update, id));
171                }
172            }
173            UiNodeOp::Deinit => {
174                handles = VarHandles::dummy();
175            }
176            UiNodeOp::Update { updates } if let Some(ctx) = TEXT.try_rich() => {
177                let selectable = if let Some(sub) = TEXT_SELECTABLE_VAR.get_new() {
178                    if sub {
179                        let id = WIDGET.id();
180                        handles.push(MOUSE_INPUT_EVENT.subscribe(UpdateOp::Update, id));
181                        handles.push(TOUCH_INPUT_EVENT.subscribe(UpdateOp::Update, id));
182                        handles.push(TOUCH_TAP_EVENT.subscribe(UpdateOp::Update, id));
183                    } else {
184                        handles.clear();
185                    }
186                    sub
187                } else {
188                    TEXT_SELECTABLE_VAR.get()
189                };
190                if !selectable {
191                    return;
192                }
193
194                // Broadcast FOCUS_CHANGED_EVENT to all leaves, they handle this event targeting the rich text only
195                if ctx.caret.selection_index.is_some() {
196                    let extend_propagation = FOCUS_CHANGED_EVENT.any_update(true, |args| {
197                        args.prev_focus.iter().chain(args.new_focus.iter()).any(|p| p.contains(ctx.root_id))
198                    });
199                    if extend_propagation {
200                        if let Cow::Owned(updates) = updates.clone_insert_all(ctx.selection()) {
201                            drop(ctx);
202                            c.update(&updates);
203                        }
204                        return;
205                    }
206                }
207
208                // extend pointer input events targeting this widget to first leaf. Leaves handle
209                // selection events from pointer for the rich root ID too
210                let extend_propagation = MOUSE_INPUT_EVENT.any_update(true, |args| {
211                    args.is_primary() && args.is_mouse_down() && args.target.contains(ctx.root_id)
212                }) || TOUCH_INPUT_EVENT.any_update(true, |args| args.target.contains(ctx.root_id))
213                    || TOUCH_TAP_EVENT.any_update(true, |args| args.target.contains(ctx.root_id));
214                if extend_propagation && let Cow::Owned(updates) = updates.clone_insert_any(ctx.leaves()) {
215                    drop(ctx);
216                    c.update(&updates);
217                }
218            }
219            _ => (),
220        }
221    })
222}
223
224/// An UI node that implements some behavior for rich text composition.
225///
226/// This node is intrinsic to the `Text!` widget and is part of the `rich_text` property. Note that the
227/// actual rich text editing is implemented by the `resolve_text` and `layout_text` nodes that are intrinsic to `Text!`.
228///
229/// The `kind` identifies what kind of component, the value `"rich_text"` is used by the `rich_text` property, the value `"text"`
230/// is used by the `Text!` widget, any other value defines a [`RichTextComponent::Leaf`] that is expected to be focusable, inlined
231/// and able to handle rich text composition requests.
232pub fn rich_text_component(child: impl IntoUiNode, kind: &'static str) -> UiNode {
233    let mut focus_within = false;
234    let mut prev_index = ZIndex::DEFAULT;
235    let mut index_update = None;
236    match_node(child, move |c, op| match op {
237        UiNodeOp::Init => {
238            c.init();
239
240            if TEXT.try_rich().is_some() {
241                WIDGET.sub_event(&FOCUS_CHANGED_EVENT).sub_var(&RICH_TEXT_FOCUSED_Z_VAR);
242                prev_index = Z_INDEX.get();
243            }
244        }
245        UiNodeOp::Deinit => {
246            focus_within = false;
247        }
248        UiNodeOp::Info { info } if let Some(r) = TEXT.try_rich() => {
249            let c = match kind {
250                "rich_text" => {
251                    if r.root_id == WIDGET.id() {
252                        RichTextComponent::Root
253                    } else {
254                        RichTextComponent::Branch
255                    }
256                }
257                kind => RichTextComponent::Leaf { kind },
258            };
259            info.set_meta(*RICH_TEXT_COMPONENT_ID, c);
260        }
261        UiNodeOp::Update { updates } => {
262            FOCUS_CHANGED_EVENT.each_update(true, |args| {
263                let new_is_focus_within = args.is_focus_within(WIDGET.id());
264                if focus_within != new_is_focus_within {
265                    focus_within = new_is_focus_within;
266
267                    if TEXT.try_rich().is_some() {
268                        index_update = Some(focus_within);
269                    }
270                }
271            });
272
273            c.update(updates);
274
275            if let Some(apply) = index_update.take() {
276                if apply {
277                    prev_index = Z_INDEX.get();
278                    if let Some(i) = RICH_TEXT_FOCUSED_Z_VAR.get() {
279                        Z_INDEX.set(i);
280                    }
281                } else if RICH_TEXT_FOCUSED_Z_VAR.get().is_some() {
282                    Z_INDEX.set(prev_index);
283                }
284            }
285            if let Some(idx) = RICH_TEXT_FOCUSED_Z_VAR.get_new()
286                && focus_within
287            {
288                Z_INDEX.set(idx.unwrap_or(prev_index));
289            }
290        }
291        _ => {}
292    })
293}
294
295impl RichText {
296    /// Get root widget info.
297    ///
298    /// See also [`RichTextWidgetInfoExt`] to query the
299    pub fn root_info(&self) -> Option<WidgetInfo> {
300        WINDOWS.widget_info(self.root_id)
301    }
302
303    /// Iterate over the text/leaf component descendants that can be interacted with.
304    pub fn leaves(&self) -> impl Iterator<Item = WidgetInfo> + 'static {
305        self.root_info().into_iter().flat_map(|w| rich_text_leaves_static(&w))
306    }
307
308    /// Iterate over the text/leaf component descendants that can be interacted with in reverse.
309    pub fn leaves_rev(&self) -> impl Iterator<Item = WidgetInfo> + 'static {
310        self.root_info().into_iter().flat_map(|w| rich_text_leaves_rev_static(&w))
311    }
312
313    /// Iterate over all text/leaf components that are part of the selection.
314    ///
315    /// The return iterator is empty if there is no selection.
316    pub fn selection(&self) -> impl ExactSizeIterator<Item = WidgetInfo> + 'static {
317        let (root, a, b) = match (self.caret.index, self.caret.selection_index) {
318            (Some(a), Some(b)) => (self.root_info(), a, b),
319            _ => (None, self.root_id, self.root_id),
320        };
321        OptKnownLenIter {
322            known_len_iter: root.into_iter().flat_map(move |w| rich_text_selection_static(&w, a, b)),
323        }
324    }
325
326    /// Iterate over all text/leaf components that are part of the selection in reverse.
327    ///
328    /// The return iterator is empty if there is no selection.
329    pub fn selection_rev(&self) -> impl ExactSizeIterator<Item = WidgetInfo> + 'static {
330        let (root, a, b) = match (self.caret.index, self.caret.selection_index) {
331            (Some(a), Some(b)) => (self.root_info(), a, b),
332            _ => (None, self.root_id, self.root_id),
333        };
334        OptKnownLenIter {
335            known_len_iter: root.into_iter().flat_map(move |w| rich_text_selection_rev_static(&w, a, b)),
336        }
337    }
338
339    /// Gets the `caret.index` widget info if it is set and is a valid leaf.
340    pub fn caret_index_info(&self) -> Option<WidgetInfo> {
341        self.leaf_info(self.caret.index?)
342    }
343
344    /// Gets the `caret.selection_index` widget info if it is set and is a valid leaf.
345    pub fn caret_selection_index_info(&self) -> Option<WidgetInfo> {
346        self.leaf_info(self.caret.selection_index?)
347    }
348
349    /// Gets the `id` widget info if it is a valid leaf in the rich text context.
350    pub fn leaf_info(&self, id: WidgetId) -> Option<WidgetInfo> {
351        let root = self.root_info()?;
352        let wgt = root.tree().get(id)?;
353        if !matches!(wgt.rich_text_component(), Some(RichTextComponent::Leaf { .. })) {
354            return None;
355        }
356        if !wgt.is_descendant(&root) {
357            return None;
358        }
359        Some(wgt)
360    }
361}
362impl RichCaretInfo {
363    /// Update the rich selection and local selection for each rich component.
364    ///
365    /// Before calling this you must update the [`CaretInfo::index`] in `new_index` and the [`CaretInfo::selection_index`] in
366    /// `new_selection_index`. Alternatively enable `skip_end_points` to handle the local selection at the end point widgets.
367    ///
368    /// If you don't want focus to be moved to the `new_index` set `skip_focus` to `true`.
369    ///
370    /// # Panics
371    ///
372    /// Panics if `new_index` or `new_selection_index` is not inside the same rich text context.
373    ///
374    /// [`CaretInfo::index`]: crate::node::CaretInfo::index
375    /// [`CaretInfo::selection_index`]: crate::node::CaretInfo::selection_index
376    pub fn update_selection(
377        &mut self,
378        new_index: &WidgetInfo,
379        new_selection_index: Option<&WidgetInfo>,
380        skip_end_points: bool,
381        skip_focus: bool,
382    ) {
383        let root = new_index.rich_text_root().unwrap();
384        let old_index = self
385            .index
386            .and_then(|id| new_index.tree().get(id))
387            .unwrap_or_else(|| new_index.clone());
388        let old_selection_index = self.selection_index.and_then(|id| new_index.tree().get(id));
389
390        self.index = Some(new_index.id());
391        self.selection_index = new_selection_index.map(|w| w.id());
392
393        match (&old_selection_index, new_selection_index) {
394            (None, None) => self.continue_focus(skip_focus, new_index, &root),
395            (None, Some(new_sel)) => {
396                // add selection
397                let (a, b) = match new_index.cmp_sibling_in(new_sel, &root).unwrap() {
398                    std::cmp::Ordering::Less => (new_index, new_sel),
399                    std::cmp::Ordering::Greater => (new_sel, new_index),
400                    std::cmp::Ordering::Equal => {
401                        // single widget selection, already defined
402                        return self.continue_focus(skip_focus, new_index, &root);
403                    }
404                };
405                if !skip_end_points {
406                    self.continue_select_lesser(a, a == new_index);
407                }
408                let middle_op = TextSelectOp::local_select_all();
409                for middle in a.rich_text_next().take_while(|n| n != b) {
410                    notify_leaf_select_op(middle.id(), middle_op.clone());
411                }
412                if !skip_end_points {
413                    self.continue_select_greater(b, b == new_index);
414                }
415
416                self.continue_focus(skip_focus, new_index, &root);
417            }
418            (Some(old_sel), None) => {
419                // remove selection
420                let (a, b) = match old_index.cmp_sibling_in(old_sel, &root).unwrap() {
421                    std::cmp::Ordering::Less => (&old_index, old_sel),
422                    std::cmp::Ordering::Greater => (old_sel, &old_index),
423                    std::cmp::Ordering::Equal => {
424                        // was single widget selection
425                        if !skip_end_points {
426                            notify_leaf_select_op(old_sel.id(), TextSelectOp::local_clear_selection());
427                        }
428                        return self.continue_focus(skip_focus, new_index, &root);
429                    }
430                };
431                let op = TextSelectOp::local_clear_selection();
432                if !skip_end_points {
433                    notify_leaf_select_op(a.id(), op.clone());
434                }
435                for middle in a.rich_text_next().take_while(|n| n != b) {
436                    notify_leaf_select_op(middle.id(), op.clone());
437                }
438                if !skip_end_points {
439                    notify_leaf_select_op(b.id(), op);
440                }
441
442                self.continue_focus(skip_focus, new_index, &root);
443            }
444            (Some(old_sel), Some(new_sel)) => {
445                // update selection
446
447                let (old_a, old_b) = match old_index.cmp_sibling_in(old_sel, &root).unwrap() {
448                    std::cmp::Ordering::Less | std::cmp::Ordering::Equal => (&old_index, old_sel),
449                    std::cmp::Ordering::Greater => (old_sel, &old_index),
450                };
451                let (new_a, new_b) = match new_index.cmp_sibling_in(new_sel, &root).unwrap() {
452                    std::cmp::Ordering::Less | std::cmp::Ordering::Equal => (new_index, new_sel),
453                    std::cmp::Ordering::Greater => (new_sel, new_index),
454                };
455
456                let min_a = match old_a.cmp_sibling_in(new_a, &root).unwrap() {
457                    std::cmp::Ordering::Less | std::cmp::Ordering::Equal => old_a,
458                    std::cmp::Ordering::Greater => new_a,
459                };
460                let max_b = match old_b.cmp_sibling_in(new_b, &root).unwrap() {
461                    std::cmp::Ordering::Less => new_b,
462                    std::cmp::Ordering::Equal | std::cmp::Ordering::Greater => old_b,
463                };
464
465                fn inclusive_range_contains(a: &WidgetInfo, b: &WidgetInfo, q: &WidgetInfo, root: &WidgetInfo) -> bool {
466                    match a.cmp_sibling_in(q, root).unwrap() {
467                        // a < q
468                        std::cmp::Ordering::Less => match b.cmp_sibling_in(q, root).unwrap() {
469                            // b < q
470                            std::cmp::Ordering::Less => false,
471                            // b >= q
472                            std::cmp::Ordering::Equal | std::cmp::Ordering::Greater => true,
473                        },
474                        // a == q
475                        std::cmp::Ordering::Equal => true,
476                        // a > q
477                        std::cmp::Ordering::Greater => false,
478                    }
479                }
480
481                // println!("(old_a, old_b) = ({}, {})", old_a.id(), old_b.id());
482                // println!("(new_a, new_b) = ({}, {})", new_a.id(), new_b.id());
483                // println!("(min_a, max_b) = ({}, {})", min_a.id(), max_b.id());
484
485                for wgt in min_a.rich_text_self_and_next() {
486                    if &wgt == new_a {
487                        if !skip_end_points && new_a != new_b {
488                            self.continue_select_lesser(new_a, new_a == new_index);
489                        }
490                    } else if &wgt == new_b {
491                        if !skip_end_points && new_a != new_b {
492                            self.continue_select_greater(new_b, new_b == new_index);
493                        }
494                    } else {
495                        let is_old = inclusive_range_contains(old_a, old_b, &wgt, &root);
496                        let is_new = inclusive_range_contains(new_a, new_b, &wgt, &root);
497
498                        match (is_old, is_new) {
499                            (true, true) => {
500                                if &wgt == old_a || &wgt == old_b {
501                                    // was endpoint now is full selection
502                                    notify_leaf_select_op(wgt.id(), TextSelectOp::local_select_all())
503                                }
504                            }
505                            (true, false) => {
506                                notify_leaf_select_op(wgt.id(), TextSelectOp::local_clear_selection());
507                            }
508                            (false, true) => notify_leaf_select_op(wgt.id(), TextSelectOp::local_select_all()),
509                            (false, false) => {}
510                        }
511                    }
512
513                    if &wgt == max_b {
514                        break;
515                    }
516                }
517
518                self.continue_focus(skip_focus, new_index, &root);
519            }
520        }
521    }
522    fn continue_select_lesser(&self, a: &WidgetInfo, a_is_caret: bool) {
523        notify_leaf_select_op(
524            a.id(),
525            TextSelectOp::new(move || {
526                let len = TEXT.resolved().segmented_text.text().len();
527                let len = CaretIndex { index: len, line: 0 }; // line is updated next layout
528                let mut ctx = TEXT.resolve_caret();
529                if a_is_caret {
530                    ctx.selection_index = Some(len);
531                } else {
532                    ctx.index = Some(len);
533                }
534                ctx.index_version += 1;
535            }),
536        );
537    }
538    fn continue_select_greater(&self, b: &WidgetInfo, b_is_caret: bool) {
539        notify_leaf_select_op(
540            b.id(),
541            TextSelectOp::new(move || {
542                let mut ctx = TEXT.resolve_caret();
543                if b_is_caret {
544                    ctx.selection_index = Some(CaretIndex::ZERO);
545                } else {
546                    ctx.index = Some(CaretIndex::ZERO);
547                }
548                ctx.index_version += 1;
549            }),
550        );
551    }
552    fn continue_focus(&self, skip_focus: bool, new_index: &WidgetInfo, root: &WidgetInfo) {
553        if !skip_focus && FOCUS.is_focus_within(root.id()).get() {
554            // focus leaf with caret to receive key input
555            FOCUS.focus_widget(new_index.id(), false);
556            // don't scroll to focused, text nodes already implement scrolling to the caret,
557            // with support for not scrolling in some cases
558            SCROLL.ignore_next_scroll_to_focused();
559        }
560    }
561}
562
563pub(crate) fn notify_leaf_select_op(leaf_id: WidgetId, op: TextSelectOp) {
564    SELECT_CMD.scoped(leaf_id).notify_param(op);
565}
566
567/// Extends [`WidgetInfo`] state to provide information about rich text.
568pub trait RichTextWidgetInfoExt {
569    /// Gets the outer most ancestor that defines the rich text root.
570    fn rich_text_root(&self) -> Option<WidgetInfo>;
571
572    /// Gets what kind of component of the rich text tree this widget is.
573    fn rich_text_component(&self) -> Option<RichTextComponent>;
574
575    /// Iterate over the text/leaf component descendants that can be interacted with.
576    fn rich_text_leaves(&self) -> impl Iterator<Item = WidgetInfo> + 'static;
577    /// Iterate over the text/leaf component descendants that can be interacted with, in reverse.
578    fn rich_text_leaves_rev(&self) -> impl Iterator<Item = WidgetInfo> + 'static;
579
580    /// Iterate over the selection text/leaf component descendants that can be interacted with.
581    ///
582    /// The iterator is over `a..=b` or if `a` is after `b` the iterator is over `b..=a`.
583    fn rich_text_selection(&self, a: WidgetId, b: WidgetId) -> impl ExactSizeIterator<Item = WidgetInfo> + 'static;
584    /// Iterate over the selection text/leaf component descendants that can be interacted with, in reverse.
585    ///
586    /// The iterator is over `b..=a` or if `a` is after `b` the iterator is over `a..=b`.
587    fn rich_text_selection_rev(&self, a: WidgetId, b: WidgetId) -> impl ExactSizeIterator<Item = WidgetInfo> + 'static;
588
589    /// Iterate over the prev text/leaf components before the current one.
590    fn rich_text_prev(&self) -> impl Iterator<Item = WidgetInfo> + 'static;
591    /// Iterate over the text/leaf component and previous.
592    fn rich_text_self_and_prev(&self) -> impl Iterator<Item = WidgetInfo> + 'static;
593    /// Iterate over the next text/leaf components after the current one.
594    fn rich_text_next(&self) -> impl Iterator<Item = WidgetInfo> + 'static;
595    /// Iterate over the text/leaf component and next.
596    fn rich_text_self_and_next(&self) -> impl Iterator<Item = WidgetInfo> + 'static;
597
598    /// Gets info about how this rich leaf affects the text lines.
599    fn rich_text_line_info(&self) -> RichLineInfo;
600
601    /// Gets the leaf descendant that is nearest to the `window_point`.
602    fn rich_text_nearest_leaf(&self, window_point: PxPoint) -> Option<WidgetInfo>;
603    /// Gets the leaf descendant that is nearest to the `window_point` and is approved by the filter.
604    ///
605    /// The filter parameters are the widget, the rect, the rect row index and the widget inline rows length. If the widget is not inlined
606    /// both index and len are zero.
607    fn rich_text_nearest_leaf_filtered(
608        &self,
609        window_point: PxPoint,
610        filter: impl FnMut(&WidgetInfo, PxRect, usize, usize) -> bool,
611    ) -> Option<WidgetInfo>;
612}
613impl RichTextWidgetInfoExt for WidgetInfo {
614    fn rich_text_root(&self) -> Option<WidgetInfo> {
615        self.self_and_ancestors()
616            .find(|w| matches!(w.rich_text_component(), Some(RichTextComponent::Root)))
617    }
618
619    fn rich_text_component(&self) -> Option<RichTextComponent> {
620        self.meta().copy(*RICH_TEXT_COMPONENT_ID)
621    }
622
623    fn rich_text_leaves(&self) -> impl Iterator<Item = WidgetInfo> + 'static {
624        rich_text_leaves_static(self)
625    }
626    fn rich_text_leaves_rev(&self) -> impl Iterator<Item = WidgetInfo> + 'static {
627        rich_text_leaves_rev_static(self)
628    }
629
630    fn rich_text_selection(&self, a: WidgetId, b: WidgetId) -> impl ExactSizeIterator<Item = WidgetInfo> + 'static {
631        rich_text_selection_static(self, a, b)
632    }
633    fn rich_text_selection_rev(&self, a: WidgetId, b: WidgetId) -> impl ExactSizeIterator<Item = WidgetInfo> + 'static {
634        rich_text_selection_rev_static(self, a, b)
635    }
636
637    fn rich_text_prev(&self) -> impl Iterator<Item = WidgetInfo> + 'static {
638        let me = self.clone();
639        self.rich_text_root()
640            .into_iter()
641            .flat_map(move |w| me.prev_siblings_in(&w))
642            .filter(|w| matches!(w.rich_text_component(), Some(RichTextComponent::Leaf { .. })))
643    }
644
645    fn rich_text_next(&self) -> impl Iterator<Item = WidgetInfo> + 'static {
646        let me = self.clone();
647        self.rich_text_root()
648            .into_iter()
649            .flat_map(move |w| me.next_siblings_in(&w))
650            .filter(|w| matches!(w.rich_text_component(), Some(RichTextComponent::Leaf { .. })))
651    }
652
653    fn rich_text_self_and_prev(&self) -> impl Iterator<Item = WidgetInfo> + 'static {
654        let me = self.clone();
655        self.rich_text_root()
656            .into_iter()
657            .flat_map(move |w| me.self_and_prev_siblings_in(&w))
658            .filter(|w| matches!(w.rich_text_component(), Some(RichTextComponent::Leaf { .. })))
659    }
660
661    fn rich_text_self_and_next(&self) -> impl Iterator<Item = WidgetInfo> + 'static {
662        let me = self.clone();
663        self.rich_text_root()
664            .into_iter()
665            .flat_map(move |w| me.self_and_next_siblings_in(&w))
666            .filter(|w| matches!(w.rich_text_component(), Some(RichTextComponent::Leaf { .. })))
667    }
668
669    fn rich_text_line_info(&self) -> RichLineInfo {
670        let (prev_min, prev_max) = match self.rich_text_prev().next() {
671            Some(p) => {
672                let bounds = p.bounds_info();
673                let inner_bounds = bounds.inner_bounds();
674                if let Some(inline) = bounds.inline() {
675                    let mut last = inline.rows[inline.rows.len() - 1];
676                    last.origin += inner_bounds.origin.to_vector();
677                    (last.min_y(), last.max_y())
678                } else {
679                    (inner_bounds.min_y(), inner_bounds.max_y())
680                }
681            }
682            None => (Px::MIN, Px::MIN),
683        };
684
685        let bounds = self.bounds_info();
686        let inner_bounds = bounds.inner_bounds();
687        let (min, max, wraps, inlined) = if let Some(inline) = bounds.inline() {
688            let mut first = inline.rows[0];
689            first.origin += inner_bounds.origin.to_vector();
690            (first.min_y(), first.max_y(), inline.rows.len() > 1, true)
691        } else {
692            (inner_bounds.min_y(), inner_bounds.max_y(), false, false)
693        };
694
695        let starts = !lines_overlap_strict(prev_min, prev_max, min, max);
696
697        let mut is_wrap_start = false;
698        if inlined {
699            let mut child_id = self.id();
700            for parent in self.ancestors() {
701                if parent.first_child().unwrap().id() != child_id {
702                    is_wrap_start = true; // inlined and is not first
703                    break;
704                }
705                if parent.bounds_info().inline().is_none() {
706                    break;
707                }
708                child_id = parent.id();
709            }
710        }
711
712        RichLineInfo {
713            starts_new_line: starts,
714            is_wrap_start,
715            ends_in_new_line: wraps,
716        }
717    }
718
719    fn rich_text_nearest_leaf(&self, window_point: PxPoint) -> Option<WidgetInfo> {
720        self.rich_text_nearest_leaf_filtered(window_point, |_, _, _, _| true)
721    }
722    fn rich_text_nearest_leaf_filtered(
723        &self,
724        window_point: PxPoint,
725        mut filter: impl FnMut(&WidgetInfo, PxRect, usize, usize) -> bool,
726    ) -> Option<WidgetInfo> {
727        let root_size = self.inner_border_size();
728        let search_radius = root_size.width.max(root_size.height);
729
730        self.nearest_rect_filtered(window_point, search_radius, |w, rect, i, len| {
731            matches!(w.rich_text_component(), Some(RichTextComponent::Leaf { .. })) && filter(w, rect, i, len)
732        })
733    }
734}
735fn lines_overlap_strict(y_min1: Px, y_max1: Px, y_min2: Px, y_max2: Px) -> bool {
736    let (a_min, a_max) = if y_min1 <= y_max1 { (y_min1, y_max1) } else { (y_max1, y_min1) };
737    let (b_min, b_max) = if y_min2 <= y_max2 { (y_min2, y_max2) } else { (y_max2, y_min2) };
738
739    a_min < b_max && b_min < a_max
740}
741
742/// Info about how a rich text leaf defines new lines in a rich text.
743#[derive(Debug, Clone, PartialEq, Eq)]
744#[non_exhaustive]
745pub struct RichLineInfo {
746    /// Leaf widget first line height span does not intersect the previous sibling last line height span vertically.
747    ///
748    /// This heuristic allow multiple *baselines* in the same row (sub/superscript), it also allows bidi mixed segments that
749    /// maybe have negative horizontal offsets, but very custom layouts such as a diagonal stack panel may want to provide
750    /// their own definition of a *line* as an alternative to this API.
751    ///
752    /// Note that this can be `true` due to wrap layout, usually this is ignored when defining a *line* for selection. Check
753    /// `is_wrap_start` to implement *real lines*.
754    pub starts_new_line: bool,
755
756    /// if `starts_new_line` is `true` because the parent widget wrapped the leaf widget.
757    pub is_wrap_start: bool,
758
759    /// Leaf widget inline layout declared multiple lines so the end is in a new line.
760    ///
761    /// Note that the widget may define multiple other lines inside itself, those don't count as "rich text lines".
762    pub ends_in_new_line: bool,
763}
764
765// implemented here because there is a borrow checker limitation with `+'static`
766// that will only be fixed when `+use<>` is allowed in trait methods.
767fn rich_text_leaves_static(wgt: &WidgetInfo) -> impl Iterator<Item = WidgetInfo> + use<> {
768    wgt.descendants()
769        .filter(|w| matches!(w.rich_text_component(), Some(RichTextComponent::Leaf { .. })))
770}
771fn rich_text_leaves_rev_static(wgt: &WidgetInfo) -> impl Iterator<Item = WidgetInfo> + use<> {
772    wgt.descendants()
773        .tree_rev()
774        .filter(|w| matches!(w.rich_text_component(), Some(RichTextComponent::Leaf { .. })))
775}
776fn rich_text_selection_static(wgt: &WidgetInfo, a: WidgetId, b: WidgetId) -> impl ExactSizeIterator<Item = WidgetInfo> + use<> {
777    let mut ai = usize::MAX;
778    let mut bi = usize::MAX;
779
780    for (i, leaf) in wgt.rich_text_leaves().enumerate() {
781        let id = leaf.id();
782        if id == a {
783            ai = i;
784        }
785        if id == b {
786            bi = i;
787        }
788        if ai != usize::MAX && bi != usize::MAX {
789            break;
790        }
791    }
792
793    if ai > bi {
794        std::mem::swap(&mut ai, &mut bi);
795    }
796
797    let (skip, take) = if ai != usize::MAX && bi != usize::MAX {
798        (ai, bi - ai + 1)
799    } else {
800        (0, 0)
801    };
802
803    KnownLenIter {
804        take: rich_text_leaves_static(wgt).skip(skip).take(take),
805        len: take,
806    }
807}
808fn rich_text_selection_rev_static(wgt: &WidgetInfo, a: WidgetId, b: WidgetId) -> impl ExactSizeIterator<Item = WidgetInfo> + use<> {
809    let mut ai = usize::MAX;
810    let mut bi = usize::MAX;
811
812    for (i, leaf) in wgt.rich_text_leaves_rev().enumerate() {
813        let id = leaf.id();
814        if id == a {
815            ai = i;
816        } else if id == b {
817            bi = i;
818        }
819        if ai != usize::MAX && bi != usize::MAX {
820            break;
821        }
822    }
823
824    if ai > bi {
825        std::mem::swap(&mut ai, &mut bi);
826    }
827
828    let (skip, take) = if ai != usize::MAX && bi != usize::MAX {
829        (ai, bi - ai + 1)
830    } else {
831        (0, 0)
832    };
833
834    KnownLenIter {
835        take: rich_text_leaves_rev_static(wgt).skip(skip).take(take),
836        len: take,
837    }
838}
839
840/// Represents what kind of component the widget is in a rich text tree.
841#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
842pub enum RichTextComponent {
843    /// Outermost widget that is `rich_text` enabled.
844    Root,
845    /// Widget is `rich_text` enabled, but is inside another rich text tree.
846    Branch,
847    /// Widget is a text or custom component of the rich text.
848    Leaf {
849        /// Arbitrary identifier.
850        ///
851        /// Is `"text"` for `Text!` widgets.
852        kind: &'static str,
853    },
854}
855
856static_id! {
857    static ref RICH_TEXT_COMPONENT_ID: StateId<RichTextComponent>;
858}
859
860struct KnownLenIter<I> {
861    take: I,
862    len: usize,
863}
864impl<I: Iterator<Item = WidgetInfo>> Iterator for KnownLenIter<I> {
865    type Item = WidgetInfo;
866
867    fn next(&mut self) -> Option<Self::Item> {
868        match self.take.next() {
869            Some(r) => {
870                self.len -= 1;
871                Some(r)
872            }
873            None => None,
874        }
875    }
876
877    fn size_hint(&self) -> (usize, Option<usize>) {
878        (self.len, Some(self.len))
879    }
880}
881impl<I: Iterator<Item = WidgetInfo>> ExactSizeIterator for KnownLenIter<I> {}
882
883struct OptKnownLenIter<I> {
884    known_len_iter: I,
885}
886impl<I: Iterator<Item = WidgetInfo>> Iterator for OptKnownLenIter<I> {
887    type Item = WidgetInfo;
888
889    fn next(&mut self) -> Option<Self::Item> {
890        self.known_len_iter.next()
891    }
892
893    fn size_hint(&self) -> (usize, Option<usize>) {
894        // is either 0 from `None` or known len from `Some(KnownLenIter)`
895        self.known_len_iter.size_hint()
896    }
897}
898impl<I: Iterator<Item = WidgetInfo>> ExactSizeIterator for OptKnownLenIter<I> {}
899
900/// Parameter used in [`COPY_CMD`] requests sent from the rich text context to collect
901/// all selected text for copy.
902#[derive(Clone, Default)]
903pub struct RichTextCopyParam(Arc<Mutex<Option<Txt>>>);
904impl RichTextCopyParam {
905    /// Set the text fragment for the rich text copy.
906    pub fn set_text(&self, txt: impl Into<Txt>) {
907        *self.0.lock() = Some(txt.into());
908    }
909
910    /// Get the text fragment, if it was set.
911    pub fn into_text(self) -> Option<Txt> {
912        match Arc::try_unwrap(self.0) {
913            Ok(m) => m.into_inner(),
914            Err(c) => c.lock().clone(),
915        }
916    }
917}
918impl fmt::Debug for RichTextCopyParam {
919    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
920        if let Some(t) = self.0.try_lock() {
921            f.debug_tuple("RichTextCopyParam").field(&*t).finish()
922        } else {
923            f.debug_tuple("RichTextCopyParam").finish_non_exhaustive()
924        }
925    }
926}