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