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