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