zng_wgt_text/node/
rich.rs

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