Skip to main content

zng_wgt_text/
cmd.rs

1//! Commands that control the editable text.
2//!
3//! Most of the normal text editing is controlled by keyboard events, the [`EDIT_CMD`]
4//! command allows for arbitrary text editing without needing to simulate keyboard events.
5//!
6//! The [`node::resolve_text`] node implements [`EDIT_CMD`] when the text is editable.
7
8use std::{any::Any, borrow::Cow, cmp, fmt, ops, sync::Arc};
9
10use parking_lot::Mutex;
11use zng_ext_font::*;
12use zng_ext_l10n::l10n;
13use zng_ext_undo::*;
14use zng_layout::unit::DistanceKey;
15use zng_wgt::prelude::*;
16
17use crate::node::{RichText, RichTextWidgetInfoExt, notify_leaf_select_op};
18
19use super::{node::TEXT, *};
20
21command! {
22    /// Applies the [`TextEditOp`] into the text if it is editable.
23    ///
24    /// The request must be set as the command parameter.
25    pub static EDIT_CMD;
26
27    /// Applies the [`TextSelectOp`] into the text if it is editable.
28    ///
29    /// The request must be set as the command parameter.
30    pub static SELECT_CMD;
31
32    /// Select all text.
33    ///
34    /// The request is the same as [`SELECT_CMD`] with [`TextSelectOp::select_all`].
35    pub static SELECT_ALL_CMD {
36        l10n!: true,
37        name: "Select All",
38        shortcut: shortcut!(CTRL + 'A'),
39        shortcut_filter: ShortcutFilter::FOCUSED | ShortcutFilter::CMD_ENABLED,
40    };
41
42    /// Parse text and update value if [`txt_parse`] is pending.
43    ///
44    /// [`txt_parse`]: fn@super::txt_parse
45    pub static PARSE_CMD;
46}
47
48struct SharedTextEditOp {
49    data: Box<dyn Any + Send>,
50    op: Box<dyn FnMut(&mut dyn Any, UndoFullOp) + Send>,
51}
52
53/// Represents a text edit operation that can be send to an editable text using [`EDIT_CMD`].
54#[derive(Clone)]
55pub struct TextEditOp(Arc<Mutex<SharedTextEditOp>>);
56impl fmt::Debug for TextEditOp {
57    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58        f.debug_struct("TextEditOp").finish_non_exhaustive()
59    }
60}
61impl TextEditOp {
62    /// New text edit operation.
63    ///
64    /// The editable text widget that handles [`EDIT_CMD`] will call `op` during event handling in
65    /// the [`node::resolve_text`] context meaning the [`TEXT.resolved`] and [`TEXT.resolve_caret`] service is available in `op`.
66    /// The text is edited by modifying [`ResolvedText::txt`]. The text widget will detect changes to the caret and react s
67    /// accordingly (updating caret position and animation), the caret index is also snapped to the nearest grapheme start.
68    ///
69    /// The `op` arguments are a custom data `D` and what [`UndoFullOp`] to run, all
70    /// text edit operations must be undoable, first [`UndoOp::Redo`] is called to "do", then undo and redo again
71    /// if the user requests undo & redo. The text variable is always read-write when `op` is called, more than
72    /// one op can be called before the text variable updates, and [`ResolvedText::pending_edit`] is always false.
73    ///
74    /// [`ResolvedText::txt`]: crate::node::ResolvedText::txt
75    /// [`ResolvedText::caret`]: crate::node::ResolvedText::caret
76    /// [`ResolvedText::pending_edit`]: crate::node::ResolvedText::pending_edit
77    /// [`TEXT.resolved`]: crate::node::TEXT::resolved
78    /// [`TEXT.resolve_caret`]: crate::node::TEXT::resolve_caret
79    /// [`UndoFullOp`]: zng_ext_undo::UndoFullOp
80    /// [`UndoOp::Redo`]: zng_ext_undo::UndoOp::Redo
81    pub fn new<D>(data: D, mut op: impl FnMut(&mut D, UndoFullOp) + Send + 'static) -> Self
82    where
83        D: Send + Any + 'static,
84    {
85        Self(Arc::new(Mutex::new(SharedTextEditOp {
86            data: Box::new(data),
87            op: Box::new(move |data, o| op(data.downcast_mut().unwrap(), o)),
88        })))
89    }
90
91    /// Insert operation.
92    ///
93    /// The `insert` text is inserted at the current caret index or at `0`, or replaces the current selection,
94    /// after insert the caret is positioned after the inserted text.
95    pub fn insert(insert: impl Into<Txt>) -> Self {
96        struct InsertData {
97            insert: Txt,
98            selection_state: SelectionState,
99            removed: Txt,
100        }
101        let data = InsertData {
102            insert: insert.into(),
103            selection_state: SelectionState::PreInit,
104            removed: Txt::from_static(""),
105        };
106
107        Self::new(data, move |data, op| match op {
108            UndoFullOp::Init { redo } => {
109                let ctx = TEXT.resolved();
110                let caret = &ctx.caret;
111
112                let mut rmv_range = 0..0;
113
114                if let Some(range) = caret.selection_range() {
115                    rmv_range = range.start.index..range.end.index;
116
117                    ctx.txt.with(|t| {
118                        let r = &t[rmv_range.clone()];
119                        if r != data.removed {
120                            data.removed = Txt::from_str(r);
121                        }
122                    });
123
124                    if range.start.index == caret.index.unwrap_or(CaretIndex::ZERO).index {
125                        data.selection_state = SelectionState::CaretSelection(range.start, range.end);
126                    } else {
127                        data.selection_state = SelectionState::SelectionCaret(range.start, range.end);
128                    }
129                } else {
130                    data.selection_state = SelectionState::Caret(caret.index.unwrap_or(CaretIndex::ZERO));
131                }
132
133                Self::apply_max_count(redo, &ctx.txt, rmv_range, &mut data.insert)
134            }
135            UndoFullOp::Op(UndoOp::Redo) => {
136                let insert = &data.insert;
137
138                match data.selection_state {
139                    SelectionState::PreInit => unreachable!(),
140                    SelectionState::Caret(insert_idx) => {
141                        let i = insert_idx.index;
142                        TEXT.resolved().txt.modify(clmv!(insert, |args| {
143                            args.to_mut().insert_str(i, insert.as_str());
144                        }));
145
146                        let mut i = insert_idx;
147                        i.index += insert.len();
148
149                        let mut caret = TEXT.resolve_caret();
150                        caret.set_index(i);
151                        caret.clear_selection();
152                    }
153                    SelectionState::CaretSelection(start, end) | SelectionState::SelectionCaret(start, end) => {
154                        let char_range = start.index..end.index;
155                        TEXT.resolved().txt.modify(clmv!(insert, |args| {
156                            args.to_mut().replace_range(char_range, insert.as_str());
157                        }));
158
159                        let mut caret = TEXT.resolve_caret();
160                        caret.set_char_index(start.index + insert.len());
161                        caret.clear_selection();
162                    }
163                }
164            }
165            UndoFullOp::Op(UndoOp::Undo) => {
166                let len = data.insert.len();
167                let (insert_idx, selection_idx, caret_idx) = match data.selection_state {
168                    SelectionState::Caret(c) => (c, None, c),
169                    SelectionState::CaretSelection(start, end) => (start, Some(end), start),
170                    SelectionState::SelectionCaret(start, end) => (start, Some(start), end),
171                    SelectionState::PreInit => unreachable!(),
172                };
173                let i = insert_idx.index;
174                let removed = &data.removed;
175
176                TEXT.resolved().txt.modify(clmv!(removed, |args| {
177                    args.to_mut().replace_range(i..i + len, removed.as_str());
178                }));
179
180                let mut caret = TEXT.resolve_caret();
181                caret.set_index(caret_idx);
182                caret.selection_index = selection_idx;
183            }
184            UndoFullOp::Info { info } => {
185                let mut label = Txt::from_static("\"");
186                for (i, mut c) in data.insert.chars().take(21).enumerate() {
187                    if i == 20 {
188                        c = '…';
189                    } else if c == '\n' {
190                        c = '↵';
191                    } else if c == '\t' {
192                        c = '→';
193                    } else if c == '\r' {
194                        continue;
195                    }
196                    label.push(c);
197                }
198                label.push('"');
199                *info = Some(Arc::new(label));
200            }
201            UndoFullOp::Merge {
202                next_data,
203                within_undo_interval,
204                merged,
205                ..
206            } => {
207                if within_undo_interval
208                    && let Some(next_data) = next_data.downcast_mut::<InsertData>()
209                    && let SelectionState::Caret(mut after_idx) = data.selection_state
210                    && let SelectionState::Caret(caret) = next_data.selection_state
211                {
212                    after_idx.index += data.insert.len();
213
214                    if after_idx.index == caret.index {
215                        data.insert.push_str(&next_data.insert);
216                        *merged = true;
217                    }
218                }
219            }
220        })
221    }
222
223    /// Remove one *backspace range* ending at the caret index, or removes the selection.
224    ///
225    /// See [`SegmentedText::backspace_range`] for more details about what is removed.
226    ///
227    /// [`SegmentedText::backspace_range`]: zng_ext_font::SegmentedText::backspace_range
228    pub fn backspace() -> Self {
229        Self::backspace_impl(SegmentedText::backspace_range)
230    }
231    /// Remove one *backspace word range* ending at the caret index, or removes the selection.
232    ///
233    /// See [`SegmentedText::backspace_word_range`] for more details about what is removed.
234    ///
235    /// [`SegmentedText::backspace_word_range`]: zng_ext_font::SegmentedText::backspace_word_range
236    pub fn backspace_word() -> Self {
237        Self::backspace_impl(SegmentedText::backspace_word_range)
238    }
239    fn backspace_impl(backspace_range: fn(&SegmentedText, usize, u32) -> std::ops::Range<usize>) -> Self {
240        struct BackspaceData {
241            selection_state: SelectionState,
242            count: u32,
243            removed: Txt,
244        }
245        let data = BackspaceData {
246            selection_state: SelectionState::PreInit,
247            count: 1,
248            removed: Txt::from_static(""),
249        };
250
251        Self::new(data, move |data, op| match op {
252            UndoFullOp::Init { .. } => {
253                let ctx = TEXT.resolved();
254                let caret = &ctx.caret;
255
256                if let Some(range) = caret.selection_range() {
257                    if range.start.index == caret.index.unwrap_or(CaretIndex::ZERO).index {
258                        data.selection_state = SelectionState::CaretSelection(range.start, range.end);
259                    } else {
260                        data.selection_state = SelectionState::SelectionCaret(range.start, range.end);
261                    }
262                } else {
263                    data.selection_state = SelectionState::Caret(caret.index.unwrap_or(CaretIndex::ZERO));
264                }
265            }
266            UndoFullOp::Op(UndoOp::Redo) => {
267                let rmv = match data.selection_state {
268                    SelectionState::Caret(c) => backspace_range(&TEXT.resolved().segmented_text, c.index, data.count),
269                    SelectionState::CaretSelection(s, e) | SelectionState::SelectionCaret(s, e) => s.index..e.index,
270                    SelectionState::PreInit => unreachable!(),
271                };
272                if rmv.is_empty() {
273                    data.removed = Txt::from_static("");
274                    return;
275                }
276
277                {
278                    let mut caret = TEXT.resolve_caret();
279                    caret.set_char_index(rmv.start);
280                    caret.clear_selection();
281                }
282
283                let ctx = TEXT.resolved();
284                ctx.txt.with(|t| {
285                    let r = &t[rmv.clone()];
286                    if r != data.removed {
287                        data.removed = Txt::from_str(r);
288                    }
289                });
290
291                ctx.txt.modify(move |args| {
292                    args.to_mut().replace_range(rmv, "");
293                });
294            }
295            UndoFullOp::Op(UndoOp::Undo) => {
296                if data.removed.is_empty() {
297                    return;
298                }
299
300                let (insert_idx, selection_idx, caret_idx) = match data.selection_state {
301                    SelectionState::Caret(c) => (c.index - data.removed.len(), None, c),
302                    SelectionState::CaretSelection(s, e) => (s.index, Some(e), s),
303                    SelectionState::SelectionCaret(s, e) => (s.index, Some(s), e),
304                    SelectionState::PreInit => unreachable!(),
305                };
306                let removed = &data.removed;
307
308                TEXT.resolved().txt.modify(clmv!(removed, |args| {
309                    args.to_mut().insert_str(insert_idx, removed.as_str());
310                }));
311
312                let mut caret = TEXT.resolve_caret();
313                caret.set_index(caret_idx);
314                caret.selection_index = selection_idx;
315            }
316            UndoFullOp::Info { info } => {
317                *info = Some(if data.count == 1 {
318                    Arc::new("⌫")
319                } else {
320                    Arc::new(formatx!("⌫ (x{})", data.count))
321                })
322            }
323            UndoFullOp::Merge {
324                next_data,
325                within_undo_interval,
326                merged,
327                ..
328            } => {
329                if within_undo_interval
330                    && let Some(next_data) = next_data.downcast_mut::<BackspaceData>()
331                    && let SelectionState::Caret(mut after_idx) = data.selection_state
332                    && let SelectionState::Caret(caret) = next_data.selection_state
333                {
334                    after_idx.index -= data.removed.len();
335
336                    if after_idx.index == caret.index {
337                        data.count += next_data.count;
338
339                        next_data.removed.push_str(&data.removed);
340                        data.removed = std::mem::take(&mut next_data.removed);
341                        *merged = true;
342                    }
343                }
344            }
345        })
346    }
347
348    /// Remove one *delete range* starting at the caret index, or removes the selection.
349    ///
350    /// See [`SegmentedText::delete_range`] for more details about what is removed.
351    ///
352    /// [`SegmentedText::delete_range`]: zng_ext_font::SegmentedText::delete_range
353    pub fn delete() -> Self {
354        Self::delete_impl(SegmentedText::delete_range)
355    }
356    /// Remove one *delete word range* starting at the caret index, or removes the selection.
357    ///
358    /// See [`SegmentedText::delete_word_range`] for more details about what is removed.
359    ///
360    /// [`SegmentedText::delete_word_range`]: zng_ext_font::SegmentedText::delete_word_range
361    pub fn delete_word() -> Self {
362        Self::delete_impl(SegmentedText::delete_word_range)
363    }
364    fn delete_impl(delete_range: fn(&SegmentedText, usize, u32) -> std::ops::Range<usize>) -> Self {
365        struct DeleteData {
366            selection_state: SelectionState,
367            count: u32,
368            removed: Txt,
369        }
370        let data = DeleteData {
371            selection_state: SelectionState::PreInit,
372            count: 1,
373            removed: Txt::from_static(""),
374        };
375
376        Self::new(data, move |data, op| match op {
377            UndoFullOp::Init { .. } => {
378                let ctx = TEXT.resolved();
379                let caret = &ctx.caret;
380
381                if let Some(range) = caret.selection_range() {
382                    if range.start.index == caret.index.unwrap_or(CaretIndex::ZERO).index {
383                        data.selection_state = SelectionState::CaretSelection(range.start, range.end);
384                    } else {
385                        data.selection_state = SelectionState::SelectionCaret(range.start, range.end);
386                    }
387                } else {
388                    data.selection_state = SelectionState::Caret(caret.index.unwrap_or(CaretIndex::ZERO));
389                }
390            }
391            UndoFullOp::Op(UndoOp::Redo) => {
392                let rmv = match data.selection_state {
393                    SelectionState::CaretSelection(s, e) | SelectionState::SelectionCaret(s, e) => s.index..e.index,
394                    SelectionState::Caret(c) => delete_range(&TEXT.resolved().segmented_text, c.index, data.count),
395                    SelectionState::PreInit => unreachable!(),
396                };
397
398                if rmv.is_empty() {
399                    data.removed = Txt::from_static("");
400                    return;
401                }
402
403                {
404                    let mut caret = TEXT.resolve_caret();
405                    caret.set_char_index(rmv.start); // (re)start caret animation
406                    caret.clear_selection();
407                }
408
409                let ctx = TEXT.resolved();
410                ctx.txt.with(|t| {
411                    let r = &t[rmv.clone()];
412                    if r != data.removed {
413                        data.removed = Txt::from_str(r);
414                    }
415                });
416                ctx.txt.modify(move |args| {
417                    args.to_mut().replace_range(rmv, "");
418                });
419            }
420            UndoFullOp::Op(UndoOp::Undo) => {
421                let removed = &data.removed;
422
423                if data.removed.is_empty() {
424                    return;
425                }
426
427                let (insert_idx, selection_idx, caret_idx) = match data.selection_state {
428                    SelectionState::Caret(c) => (c.index, None, c),
429                    SelectionState::CaretSelection(s, e) => (s.index, Some(e), s),
430                    SelectionState::SelectionCaret(s, e) => (s.index, Some(s), e),
431                    SelectionState::PreInit => unreachable!(),
432                };
433
434                TEXT.resolved().txt.modify(clmv!(removed, |args| {
435                    args.to_mut().insert_str(insert_idx, removed.as_str());
436                }));
437
438                let mut caret = TEXT.resolve_caret();
439                caret.set_index(caret_idx); // (re)start caret animation
440                caret.selection_index = selection_idx;
441            }
442            UndoFullOp::Info { info } => {
443                *info = Some(if data.count == 1 {
444                    Arc::new("⌦")
445                } else {
446                    Arc::new(formatx!("⌦ (x{})", data.count))
447                })
448            }
449            UndoFullOp::Merge {
450                next_data,
451                within_undo_interval,
452                merged,
453                ..
454            } => {
455                if within_undo_interval
456                    && let Some(next_data) = next_data.downcast_ref::<DeleteData>()
457                    && let SelectionState::Caret(after_idx) = data.selection_state
458                    && let SelectionState::Caret(caret) = next_data.selection_state
459                    && after_idx.index == caret.index
460                {
461                    data.count += next_data.count;
462                    data.removed.push_str(&next_data.removed);
463                    *merged = true;
464                }
465            }
466        })
467    }
468
469    fn apply_max_count(redo: &mut bool, txt: &Var<Txt>, rmv_range: ops::Range<usize>, insert: &mut Txt) {
470        let max_count = MAX_CHARS_COUNT_VAR.get();
471        if max_count > 0 {
472            // max count enabled
473            let (txt_count, rmv_count) = txt.with(|t| (t.chars().count(), t[rmv_range].chars().count()));
474            let ins_count = insert.chars().count();
475
476            let final_count = txt_count - rmv_count + ins_count;
477            if final_count > max_count {
478                // need to truncate insert
479                let ins_rmv = final_count - max_count;
480                if ins_rmv < ins_count {
481                    // can truncate insert
482                    let i = insert.char_indices().nth(ins_count - ins_rmv).unwrap().0;
483                    insert.truncate(i);
484                } else {
485                    // cannot insert
486                    debug_assert!(txt_count >= max_count);
487                    *redo = false;
488                }
489            }
490        }
491    }
492
493    /// Remove all the text.
494    pub fn clear() -> Self {
495        #[derive(Default, Clone)]
496        struct Cleared {
497            txt: Txt,
498            selection: SelectionState,
499        }
500        Self::new(Cleared::default(), |data, op| match op {
501            UndoFullOp::Init { .. } => {
502                let ctx = TEXT.resolved();
503                data.txt = ctx.txt.get();
504                if let Some(range) = ctx.caret.selection_range() {
505                    if range.start.index == ctx.caret.index.unwrap_or(CaretIndex::ZERO).index {
506                        data.selection = SelectionState::CaretSelection(range.start, range.end);
507                    } else {
508                        data.selection = SelectionState::SelectionCaret(range.start, range.end);
509                    }
510                } else {
511                    data.selection = SelectionState::Caret(ctx.caret.index.unwrap_or(CaretIndex::ZERO));
512                };
513            }
514            UndoFullOp::Op(UndoOp::Redo) => {
515                TEXT.resolved().txt.set("");
516            }
517            UndoFullOp::Op(UndoOp::Undo) => {
518                TEXT.resolved().txt.set(data.txt.clone());
519
520                let (selection_idx, caret_idx) = match data.selection {
521                    SelectionState::Caret(c) => (None, c),
522                    SelectionState::CaretSelection(s, e) => (Some(e), s),
523                    SelectionState::SelectionCaret(s, e) => (Some(s), e),
524                    SelectionState::PreInit => unreachable!(),
525                };
526                let mut caret = TEXT.resolve_caret();
527                caret.set_index(caret_idx); // (re)start caret animation
528                caret.selection_index = selection_idx;
529            }
530            UndoFullOp::Info { info } => *info = Some(Arc::new(l10n!("text-edit-op.clear", "clear").get())),
531            UndoFullOp::Merge {
532                next_data,
533                within_undo_interval,
534                merged,
535                ..
536            } => *merged = within_undo_interval && next_data.is::<Cleared>(),
537        })
538    }
539
540    /// Replace operation.
541    ///
542    /// The `select_before` is removed, and `insert` inserted at the `select_before.start`, after insertion
543    /// the `select_after` is applied, you can use an empty insert to just remove.
544    ///
545    /// All indexes are snapped to the nearest grapheme, you can use empty ranges to just position the caret.
546    pub fn replace(mut select_before: ops::Range<usize>, insert: impl Into<Txt>, mut select_after: ops::Range<usize>) -> Self {
547        let mut insert = insert.into();
548        let mut removed = Txt::from_static("");
549
550        Self::new((), move |_, op| match op {
551            UndoFullOp::Init { redo } => {
552                let ctx = TEXT.resolved();
553
554                select_before.start = ctx.segmented_text.snap_grapheme_boundary(select_before.start);
555                select_before.end = ctx.segmented_text.snap_grapheme_boundary(select_before.end);
556
557                ctx.txt.with(|t| {
558                    removed = Txt::from_str(&t[select_before.clone()]);
559                });
560
561                Self::apply_max_count(redo, &ctx.txt, select_before.clone(), &mut insert);
562            }
563            UndoFullOp::Op(UndoOp::Redo) => {
564                TEXT.resolved().txt.modify(clmv!(select_before, insert, |args| {
565                    args.to_mut().replace_range(select_before, insert.as_str());
566                }));
567
568                TEXT.resolve_caret().set_char_selection(select_after.start, select_after.end);
569            }
570            UndoFullOp::Op(UndoOp::Undo) => {
571                let ctx = TEXT.resolved();
572
573                select_after.start = ctx.segmented_text.snap_grapheme_boundary(select_after.start);
574                select_after.end = ctx.segmented_text.snap_grapheme_boundary(select_after.end);
575
576                ctx.txt.modify(clmv!(select_after, removed, |args| {
577                    args.to_mut().replace_range(select_after, removed.as_str());
578                }));
579
580                drop(ctx);
581                TEXT.resolve_caret().set_char_selection(select_before.start, select_before.end);
582            }
583            UndoFullOp::Info { info } => *info = Some(Arc::new(l10n!("text-edit-op.replace", "replace").get())),
584            UndoFullOp::Merge { .. } => {}
585        })
586    }
587
588    /// Applies [`TEXT_TRANSFORM_VAR`] and [`WHITE_SPACE_VAR`] to the text.
589    pub fn apply_transforms() -> Self {
590        let mut prev = Txt::from_static("");
591        let mut transform = None::<(TextTransformFn, WhiteSpace)>;
592        Self::new((), move |_, op| match op {
593            UndoFullOp::Init { .. } => {}
594            UndoFullOp::Op(UndoOp::Redo) => {
595                let (t, w) = transform.get_or_insert_with(|| (TEXT_TRANSFORM_VAR.get(), WHITE_SPACE_VAR.get()));
596
597                let ctx = TEXT.resolved();
598
599                let new_txt = ctx.txt.with(|txt| {
600                    let transformed = t.transform(txt);
601                    let white_spaced = w.transform(transformed.as_ref());
602                    if let Cow::Owned(w) = white_spaced {
603                        Some(w)
604                    } else if let Cow::Owned(t) = transformed {
605                        Some(t)
606                    } else {
607                        None
608                    }
609                });
610
611                if let Some(t) = new_txt {
612                    if ctx.txt.with(|t| t != prev.as_str()) {
613                        prev = ctx.txt.get();
614                    }
615                    ctx.txt.set(t);
616                }
617            }
618            UndoFullOp::Op(UndoOp::Undo) => {
619                let ctx = TEXT.resolved();
620
621                if ctx.txt.with(|t| t != prev.as_str()) {
622                    ctx.txt.set(prev.clone());
623                }
624            }
625            UndoFullOp::Info { info } => *info = Some(Arc::new(l10n!("text-edit-op.transform", "transform").get())),
626            UndoFullOp::Merge { .. } => {}
627        })
628    }
629
630    fn call(self) -> bool {
631        {
632            let mut op = self.0.lock();
633            let op = &mut *op;
634
635            let mut redo = true;
636            (op.op)(&mut *op.data, UndoFullOp::Init { redo: &mut redo });
637            if !redo {
638                return false;
639            }
640
641            (op.op)(&mut *op.data, UndoFullOp::Op(UndoOp::Redo));
642        }
643
644        if !OBSCURE_TXT_VAR.get() {
645            UNDO.register(UndoTextEditOp::new(self));
646        }
647        true
648    }
649
650    pub(super) fn call_edit_op(self) {
651        let registered = self.call();
652        if registered && !TEXT.resolved().pending_edit {
653            TEXT.resolve().pending_edit = true;
654            WIDGET.update(); // in case the edit does not actually change the text.
655        }
656    }
657}
658/// Used by `TextEditOp::insert`, `backspace` and `delete`.
659#[derive(Clone, Copy, Default)]
660enum SelectionState {
661    #[default]
662    PreInit,
663    Caret(CaretIndex),
664    CaretSelection(CaretIndex, CaretIndex),
665    SelectionCaret(CaretIndex, CaretIndex),
666}
667
668/// Parameter for [`EDIT_CMD`], apply the request and don't register undo.
669#[derive(Debug, Clone)]
670pub(super) struct UndoTextEditOp {
671    pub target: WidgetId,
672    edit_op: TextEditOp,
673    exec_op: UndoOp,
674}
675impl UndoTextEditOp {
676    fn new(edit_op: TextEditOp) -> Self {
677        Self {
678            target: WIDGET.id(),
679            edit_op,
680            exec_op: UndoOp::Undo,
681        }
682    }
683
684    pub(super) fn call(&self) {
685        let mut op = self.edit_op.0.lock();
686        let op = &mut *op;
687        (op.op)(&mut *op.data, UndoFullOp::Op(self.exec_op))
688    }
689}
690impl UndoAction for UndoTextEditOp {
691    fn undo(self: Box<Self>) -> Box<dyn RedoAction> {
692        EDIT_CMD.scoped(self.target).notify_param(Self {
693            target: self.target,
694            edit_op: self.edit_op.clone(),
695            exec_op: UndoOp::Undo,
696        });
697        self
698    }
699
700    fn info(&mut self) -> Arc<dyn UndoInfo> {
701        let mut op = self.edit_op.0.lock();
702        let op = &mut *op;
703        let mut info = None;
704        (op.op)(&mut *op.data, UndoFullOp::Info { info: &mut info });
705
706        info.unwrap_or_else(|| Arc::new(l10n!("text-edit-op.generic", "text edit").get()))
707    }
708
709    fn as_any(&mut self) -> &mut dyn std::any::Any {
710        self
711    }
712
713    fn merge(self: Box<Self>, mut args: UndoActionMergeArgs) -> Result<Box<dyn UndoAction>, (Box<dyn UndoAction>, Box<dyn UndoAction>)> {
714        if let Some(next) = args.next.as_any().downcast_mut::<Self>() {
715            let mut merged = false;
716
717            {
718                let mut op = self.edit_op.0.lock();
719                let op = &mut *op;
720
721                let mut next_op = next.edit_op.0.lock();
722
723                (op.op)(
724                    &mut *op.data,
725                    UndoFullOp::Merge {
726                        next_data: &mut *next_op.data,
727                        prev_timestamp: args.prev_timestamp,
728                        within_undo_interval: args.within_undo_interval,
729                        merged: &mut merged,
730                    },
731                );
732            }
733
734            if merged {
735                return Ok(self);
736            }
737        }
738
739        Err((self, args.next))
740    }
741}
742impl RedoAction for UndoTextEditOp {
743    fn redo(self: Box<Self>) -> Box<dyn UndoAction> {
744        EDIT_CMD.scoped(self.target).notify_param(Self {
745            target: self.target,
746            edit_op: self.edit_op.clone(),
747            exec_op: UndoOp::Redo,
748        });
749        self
750    }
751
752    fn info(&mut self) -> Arc<dyn UndoInfo> {
753        let mut op = self.edit_op.0.lock();
754        let op = &mut *op;
755        let mut info = None;
756        (op.op)(&mut *op.data, UndoFullOp::Info { info: &mut info });
757
758        info.unwrap_or_else(|| Arc::new(l10n!("text-edit-op.generic", "text edit").get()))
759    }
760}
761
762/// Represents a text caret/selection operation that can be send to an editable text using [`SELECT_CMD`].
763///
764/// The provided operations work in rich texts by default, unless they are named with prefix `local_`. In
765/// rich text contexts the operation may generate other `SELECT_CMD` requests as it propagates to all involved component texts.
766/// The `local_` operations are for use by other operations only, direct use inside rich text requires updating the rich text state
767/// to match.
768#[derive(Clone)]
769pub struct TextSelectOp {
770    op: Arc<Mutex<dyn FnMut() + Send>>,
771}
772impl fmt::Debug for TextSelectOp {
773    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
774        f.debug_struct("TextSelectOp").finish_non_exhaustive()
775    }
776}
777impl TextSelectOp {
778    /// Clear selection and move the caret to the next insert index.
779    ///
780    /// This is the `Right` key operation.
781    pub fn next() -> Self {
782        rich_clear_next_prev(true, false)
783    }
784
785    /// Extend or shrink selection by moving the caret to the next insert index.
786    ///
787    /// This is the `SHIFT+Right` key operation.
788    pub fn select_next() -> Self {
789        rich_select_next_prev(true, false)
790    }
791
792    /// Clear selection and move the caret to the previous insert index.
793    ///
794    /// This is the `Left` key operation.
795    pub fn prev() -> Self {
796        rich_clear_next_prev(false, false)
797    }
798
799    /// Extend or shrink selection by moving the caret to the previous insert index.
800    ///
801    /// This is the `SHIFT+Left` key operation.
802    pub fn select_prev() -> Self {
803        rich_select_next_prev(false, false)
804    }
805
806    /// Clear selection and move the caret to the next word insert index.
807    ///
808    /// This is the `CTRL+Right` shortcut operation.
809    pub fn next_word() -> Self {
810        rich_clear_next_prev(true, true)
811    }
812    /// Extend or shrink selection by moving the caret to the next word insert index.
813    ///
814    /// This is the `CTRL+SHIFT+Right` shortcut operation.
815    pub fn select_next_word() -> Self {
816        rich_select_next_prev(true, true)
817    }
818    /// Clear selection and move the caret to the previous word insert index.
819    ///
820    /// This is the `CTRL+Left` shortcut operation.
821    pub fn prev_word() -> Self {
822        rich_clear_next_prev(false, true)
823    }
824
825    /// Extend or shrink selection by moving the caret to the previous word insert index.
826    ///
827    /// This is the `CTRL+SHIFT+Left` shortcut operation.
828    pub fn select_prev_word() -> Self {
829        rich_select_next_prev(false, true)
830    }
831
832    /// Clear selection and move the caret to the nearest insert index on the previous line.
833    ///
834    /// This is the `Up` key operation.
835    pub fn line_up() -> Self {
836        rich_up_down(true, false, false)
837    }
838
839    /// Extend or shrink selection by moving the caret to the nearest insert index on the previous line.
840    ///
841    /// This is the `SHIFT+Up` key operation.
842    pub fn select_line_up() -> Self {
843        rich_up_down(false, false, false)
844    }
845
846    /// Clear selection and move the caret to the nearest insert index on the next line.
847    ///
848    /// This is the `Down` key operation.
849    pub fn line_down() -> Self {
850        rich_up_down(true, true, false)
851    }
852
853    /// Extend or shrink selection by moving the caret to the nearest insert index on the next line.
854    ///
855    /// This is the `SHIFT+Down` key operation.
856    pub fn select_line_down() -> Self {
857        rich_up_down(false, true, false)
858    }
859
860    /// Clear selection and move the caret one viewport up.
861    ///
862    /// This is the `PageUp` key operation.
863    pub fn page_up() -> Self {
864        rich_up_down(true, false, true)
865    }
866
867    /// Extend or shrink selection by moving the caret one viewport up.
868    ///
869    /// This is the `SHIFT+PageUp` key operation.
870    pub fn select_page_up() -> Self {
871        rich_up_down(false, false, true)
872    }
873
874    /// Clear selection and move the caret one viewport down.
875    ///
876    /// This is the `PageDown` key operation.
877    pub fn page_down() -> Self {
878        rich_up_down(true, true, true)
879    }
880
881    /// Extend or shrink selection by moving the caret one viewport down.
882    ///
883    /// This is the `SHIFT+PageDown` key operation.
884    pub fn select_page_down() -> Self {
885        rich_up_down(false, true, true)
886    }
887
888    /// Clear selection and move the caret to the start of the line.
889    ///
890    /// This is the `Home` key operation.
891    pub fn line_start() -> Self {
892        rich_line_start_end(true, false)
893    }
894
895    /// Extend or shrink selection by moving the caret to the start of the line.
896    ///
897    /// This is the `SHIFT+Home` key operation.
898    pub fn select_line_start() -> Self {
899        rich_line_start_end(false, false)
900    }
901
902    /// Clear selection and move the caret to the end of the line (before the line-break if any).
903    ///
904    /// This is the `End` key operation.
905    pub fn line_end() -> Self {
906        rich_line_start_end(true, true)
907    }
908
909    /// Extend or shrink selection by moving the caret to the end of the line (before the line-break if any).
910    ///
911    /// This is the `SHIFT+End` key operation.
912    pub fn select_line_end() -> Self {
913        rich_line_start_end(false, true)
914    }
915
916    /// Clear selection and move the caret to the text start.
917    ///
918    /// This is the `CTRL+Home` shortcut operation.
919    pub fn text_start() -> Self {
920        rich_text_start_end(true, false)
921    }
922
923    /// Extend or shrink selection by moving the caret to the text start.
924    ///
925    /// This is the `CTRL+SHIFT+Home` shortcut operation.
926    pub fn select_text_start() -> Self {
927        rich_text_start_end(false, false)
928    }
929
930    /// Clear selection and move the caret to the text end.
931    ///
932    /// This is the `CTRL+End` shortcut operation.
933    pub fn text_end() -> Self {
934        rich_text_start_end(true, true)
935    }
936
937    /// Extend or shrink selection by moving the caret to the text end.
938    ///
939    /// This is the `CTRL+SHIFT+End` shortcut operation.
940    pub fn select_text_end() -> Self {
941        rich_text_start_end(false, true)
942    }
943
944    /// Clear selection and move the caret to the insert point nearest to the `window_point`.
945    ///
946    /// This is the mouse primary button down operation.
947    pub fn nearest_to(window_point: DipPoint) -> Self {
948        rich_nearest_char_word_to(true, window_point, false)
949    }
950
951    /// Extend or shrink selection by moving the caret to the insert point nearest to the `window_point`.
952    ///
953    /// This is the mouse primary button down when holding SHIFT operation.
954    pub fn select_nearest_to(window_point: DipPoint) -> Self {
955        rich_nearest_char_word_to(false, window_point, false)
956    }
957
958    /// Replace or extend selection with the word nearest to the `window_point`
959    ///
960    /// This is the mouse primary button double click.
961    pub fn select_word_nearest_to(replace_selection: bool, window_point: DipPoint) -> Self {
962        rich_nearest_char_word_to(replace_selection, window_point, true)
963    }
964
965    /// Replace or extend selection with the line nearest to the `window_point`
966    ///
967    /// This is the mouse primary button triple click.
968    pub fn select_line_nearest_to(replace_selection: bool, window_point: DipPoint) -> Self {
969        rich_nearest_line_to(replace_selection, window_point)
970    }
971
972    /// Extend or shrink selection by moving the caret index or caret selection index to the insert point nearest to `window_point`.
973    ///
974    /// This is the touch selection caret drag operation.
975    pub fn select_index_nearest_to(window_point: DipPoint, move_selection_index: bool) -> Self {
976        rich_selection_index_nearest_to(window_point, move_selection_index)
977    }
978
979    /// Select the full text.
980    pub fn select_all() -> Self {
981        Self::new_rich(
982            |ctx| (ctx.leaves_rev().next().map(|w| w.id()).unwrap_or_else(|| WIDGET.id()), ()),
983            |()| {
984                TEXT.resolve_caret().skip_next_scroll = true;
985                (
986                    CaretIndex {
987                        index: TEXT.resolved().segmented_text.text().len(),
988                        line: 0,
989                    },
990                    (),
991                )
992            },
993            |ctx, ()| Some((ctx.leaves().next().map(|w| w.id()).unwrap_or_else(|| WIDGET.id()), ())),
994            |()| {
995                TEXT.resolve_caret().skip_next_scroll = true;
996                Some(CaretIndex::ZERO)
997            },
998        )
999    }
1000
1001    /// Clear selection and keep the caret at the same position.
1002    ///
1003    /// This is the `Esc` shortcut operation.
1004    pub fn clear_selection() -> Self {
1005        Self::new_rich(
1006            |ctx| (ctx.caret.index.unwrap_or_else(|| WIDGET.id()), ()),
1007            |()| (TEXT.resolved().caret.index.unwrap_or(CaretIndex::ZERO), ()),
1008            |_, ()| None,
1009            |()| None,
1010        )
1011    }
1012}
1013
1014/// Operations that ignore the rich text context, for internal use only.
1015impl TextSelectOp {
1016    /// Like [`next`] but stays within the same text widget, ignores rich text context.
1017    ///
1018    /// [`next`]: Self::next
1019    pub fn local_next() -> Self {
1020        Self::new(|| {
1021            local_clear_next_prev(true, false);
1022        })
1023    }
1024
1025    /// Like [`select_next`] but stays within the same text widget, ignores rich text context.
1026    ///
1027    /// [`select_next`]: Self::select_next
1028    pub fn local_select_next() -> Self {
1029        Self::new(|| {
1030            local_select_next_prev(true, false);
1031        })
1032    }
1033
1034    /// Like [`prev`] but stays within the same text widget, ignores rich text context.
1035    ///
1036    /// [`prev`]: Self::prev
1037    pub fn local_prev() -> Self {
1038        Self::new(|| {
1039            local_clear_next_prev(false, false);
1040        })
1041    }
1042
1043    /// Like [`select_prev`] but stays within the same text widget, ignores rich text context.
1044    ///
1045    /// [`select_prev`]: Self::select_prev
1046    pub fn local_select_prev() -> Self {
1047        Self::new(|| {
1048            local_select_next_prev(false, false);
1049        })
1050    }
1051
1052    /// Like [`next_word`] but stays within the same text widget, ignores rich text context.
1053    ///
1054    /// [`next_word`]: Self::next_word
1055    pub fn local_next_word() -> Self {
1056        Self::new(|| {
1057            local_clear_next_prev(true, true);
1058        })
1059    }
1060
1061    /// Like [`select_next_word`] but stays within the same text widget, ignores rich text context.
1062    ///
1063    /// [`select_next_word`]: Self::select_next_word
1064    pub fn local_select_next_word() -> Self {
1065        Self::new(|| {
1066            local_select_next_prev(true, true);
1067        })
1068    }
1069
1070    /// Like [`prev_word`] but stays within the same text widget, ignores rich text context.
1071    ///
1072    /// [`prev_word`]: Self::prev_word
1073    pub fn local_prev_word() -> Self {
1074        Self::new(|| {
1075            local_clear_next_prev(false, true);
1076        })
1077    }
1078
1079    /// Like [`select_prev_word`] but stays within the same text widget, ignores rich text context.
1080    ///
1081    /// [`select_prev_word`]: Self::select_prev_word
1082    pub fn local_select_prev_word() -> Self {
1083        Self::new(|| {
1084            local_select_next_prev(false, true);
1085        })
1086    }
1087
1088    /// Like [`line_start`] but stays within the same text widget, ignores rich text context.
1089    ///
1090    /// [`line_start`]: Self::line_start
1091    pub fn local_line_start() -> Self {
1092        Self::new(|| local_line_start_end(true, false))
1093    }
1094
1095    /// Like [`select_line_start`] but stays within the same text widget, ignores rich text context.
1096    ///
1097    /// [`select_line_start`]: Self::select_line_start
1098    pub fn local_select_line_start() -> Self {
1099        Self::new(|| local_line_start_end(false, false))
1100    }
1101
1102    /// Like [`line_end`] but stays within the same text widget, ignores rich text context.
1103    ///
1104    /// [`line_end`]: Self::line_end
1105    pub fn local_line_end() -> Self {
1106        Self::new(|| local_line_start_end(true, true))
1107    }
1108
1109    /// Like [`select_line_end`] but stays within the same text widget, ignores rich text context.
1110    ///
1111    /// [`select_line_end`]: Self::select_line_end
1112    pub fn local_select_line_end() -> Self {
1113        Self::new(|| local_line_start_end(false, true))
1114    }
1115
1116    /// Like [`text_start`] but stays within the same text widget, ignores rich text context.
1117    ///
1118    /// [`text_start`]: Self::text_start
1119    pub fn local_text_start() -> Self {
1120        Self::new(|| local_text_start_end(true, false))
1121    }
1122
1123    /// Like [`select_text_start`] but stays within the same text widget, ignores rich text context.
1124    ///
1125    /// [`select_text_start`]: Self::select_text_start
1126    pub fn local_select_text_start() -> Self {
1127        Self::new(|| local_text_start_end(false, false))
1128    }
1129
1130    /// Like [`text_end`] but stays within the same text widget, ignores rich text context.
1131    ///
1132    /// [`text_end`]: Self::text_end
1133    pub fn local_text_end() -> Self {
1134        Self::new(|| local_text_start_end(true, true))
1135    }
1136
1137    /// Like [`select_text_end`] but stays within the same text widget, ignores rich text context.
1138    ///
1139    /// [`select_text_end`]: Self::select_text_end
1140    pub fn local_select_text_end() -> Self {
1141        Self::new(|| local_text_start_end(false, true))
1142    }
1143
1144    /// Like [`select_all`]  but stays within the same text widget, ignores rich text context.
1145    ///
1146    /// [`select_all`]: Self::select_all
1147    pub fn local_select_all() -> Self {
1148        Self::new(|| {
1149            let len = TEXT.resolved().segmented_text.text().len();
1150            let mut caret = TEXT.resolve_caret();
1151            caret.set_char_selection(0, len);
1152            caret.skip_next_scroll = true;
1153        })
1154    }
1155
1156    /// Like [`clear_selection`]  but stays within the same text widget, ignores rich text context.
1157    ///
1158    /// [`clear_selection`]: Self::clear_selection
1159    pub fn local_clear_selection() -> Self {
1160        Self::new(|| {
1161            let mut ctx = TEXT.resolve_caret();
1162            ctx.clear_selection();
1163        })
1164    }
1165
1166    /// Like [`line_up`]  but stays within the same text widget, ignores rich text context.
1167    ///
1168    /// [`line_up`]: Self::line_up
1169    pub fn local_line_up() -> Self {
1170        Self::new(|| local_line_up_down(true, -1))
1171    }
1172
1173    /// Like [`select_line_up`]  but stays within the same text widget, ignores rich text context.
1174    ///
1175    /// [`select_line_up`]: Self::select_line_up
1176    pub fn local_select_line_up() -> Self {
1177        Self::new(|| local_line_up_down(false, -1))
1178    }
1179
1180    /// Like [`line_down`]  but stays within the same text widget, ignores rich text context.
1181    ///
1182    /// [`line_down`]: Self::line_down
1183    pub fn local_line_down() -> Self {
1184        Self::new(|| local_line_up_down(true, 1))
1185    }
1186
1187    /// Like [`select_line_down`]  but stays within the same text widget, ignores rich text context.
1188    ///
1189    /// [`select_line_down`]: Self::select_line_down
1190    pub fn local_select_line_down() -> Self {
1191        Self::new(|| local_line_up_down(false, 1))
1192    }
1193
1194    /// Like [`page_up`]  but stays within the same text widget, ignores rich text context.
1195    ///
1196    /// [`page_up`]: Self::page_up
1197    pub fn local_page_up() -> Self {
1198        Self::new(|| local_page_up_down(true, -1))
1199    }
1200
1201    /// Like [`select_page_up`]  but stays within the same text widget, ignores rich text context.
1202    ///
1203    /// [`select_page_up`]: Self::select_page_up
1204    pub fn local_select_page_up() -> Self {
1205        Self::new(|| local_page_up_down(false, -1))
1206    }
1207
1208    /// Like [`page_down`]  but stays within the same text widget, ignores rich text context.
1209    ///
1210    /// [`page_down`]: Self::page_down
1211    pub fn local_page_down() -> Self {
1212        Self::new(|| local_page_up_down(true, 1))
1213    }
1214
1215    /// Like [`select_page_down`]  but stays within the same text widget, ignores rich text context.
1216    ///
1217    /// [`select_page_down`]: Self::select_page_down
1218    pub fn local_select_page_down() -> Self {
1219        Self::new(|| local_page_up_down(false, 1))
1220    }
1221
1222    /// Like [`nearest_to`]  but stays within the same text widget, ignores rich text context.
1223    ///
1224    /// [`nearest_to`]: Self::nearest_to
1225    pub fn local_nearest_to(window_point: DipPoint) -> Self {
1226        Self::new(move || {
1227            local_nearest_to(true, window_point);
1228        })
1229    }
1230
1231    /// Like [`select_nearest_to`]  but stays within the same text widget, ignores rich text context.
1232    ///
1233    /// [`select_nearest_to`]: Self::select_nearest_to
1234    pub fn local_select_nearest_to(window_point: DipPoint) -> Self {
1235        Self::new(move || {
1236            local_nearest_to(false, window_point);
1237        })
1238    }
1239
1240    /// Like [`select_index_nearest_to`]  but stays within the same text widget, ignores rich text context.
1241    ///
1242    /// [`select_index_nearest_to`]: Self::select_index_nearest_to
1243    pub fn local_select_index_nearest_to(window_point: DipPoint, move_selection_index: bool) -> Self {
1244        Self::new(move || {
1245            local_select_index_nearest_to(window_point, move_selection_index);
1246        })
1247    }
1248
1249    /// Like [`select_word_nearest_to`]  but stays within the same text widget, ignores rich text context.
1250    ///
1251    /// [`select_word_nearest_to`]: Self::select_word_nearest_to
1252    pub fn local_select_word_nearest_to(replace_selection: bool, window_point: DipPoint) -> Self {
1253        Self::new(move || local_select_line_word_nearest_to(replace_selection, true, window_point))
1254    }
1255
1256    /// Like [`select_line_nearest_to`]  but stays within the same text widget, ignores rich text context.
1257    ///
1258    /// [`select_line_nearest_to`]: Self::select_line_nearest_to
1259    pub fn local_select_line_nearest_to(replace_selection: bool, window_point: DipPoint) -> Self {
1260        Self::new(move || local_select_line_word_nearest_to(replace_selection, false, window_point))
1261    }
1262}
1263
1264impl TextSelectOp {
1265    /// New text select operation.
1266    ///
1267    /// The editable text widget that handles [`SELECT_CMD`] will call `op` during event handling in
1268    /// the [`node::layout_text`] context. You can position the caret using [`TEXT.resolve_caret`] and [`TEXT.resolve_rich_caret`],
1269    /// the text widget will detect changes to it and react accordingly (updating caret position and animation),
1270    /// the caret index is also snapped to the nearest grapheme start.
1271    ///
1272    /// [`TEXT.resolve_caret`]: super::node::TEXT::resolve_caret
1273    /// [`TEXT.resolve_rich_caret`]: super::node::TEXT::resolve_rich_caret
1274    pub fn new(op: impl FnMut() + Send + 'static) -> Self {
1275        Self {
1276            op: Arc::new(Mutex::new(op)),
1277        }
1278    }
1279
1280    /// New text selection operation with helpers for implementing rich selection.
1281    ///
1282    /// The input closures are:
1283    ///
1284    /// * `rich_caret_index` - Called if the op executes inside a rich text, must return the leaf widget that will contain the rich text caret.
1285    /// * `local_caret_index` - Called in the caret widget context, must return the local caret index.
1286    /// * `rich_selection_index` - Called if the op executes inside a rich text, must return the leaf widget that will contain the rich text selection end.
1287    /// * `local_selection_index` - Called in selection end widget context, must return the local selection end index.
1288    ///
1289    /// Data can be passed between each stage with types `D0` from `rich_caret_index` to `local_caret_index`, `D1` from `local_caret_index` to
1290    /// `rich_selection_index` and `D2` from `rich_selection_index` to `local_selection_index`.
1291    ///
1292    /// If the op is not called inside a rich text only `local_caret_index` and `local_selection_index` are called with the default data values.
1293    ///
1294    /// The rich selection is updated if needed. If the local caret or selection index of an widget is set to 0(start) it is automatically corrected
1295    /// to the end of the previous rich leaf.
1296    pub fn new_rich<D0, D1, D2>(
1297        rich_caret_index: impl FnOnce(&RichText) -> (WidgetId, D0) + Send + 'static,
1298        local_caret_index: impl FnOnce(D0) -> (CaretIndex, D1) + Send + 'static,
1299        rich_selection_index: impl FnOnce(&RichText, D1) -> Option<(WidgetId, D2)> + Send + 'static,
1300        local_selection_index: impl FnOnce(D2) -> Option<CaretIndex> + Send + 'static,
1301    ) -> Self
1302    where
1303        D0: Default + Send + 'static,
1304        D1: Send + 'static,
1305        D2: Default + Send + 'static,
1306    {
1307        let mut f0 = Some(rich_caret_index);
1308        let mut f1 = Some(local_caret_index);
1309        let mut f2 = Some(rich_selection_index);
1310        let mut f3 = Some(local_selection_index);
1311        Self::new(move || {
1312            if let Some(ctx) = TEXT.try_rich() {
1313                rich_select_op_start(ctx, f0.take().unwrap(), f1.take().unwrap(), f2.take().unwrap(), f3.take().unwrap());
1314            } else {
1315                let (index, _) = f1.take().unwrap()(D0::default());
1316                let selection_index = f3.take().unwrap()(D2::default());
1317                let mut ctx = TEXT.resolve_caret();
1318                ctx.selection_index = selection_index;
1319                ctx.set_index(index);
1320            }
1321        })
1322    }
1323
1324    pub(super) fn call(self) {
1325        (self.op.lock())();
1326    }
1327}
1328
1329fn rich_select_op_start<D0: Send + 'static, D1: Send + 'static, D2: Send + 'static>(
1330    ctx: zng_app_context::RwLockReadGuardOwned<RichText>,
1331    rich_caret_index: impl FnOnce(&RichText) -> (WidgetId, D0),
1332    local_caret_index: impl FnOnce(D0) -> (CaretIndex, D1) + Send + 'static,
1333    rich_selection_index: impl FnOnce(&RichText, D1) -> Option<(WidgetId, D2)> + Send + 'static,
1334    local_selection_index: impl FnOnce(D2) -> Option<CaretIndex> + Send + 'static,
1335) {
1336    let (index, d0) = rich_caret_index(&ctx);
1337    if index == WIDGET.id() {
1338        rich_select_op_get_caret(ctx, index, d0, local_caret_index, rich_selection_index, local_selection_index);
1339    } else {
1340        let mut d0 = Some(d0);
1341        let mut f0 = Some(local_caret_index);
1342        let mut f1 = Some(rich_selection_index);
1343        let mut f2 = Some(local_selection_index);
1344        notify_leaf_select_op(
1345            index,
1346            TextSelectOp::new(move || {
1347                if let Some(ctx) = TEXT.try_rich()
1348                    && index == WIDGET.id()
1349                {
1350                    rich_select_op_get_caret(
1351                        ctx,
1352                        index,
1353                        d0.take().unwrap(),
1354                        f0.take().unwrap(),
1355                        f1.take().unwrap(),
1356                        f2.take().unwrap(),
1357                    );
1358                }
1359            }),
1360        );
1361    }
1362}
1363fn rich_select_op_get_caret<D0, D1, D2: Send + 'static>(
1364    ctx: zng_app_context::RwLockReadGuardOwned<RichText>,
1365    rich_caret_index: WidgetId,
1366    d0: D0,
1367    local_caret_index: impl FnOnce(D0) -> (CaretIndex, D1),
1368    rich_selection_index: impl FnOnce(&RichText, D1) -> Option<(WidgetId, D2)>,
1369    local_selection_index: impl FnOnce(D2) -> Option<CaretIndex> + Send + 'static,
1370) {
1371    let (index, d1) = local_caret_index(d0);
1372    {
1373        let mut ctx = TEXT.resolve_caret();
1374        ctx.set_index(index);
1375    }
1376
1377    match rich_selection_index(&ctx, d1) {
1378        Some((selection_index, d2)) => {
1379            if selection_index == WIDGET.id() {
1380                rich_select_op_get_selection(ctx, (rich_caret_index, index), selection_index, d2, local_selection_index);
1381            } else {
1382                let mut d2 = Some(d2);
1383                let mut f0 = Some(local_selection_index);
1384                notify_leaf_select_op(
1385                    selection_index,
1386                    TextSelectOp::new(move || {
1387                        if let Some(ctx) = TEXT.try_rich()
1388                            && selection_index == WIDGET.id()
1389                        {
1390                            rich_select_op_get_selection(
1391                                ctx,
1392                                (rich_caret_index, index),
1393                                selection_index,
1394                                d2.take().unwrap(),
1395                                f0.take().unwrap(),
1396                            );
1397                        }
1398                    }),
1399                );
1400            }
1401        }
1402        None => rich_select_op_finish(ctx, (rich_caret_index, index), None),
1403    }
1404}
1405fn rich_select_op_get_selection<D2>(
1406    ctx: zng_app_context::RwLockReadGuardOwned<RichText>,
1407    rich_caret_index: (WidgetId, CaretIndex),
1408    rich_selection_index: WidgetId,
1409    d2: D2,
1410    local_selection_index: impl FnOnce(D2) -> Option<CaretIndex>,
1411) {
1412    if let Some(index) = local_selection_index(d2) {
1413        let mut local_ctx = TEXT.resolve_caret();
1414        local_ctx.selection_index = Some(index);
1415        local_ctx.index_version += 1;
1416        rich_select_op_finish(ctx, rich_caret_index, Some((rich_selection_index, index)));
1417    } else {
1418        rich_select_op_finish(ctx, rich_caret_index, None);
1419    }
1420}
1421fn rich_select_op_finish(
1422    ctx: zng_app_context::RwLockReadGuardOwned<RichText>,
1423    rich_caret_index: (WidgetId, CaretIndex),
1424    rich_selection_index: Option<(WidgetId, CaretIndex)>,
1425) {
1426    if let Some(mut index) = ctx.leaf_info(rich_caret_index.0) {
1427        if rich_caret_index.1.index == 0 {
1428            // index 0 is the end of previous leaf
1429            if let Some(prev) = index.rich_text_prev().next() {
1430                index = prev;
1431                notify_leaf_select_op(
1432                    index.id(),
1433                    TextSelectOp::new(move || {
1434                        let end = TEXT.resolved().segmented_text.text().len();
1435                        TEXT.resolve_caret().set_char_index(end);
1436                    }),
1437                );
1438            }
1439        }
1440        if let Some(rich_selection_index) = rich_selection_index {
1441            if let Some(mut selection) = ctx.leaf_info(rich_selection_index.0) {
1442                if rich_selection_index.1.index == 0 {
1443                    // selection 0 is the end of the previous leaf
1444                    if let Some(prev) = selection.rich_text_prev().next() {
1445                        selection = prev;
1446                        notify_leaf_select_op(
1447                            selection.id(),
1448                            TextSelectOp::new(move || {
1449                                let end = TEXT.resolved().segmented_text.text().len();
1450                                TEXT.resolve_caret().set_char_index(end);
1451                            }),
1452                        );
1453                    }
1454                }
1455
1456                drop(ctx);
1457                TEXT.resolve_rich_caret().update_selection(&index, Some(&selection), false, false);
1458            }
1459        } else {
1460            // no selection
1461
1462            drop(ctx);
1463            TEXT.resolve_rich_caret().update_selection(&index, None, false, false);
1464        }
1465    }
1466}
1467
1468fn rich_clear_next_prev(is_next: bool, is_word: bool) -> TextSelectOp {
1469    TextSelectOp::new_rich(
1470        // get prev/next leaf widget
1471        move |ctx| {
1472            if let Some(i) = ctx.caret_index_info()
1473                && let Some(s) = ctx.caret_selection_index_info()
1474            {
1475                // clear selection, next places caret at end of selection, prev at start
1476
1477                let (a, b) = match i.cmp_sibling_in(&s, &i.root()).unwrap() {
1478                    cmp::Ordering::Less | cmp::Ordering::Equal => (&i, &s),
1479                    cmp::Ordering::Greater => (&s, &i),
1480                };
1481
1482                let c = if is_next { b } else { a };
1483
1484                (c.id(), false) // false to just collapse to selection
1485            } else {
1486                // no selection, actually move caret
1487
1488                let local_ctx = TEXT.resolved();
1489                if is_next {
1490                    let index = local_ctx.caret.index.unwrap_or(CaretIndex::ZERO).index;
1491                    if index == local_ctx.segmented_text.text().len() {
1492                        // next from end, check if has next sibling
1493                        if let Some(info) = ctx.leaf_info(WIDGET.id())
1494                            && let Some(next) = info.rich_text_next().next()
1495                        {
1496                            return (next.id(), true);
1497                        }
1498                    }
1499
1500                    // caret stays inside
1501                    (WIDGET.id(), false)
1502                } else {
1503                    // !is_next
1504
1505                    let cutout = if is_word { local_ctx.segmented_text.next_word_index(0) } else { 1 };
1506                    if local_ctx.caret.index.unwrap_or(CaretIndex::ZERO).index <= cutout {
1507                        // next moves to the start (or is already in start)
1508
1509                        if let Some(info) = ctx.leaf_info(WIDGET.id())
1510                            && let Some(prev) = info.rich_text_prev().next()
1511                        {
1512                            return (prev.id(), true);
1513                        }
1514                    }
1515
1516                    (WIDGET.id(), false)
1517                }
1518            }
1519        },
1520        // get caret in the prev/next widget
1521        move |is_from_sibling| {
1522            if is_from_sibling {
1523                if is_next {
1524                    (CaretIndex { index: 1, line: 0 }, ())
1525                } else {
1526                    let local_ctx = TEXT.resolved();
1527                    (
1528                        CaretIndex {
1529                            index: local_ctx.segmented_text.text().len(),
1530                            line: 0,
1531                        },
1532                        (),
1533                    )
1534                }
1535            } else {
1536                local_clear_next_prev(is_next, is_word);
1537                (TEXT.resolved().caret.index.unwrap_or(CaretIndex::ZERO), ())
1538            }
1539        },
1540        |_, _| None,
1541        |()| None,
1542    )
1543}
1544fn local_clear_next_prev(is_next: bool, is_word: bool) {
1545    // compute next caret position
1546    let ctx = TEXT.resolved();
1547    let current_index = ctx.caret.index.unwrap_or(CaretIndex::ZERO);
1548    let mut next_index = current_index;
1549    if let Some(selection) = ctx.caret.selection_range() {
1550        next_index.index = if is_next { selection.end.index } else { selection.start.index };
1551    } else {
1552        next_index.index = if is_next {
1553            let from = current_index.index;
1554            if is_word {
1555                ctx.segmented_text.next_word_index(from)
1556            } else {
1557                ctx.segmented_text.next_insert_index(from)
1558            }
1559        } else {
1560            let from = current_index.index;
1561            if is_word {
1562                ctx.segmented_text.prev_word_index(from)
1563            } else {
1564                ctx.segmented_text.prev_insert_index(from)
1565            }
1566        };
1567    }
1568
1569    drop(ctx);
1570
1571    let mut ctx = TEXT.resolve_caret();
1572    ctx.clear_selection();
1573    ctx.set_index(next_index);
1574    ctx.used_retained_x = false;
1575}
1576
1577fn rich_select_next_prev(is_next: bool, is_word: bool) -> TextSelectOp {
1578    TextSelectOp::new_rich(
1579        // get prev/next leaf widget
1580        move |ctx| {
1581            let local_ctx = TEXT.resolved();
1582
1583            let index = local_ctx.caret.index.unwrap_or(CaretIndex::ZERO).index;
1584
1585            if is_next {
1586                if index == local_ctx.segmented_text.text().len() {
1587                    // next from end
1588                    if let Some(info) = ctx.leaf_info(WIDGET.id())
1589                        && let Some(next) = info.rich_text_next().next()
1590                    {
1591                        return (next.id(), true);
1592                    }
1593                }
1594            } else {
1595                // !is_next
1596
1597                let cutout = if is_word { local_ctx.segmented_text.next_word_index(0) } else { 1 };
1598                if local_ctx.caret.index.unwrap_or(CaretIndex::ZERO).index <= cutout {
1599                    // next moves to the start (or is already in start)
1600                    if let Some(info) = ctx.leaf_info(WIDGET.id())
1601                        && let Some(prev) = info.rich_text_prev().next()
1602                    {
1603                        return (prev.id(), true);
1604                    }
1605                }
1606            }
1607            (WIDGET.id(), false)
1608        },
1609        // get caret in the prev/next widget
1610        move |is_from_sibling| {
1611            let id = WIDGET.id();
1612            if is_from_sibling {
1613                if is_next {
1614                    // caret was at sibling end
1615                    (CaretIndex { index: 1, line: 0 }, id)
1616                } else {
1617                    // caret was at sibling start or moves to sibling start (that is the same as our end)
1618                    let len = TEXT.resolved().segmented_text.text().len();
1619                    (CaretIndex { index: len, line: 0 }, id)
1620                }
1621            } else {
1622                local_select_next_prev(is_next, is_word);
1623                (TEXT.resolved().caret.index.unwrap_or(CaretIndex::ZERO), id)
1624            }
1625        },
1626        // get selection_index leaf widget
1627        |ctx, index| Some((ctx.caret.selection_index.unwrap_or(index), ())),
1628        // get local selection_index
1629        |()| {
1630            let local_ctx = TEXT.resolved();
1631            Some(
1632                local_ctx
1633                    .caret
1634                    .selection_index
1635                    .unwrap_or(local_ctx.caret.index.unwrap_or(CaretIndex::ZERO)),
1636            )
1637        },
1638    )
1639}
1640fn local_select_next_prev(is_next: bool, is_word: bool) {
1641    // compute next caret position
1642    let ctx = TEXT.resolved();
1643    let current_index = ctx.caret.index.unwrap_or(CaretIndex::ZERO);
1644    let mut next_index = current_index;
1645    next_index.index = if is_next {
1646        if is_word {
1647            ctx.segmented_text.next_word_index(current_index.index)
1648        } else {
1649            ctx.segmented_text.next_insert_index(current_index.index)
1650        }
1651    } else {
1652        // is_prev
1653        if is_word {
1654            ctx.segmented_text.prev_word_index(current_index.index)
1655        } else {
1656            ctx.segmented_text.prev_insert_index(current_index.index)
1657        }
1658    };
1659    drop(ctx);
1660
1661    let mut ctx = TEXT.resolve_caret();
1662    if ctx.selection_index.is_none() {
1663        ctx.selection_index = Some(current_index);
1664    }
1665    ctx.set_index(next_index);
1666    ctx.used_retained_x = false;
1667}
1668
1669fn rich_up_down(clear_selection: bool, is_down: bool, is_page: bool) -> TextSelectOp {
1670    TextSelectOp::new_rich(
1671        move |ctx| {
1672            let resolved = TEXT.resolved();
1673            let laidout = TEXT.laidout();
1674
1675            let local_line_i = resolved.caret.index.unwrap_or(CaretIndex::ZERO).line;
1676            let last_line_i = laidout.shaped_text.lines_len().saturating_sub(1);
1677            let next_local_line_i = local_line_i.saturating_add_signed(if is_down { 1 } else { -1 }).min(last_line_i);
1678
1679            let page_h = if is_page { laidout.viewport.height } else { Px(0) };
1680
1681            let mut need_spatial_search = local_line_i == next_local_line_i; // if already at first/last line
1682
1683            if !need_spatial_search {
1684                if is_page {
1685                    if let Some(local_line) = laidout.shaped_text.line(local_line_i) {
1686                        if is_down {
1687                            if let Some(last_line) = laidout.shaped_text.line(last_line_i) {
1688                                let max_local_y = last_line.rect().max_y() - local_line.rect().min_y();
1689                                need_spatial_search = max_local_y < page_h; // if page distance is greater than maximum down distance
1690                            }
1691                        } else if let Some(first_line) = laidout.shaped_text.line(0) {
1692                            let max_local_y = local_line.rect().max_y() - first_line.rect().min_y();
1693                            need_spatial_search = max_local_y < page_h; // if page distance is greater than maximum up distance
1694                        }
1695                    }
1696                } else if let Some(next_local_line) = laidout.shaped_text.line(next_local_line_i) {
1697                    let r = next_local_line.rect();
1698                    let x = laidout.caret_retained_x;
1699                    need_spatial_search = r.min_x() > x || r.max_x() < x; // if next local line does not contain ideal caret horizontally
1700                }
1701            }
1702
1703            if need_spatial_search
1704                && let Some(local_line) = laidout.shaped_text.line(local_line_i)
1705                && let Some(root_info) = ctx.root_info()
1706            {
1707                // line ok, rich context ok
1708                let r = local_line.rect();
1709                let local_point = PxPoint::new(laidout.caret_retained_x, r.origin.y + r.size.height / Px(2));
1710                let local_info = WIDGET.info();
1711                let local_to_window = local_info.inner_transform();
1712
1713                let local_cut_y = if is_down { local_point.y + page_h } else { local_point.y - page_h };
1714                let window_cut_y = local_to_window
1715                    .transform_point(PxPoint::new(Px(0), local_cut_y))
1716                    .unwrap_or_default()
1717                    .y;
1718
1719                if let Some(window_point) = local_to_window.transform_point(local_point) {
1720                    // transform ok
1721
1722                    // find the nearest sibling considering only the prev/next rich lines
1723                    let local_line_info = local_info.rich_text_line_info();
1724                    let filter = |other: &WidgetInfo, rect: PxRect, row_i, rows_len| {
1725                        if is_down {
1726                            if rect.max_y() < window_cut_y {
1727                                // rectangle is before the page y line or the the current line
1728                                return false;
1729                            }
1730                        } else if rect.min_y() > window_cut_y {
1731                            // rectangle is after the page y line or the current line
1732                            return false;
1733                        }
1734
1735                        match local_info.cmp_sibling_in(other, &root_info).unwrap() {
1736                            cmp::Ordering::Less => {
1737                                // other is after local
1738
1739                                if !is_down {
1740                                    return false;
1741                                }
1742                                if local_line_i < last_line_i {
1743                                    return true; // local started next line
1744                                }
1745                                for next in local_info.rich_text_next() {
1746                                    let line_info = next.rich_text_line_info();
1747                                    if line_info.starts_new_line {
1748                                        return true; // `other` starts new line or is after this line break
1749                                    }
1750                                    if line_info.ends_in_new_line {
1751                                        if &next == other {
1752                                            return row_i > 0; // `other` rect is not in the same line
1753                                        }
1754                                        return true; // `other` starts after this line break
1755                                    }
1756                                    if &next == other {
1757                                        return false; // `other` is in same line
1758                                    }
1759                                }
1760                                unreachable!() // filter only called if is sibling, cmp ensures that is next sibling
1761                            }
1762                            cmp::Ordering::Greater => {
1763                                // other is before local
1764
1765                                if is_down {
1766                                    return false;
1767                                }
1768                                if local_line_i > 0 || local_line_info.starts_new_line {
1769                                    return true; // local started line, all prev wgt in prev lines
1770                                }
1771                                for prev in local_info.rich_text_prev() {
1772                                    let line_info = prev.rich_text_line_info();
1773                                    if line_info.ends_in_new_line {
1774                                        if &prev == other {
1775                                            return row_i < rows_len - 1; // `other` rect is not in the same line
1776                                        }
1777                                        return true; // `other` ends before this linebreak
1778                                    }
1779                                    if line_info.starts_new_line {
1780                                        return &prev != other; // `other` starts the line (same line) or not (is before)
1781                                    }
1782
1783                                    if &prev == other {
1784                                        return false; // `other` is in same line
1785                                    }
1786                                }
1787                                unreachable!()
1788                            }
1789                            cmp::Ordering::Equal => false,
1790                        }
1791                    };
1792                    if let Some(next) = root_info.rich_text_nearest_leaf_filtered(window_point, filter) {
1793                        // found nearest sibling on the next/prev rich lines
1794
1795                        let next_info = next.clone();
1796
1797                        // get the next(wgt) local line that is in the next/prev rich line
1798                        let mut next_line = 0;
1799                        if let Some(next_inline_rows_len) = next_info.bounds_info().inline().map(|i| i.rows.len())
1800                            && next_inline_rows_len > 1
1801                        {
1802                            if is_down {
1803                                // next is logical next
1804
1805                                if local_line_i == last_line_i {
1806                                    // local did not start next line
1807
1808                                    for l_next in local_info.rich_text_next() {
1809                                        let line_info = l_next.rich_text_line_info();
1810                                        if line_info.starts_new_line || line_info.ends_in_new_line {
1811                                            // found rich line end
1812                                            if l_next == next {
1813                                                // its inside the `next`, meaning it starts on the same rich line
1814                                                next_line = 1;
1815                                            }
1816                                            break;
1817                                        }
1818                                    }
1819                                }
1820                            } else {
1821                                // next is up (logical prev)
1822                                next_line = next_inline_rows_len - 1;
1823
1824                                if local_line_i == 0 && !local_line_info.starts_new_line {
1825                                    // local did not start current line
1826
1827                                    for l_prev in local_info.rich_text_prev() {
1828                                        let line_info = l_prev.rich_text_line_info();
1829                                        if line_info.starts_new_line || line_info.ends_in_new_line {
1830                                            // found rich line start
1831                                            if l_prev == next {
1832                                                // its inside the `next`, meaning it ends on the same rich line
1833                                                next_line -= 1;
1834                                            }
1835                                            break;
1836                                        }
1837                                    }
1838                                }
1839                            }
1840                        }
1841                        return (next.id(), Some((window_point.x, next_line)));
1842                    }
1843                }
1844            }
1845
1846            // when can't go down within local goes to text start/end
1847            let mut cant_go_down_up = if is_down {
1848                // if already at last line
1849                local_line_i == last_line_i
1850            } else {
1851                // if already at first line
1852                local_line_i == 0
1853            };
1854            if is_page
1855                && !cant_go_down_up
1856                && let Some(local_line) = laidout.shaped_text.line(local_line_i)
1857            {
1858                if is_down {
1859                    if let Some(last_line) = laidout.shaped_text.line(last_line_i) {
1860                        // if page down distance greater than distance to last line
1861                        let max_local_y = last_line.rect().max_y() - local_line.rect().min_y();
1862                        cant_go_down_up = max_local_y < page_h;
1863                    }
1864                } else if let Some(first_line) = laidout.shaped_text.line(0) {
1865                    // if page up distance greater than distance to first line
1866                    let max_local_y = local_line.rect().max_y() - first_line.rect().min_y();
1867                    cant_go_down_up = max_local_y < page_h;
1868                }
1869            }
1870            if cant_go_down_up {
1871                if is_down {
1872                    if let Some(end) = ctx.leaves_rev().next() {
1873                        return (end.id(), None);
1874                    }
1875                } else if let Some(start) = ctx.leaves().next() {
1876                    return (start.id(), None);
1877                }
1878            }
1879
1880            (WIDGET.id(), None) // only local nav
1881        },
1882        move |rich_request| {
1883            if let Some((window_x, line_i)) = rich_request {
1884                let local_x = WIDGET
1885                    .info()
1886                    .inner_transform()
1887                    .inverse()
1888                    .and_then(|t| t.transform_point(PxPoint::new(window_x, Px(0))))
1889                    .unwrap_or_default()
1890                    .x;
1891                TEXT.set_caret_retained_x(local_x);
1892                let local_ctx = TEXT.laidout();
1893                if let Some(line) = local_ctx.shaped_text.line(line_i) {
1894                    let index = match line.nearest_seg(local_x) {
1895                        Some(s) => s.nearest_char_index(local_x, TEXT.resolved().segmented_text.text()),
1896                        None => line.text_range().end,
1897                    };
1898                    let index = CaretIndex { index, line: line_i };
1899                    TEXT.resolve_caret().used_retained_x = true; // new_rich does not set this
1900                    return (index, ());
1901                }
1902            }
1903            let diff = if is_down { 1 } else { -1 };
1904            if is_page {
1905                local_page_up_down(clear_selection, diff);
1906            } else {
1907                local_line_up_down(clear_selection, diff);
1908            }
1909            (TEXT.resolved().caret.index.unwrap(), ())
1910        },
1911        move |ctx, ()| {
1912            if clear_selection {
1913                None
1914            } else {
1915                Some((ctx.caret.selection_index.or(ctx.caret.index).unwrap_or_else(|| WIDGET.id()), ()))
1916            }
1917        },
1918        move |()| {
1919            if clear_selection {
1920                None
1921            } else {
1922                let local_ctx = TEXT.resolved();
1923                Some(
1924                    local_ctx
1925                        .caret
1926                        .selection_index
1927                        .or(local_ctx.caret.index)
1928                        .unwrap_or(CaretIndex::ZERO),
1929                )
1930            }
1931        },
1932    )
1933}
1934fn local_line_up_down(clear_selection: bool, diff: i8) {
1935    let diff = diff as isize;
1936
1937    let mut caret = TEXT.resolve_caret();
1938    let mut i = caret.index.unwrap_or(CaretIndex::ZERO);
1939    if clear_selection {
1940        caret.clear_selection();
1941    } else if caret.selection_index.is_none() {
1942        caret.selection_index = Some(i);
1943    }
1944    caret.used_retained_x = true;
1945
1946    let laidout = TEXT.laidout();
1947
1948    if laidout.caret_origin.is_some() {
1949        let last_line = laidout.shaped_text.lines_len().saturating_sub(1);
1950        let li = i.line;
1951        let next_li = li.saturating_add_signed(diff).min(last_line);
1952        if li != next_li {
1953            drop(caret);
1954            let resolved = TEXT.resolved();
1955            match laidout.shaped_text.line(next_li) {
1956                Some(l) => {
1957                    i.line = next_li;
1958                    i.index = match l.nearest_seg(laidout.caret_retained_x) {
1959                        Some(s) => s.nearest_char_index(laidout.caret_retained_x, resolved.segmented_text.text()),
1960                        None => l.text_range().end,
1961                    }
1962                }
1963                None => i = CaretIndex::ZERO,
1964            };
1965            i.index = resolved.segmented_text.snap_grapheme_boundary(i.index);
1966            drop(resolved);
1967            caret = TEXT.resolve_caret();
1968            caret.set_index(i);
1969        } else if diff == -1 {
1970            caret.set_char_index(0);
1971        } else if diff == 1 {
1972            drop(caret);
1973            let len = TEXT.resolved().segmented_text.text().len();
1974            caret = TEXT.resolve_caret();
1975            caret.set_char_index(len);
1976        }
1977    }
1978
1979    if caret.index.is_none() {
1980        caret.set_index(CaretIndex::ZERO);
1981        caret.clear_selection();
1982    }
1983}
1984fn local_page_up_down(clear_selection: bool, diff: i8) {
1985    let diff = diff as i32;
1986
1987    let mut caret = TEXT.resolve_caret();
1988    let mut i = caret.index.unwrap_or(CaretIndex::ZERO);
1989    if clear_selection {
1990        caret.clear_selection();
1991    } else if caret.selection_index.is_none() {
1992        caret.selection_index = Some(i);
1993    }
1994
1995    let laidout = TEXT.laidout();
1996
1997    let page_y = laidout.viewport.height * Px(diff);
1998    caret.used_retained_x = true;
1999    if laidout.caret_origin.is_some() {
2000        let li = i.line;
2001        if diff == -1 && li == 0 {
2002            caret.set_char_index(0);
2003        } else if diff == 1 && li == laidout.shaped_text.lines_len() - 1 {
2004            drop(caret);
2005            let len = TEXT.resolved().segmented_text.text().len();
2006            caret = TEXT.resolve_caret();
2007            caret.set_char_index(len);
2008        } else if let Some(li) = laidout.shaped_text.line(li) {
2009            drop(caret);
2010            let resolved = TEXT.resolved();
2011
2012            let target_line_y = li.rect().origin.y + page_y;
2013            match laidout.shaped_text.nearest_line(target_line_y) {
2014                Some(l) => {
2015                    i.line = l.index();
2016                    i.index = match l.nearest_seg(laidout.caret_retained_x) {
2017                        Some(s) => s.nearest_char_index(laidout.caret_retained_x, resolved.segmented_text.text()),
2018                        None => l.text_range().end,
2019                    }
2020                }
2021                None => i = CaretIndex::ZERO,
2022            };
2023            i.index = resolved.segmented_text.snap_grapheme_boundary(i.index);
2024
2025            drop(resolved);
2026            caret = TEXT.resolve_caret();
2027
2028            caret.set_index(i);
2029        }
2030    }
2031
2032    if caret.index.is_none() {
2033        caret.set_index(CaretIndex::ZERO);
2034        caret.clear_selection();
2035    }
2036}
2037
2038fn rich_line_start_end(clear_selection: bool, is_end: bool) -> TextSelectOp {
2039    TextSelectOp::new_rich(
2040        // get caret widget, rich line start/end
2041        move |ctx| {
2042            let from_id = WIDGET.id();
2043            if let Some(c) = ctx.leaf_info(WIDGET.id()) {
2044                let local_line = TEXT.resolved().caret.index.unwrap_or(CaretIndex::ZERO).line;
2045                if is_end {
2046                    let last_line = TEXT.laidout().shaped_text.lines_len() - 1;
2047                    if local_line == last_line {
2048                        // current line can end in a next sibling
2049
2050                        let mut prev_id = c.id();
2051                        for c in c.rich_text_next() {
2052                            let line_info = c.rich_text_line_info();
2053                            if line_info.starts_new_line && !line_info.is_wrap_start {
2054                                return (prev_id, Some(from_id));
2055                            } else if line_info.ends_in_new_line {
2056                                return (c.id(), Some(from_id));
2057                            }
2058                            prev_id = c.id();
2059                        }
2060
2061                        // text end
2062                        return (prev_id, Some(from_id));
2063                    }
2064                } else {
2065                    // !is_end
2066
2067                    if local_line == 0 {
2068                        // current line can start in a prev sibling
2069
2070                        let mut last_id = c.id();
2071                        let mut first = true;
2072                        for c in c.rich_text_self_and_prev() {
2073                            let line_info = c.rich_text_line_info();
2074                            if (line_info.starts_new_line && !line_info.is_wrap_start) || (line_info.ends_in_new_line && !first) {
2075                                return (c.id(), Some(from_id));
2076                            }
2077                            last_id = c.id();
2078                            first = false;
2079                        }
2080
2081                        // text start
2082                        return (last_id, Some(from_id));
2083                    }
2084                }
2085            }
2086            (from_id, None)
2087        },
2088        // get local caret index in the rich line start/end widget
2089        move |from_id| {
2090            if let Some(from_id) = from_id
2091                && from_id != WIDGET.id()
2092            {
2093                // ensure the caret is at a start/end from the other sibling for `local_line_start_end`
2094                if is_end {
2095                    TEXT.resolve_caret().index = Some(CaretIndex::ZERO);
2096                } else {
2097                    let local_ctx = TEXT.laidout();
2098                    let line = local_ctx.shaped_text.lines_len() - 1;
2099                    let index = local_ctx.shaped_text.line(line).unwrap().text_caret_range().end;
2100                    drop(local_ctx);
2101                    TEXT.resolve_caret().index = Some(CaretIndex { index, line })
2102                }
2103            }
2104            local_line_start_end(clear_selection, is_end);
2105
2106            (TEXT.resolved().caret.index.unwrap(), from_id)
2107        },
2108        // get the selection index widget, line selection always updates from the caret
2109        move |ctx, from_id| {
2110            if clear_selection {
2111                return None;
2112            }
2113            Some((ctx.caret.selection_index.or(from_id).unwrap_or_else(|| WIDGET.id()), ()))
2114        },
2115        // get the selection index
2116        move |()| {
2117            if clear_selection {
2118                return None;
2119            }
2120            let local_ctx = TEXT.resolved();
2121            Some(
2122                local_ctx
2123                    .caret
2124                    .selection_index
2125                    .or(local_ctx.caret.index)
2126                    .unwrap_or(CaretIndex::ZERO),
2127            )
2128        },
2129    )
2130}
2131fn local_line_start_end(clear_selection: bool, is_end: bool) {
2132    let mut ctx = TEXT.resolve_caret();
2133    let mut i = ctx.index.unwrap_or(CaretIndex::ZERO);
2134
2135    if clear_selection {
2136        ctx.clear_selection();
2137    } else if ctx.selection_index.is_none() {
2138        ctx.selection_index = Some(i);
2139    }
2140
2141    if let Some(li) = TEXT.laidout().shaped_text.line(i.line) {
2142        i.index = if is_end {
2143            li.actual_text_caret_range().end
2144        } else {
2145            li.actual_text_range().start
2146        };
2147        ctx.set_index(i);
2148        ctx.used_retained_x = false;
2149    }
2150}
2151
2152fn rich_text_start_end(clear_selection: bool, is_end: bool) -> TextSelectOp {
2153    TextSelectOp::new_rich(
2154        move |ctx| {
2155            let from_id = WIDGET.id();
2156            let id = if is_end { ctx.leaves_rev().next() } else { ctx.leaves().next() }.map(|w| w.id());
2157            (id.unwrap_or(from_id), Some(from_id))
2158        },
2159        move |from_id| {
2160            local_text_start_end(clear_selection, is_end);
2161            (TEXT.resolved().caret.index.unwrap(), from_id)
2162        },
2163        // get the selection index widget, line selection always updates from the caret
2164        move |ctx, from_id| {
2165            if clear_selection {
2166                return None;
2167            }
2168            Some((ctx.caret.selection_index.or(from_id).unwrap_or_else(|| WIDGET.id()), ()))
2169        },
2170        // get the selection index
2171        move |()| {
2172            if clear_selection {
2173                return None;
2174            }
2175            let local_ctx = TEXT.resolved();
2176            Some(
2177                local_ctx
2178                    .caret
2179                    .selection_index
2180                    .or(local_ctx.caret.index)
2181                    .unwrap_or(CaretIndex::ZERO),
2182            )
2183        },
2184    )
2185}
2186fn local_text_start_end(clear_selection: bool, is_end: bool) {
2187    let idx = if is_end { TEXT.resolved().segmented_text.text().len() } else { 0 };
2188
2189    let mut ctx = TEXT.resolve_caret();
2190    let mut i = ctx.index.unwrap_or(CaretIndex::ZERO);
2191    if clear_selection {
2192        ctx.clear_selection();
2193    } else if ctx.selection_index.is_none() {
2194        ctx.selection_index = Some(i);
2195    }
2196    i.index = idx;
2197    ctx.set_index(i);
2198    ctx.used_retained_x = false;
2199}
2200
2201/// `clear_selection` is `replace_selection` for `is_word` mode.
2202fn rich_nearest_char_word_to(clear_selection: bool, window_point: DipPoint, is_word: bool) -> TextSelectOp {
2203    TextSelectOp::new_rich(
2204        move |ctx| {
2205            if let Some(root) = ctx.root_info()
2206                && let Some(nearest_leaf) = root.rich_text_nearest_leaf(window_point.to_px(root.tree().scale_factor()))
2207            {
2208                return (nearest_leaf.id(), ());
2209            }
2210            (WIDGET.id(), ())
2211        },
2212        move |()| {
2213            if is_word {
2214                local_select_line_word_nearest_to(clear_selection, true, window_point)
2215            } else {
2216                local_nearest_to(clear_selection, window_point)
2217            }
2218            (TEXT.resolved().caret.index.unwrap(), ())
2219        },
2220        move |ctx, ()| {
2221            if clear_selection {
2222                if is_word && TEXT.resolved().caret.selection_index.is_some() {
2223                    Some((WIDGET.id(), ()))
2224                } else {
2225                    None
2226                }
2227            } else {
2228                Some((ctx.caret.selection_index.unwrap_or_else(|| WIDGET.id()), ()))
2229            }
2230        },
2231        move |()| {
2232            if clear_selection {
2233                if is_word { TEXT.resolved().caret.selection_index } else { None }
2234            } else {
2235                let local_ctx = TEXT.resolved();
2236                Some(
2237                    local_ctx
2238                        .caret
2239                        .selection_index
2240                        .or(local_ctx.caret.index)
2241                        .unwrap_or(CaretIndex::ZERO),
2242                )
2243            }
2244        },
2245    )
2246}
2247fn local_nearest_to(clear_selection: bool, window_point: DipPoint) {
2248    let mut caret = TEXT.resolve_caret();
2249    let mut i = caret.index.unwrap_or(CaretIndex::ZERO);
2250
2251    if clear_selection {
2252        caret.clear_selection();
2253    } else if caret.selection_index.is_none() {
2254        caret.selection_index = Some(i);
2255    } else if let Some((_, is_word)) = caret.initial_selection.clone() {
2256        drop(caret);
2257        return local_select_line_word_nearest_to(false, is_word, window_point);
2258    }
2259
2260    caret.used_retained_x = false;
2261
2262    //if there was at least one layout
2263    let laidout = TEXT.laidout();
2264    if let Some(pos) = laidout
2265        .render_info
2266        .transform
2267        .inverse()
2268        .and_then(|t| t.project_point(window_point.to_px(laidout.render_info.scale_factor)))
2269    {
2270        drop(caret);
2271        let resolved = TEXT.resolved();
2272
2273        //if has rendered
2274        i = match laidout.shaped_text.nearest_line(pos.y) {
2275            Some(l) => CaretIndex {
2276                line: l.index(),
2277                index: match l.nearest_seg(pos.x) {
2278                    Some(s) => s.nearest_char_index(pos.x, resolved.segmented_text.text()),
2279                    None => l.text_range().end,
2280                },
2281            },
2282            None => CaretIndex::ZERO,
2283        };
2284        i.index = resolved.segmented_text.snap_grapheme_boundary(i.index);
2285
2286        drop(resolved);
2287        caret = TEXT.resolve_caret();
2288
2289        caret.set_index(i);
2290    }
2291
2292    if caret.index.is_none() {
2293        caret.set_index(CaretIndex::ZERO);
2294        caret.clear_selection();
2295    }
2296}
2297
2298fn rich_selection_index_nearest_to(window_point: DipPoint, move_selection_index: bool) -> TextSelectOp {
2299    TextSelectOp::new_rich(
2300        move |ctx| {
2301            if move_selection_index {
2302                return (ctx.caret.index.unwrap_or_else(|| WIDGET.id()), ());
2303            }
2304
2305            if let Some(root) = ctx.root_info()
2306                && let Some(nearest_leaf) = root.rich_text_nearest_leaf(window_point.to_px(root.tree().scale_factor()))
2307            {
2308                return (nearest_leaf.id(), ());
2309            }
2310            (WIDGET.id(), ())
2311        },
2312        move |()| {
2313            if !move_selection_index {
2314                local_select_index_nearest_to(window_point, false);
2315            }
2316            (TEXT.resolved().caret.index.unwrap_or(CaretIndex::ZERO), ())
2317        },
2318        move |ctx, ()| {
2319            if !move_selection_index {
2320                return Some((ctx.caret.selection_index.unwrap_or_else(|| WIDGET.id()), ()));
2321            }
2322
2323            if let Some(root) = ctx.root_info()
2324                && let Some(nearest_leaf) = root.rich_text_nearest_leaf(window_point.to_px(root.tree().scale_factor()))
2325            {
2326                return Some((nearest_leaf.id(), ()));
2327            }
2328            Some((WIDGET.id(), ()))
2329        },
2330        move |()| {
2331            if move_selection_index {
2332                local_select_index_nearest_to(window_point, true);
2333            }
2334            Some(TEXT.resolved().caret.selection_index.unwrap_or(CaretIndex::ZERO))
2335        },
2336    )
2337}
2338fn local_select_index_nearest_to(window_point: DipPoint, move_selection_index: bool) {
2339    let mut caret = TEXT.resolve_caret();
2340
2341    if caret.index.is_none() {
2342        caret.index = Some(CaretIndex::ZERO);
2343    }
2344    if caret.selection_index.is_none() {
2345        caret.selection_index = Some(caret.index.unwrap());
2346    }
2347
2348    caret.used_retained_x = false;
2349    caret.index_version += 1;
2350
2351    let laidout = TEXT.laidout();
2352    if let Some(pos) = laidout
2353        .render_info
2354        .transform
2355        .inverse()
2356        .and_then(|t| t.project_point(window_point.to_px(laidout.render_info.scale_factor)))
2357    {
2358        drop(caret);
2359        let resolved = TEXT.resolved();
2360
2361        let mut i = match laidout.shaped_text.nearest_line(pos.y) {
2362            Some(l) => CaretIndex {
2363                line: l.index(),
2364                index: match l.nearest_seg(pos.x) {
2365                    Some(s) => s.nearest_char_index(pos.x, resolved.segmented_text.text()),
2366                    None => l.text_range().end,
2367                },
2368            },
2369            None => CaretIndex::ZERO,
2370        };
2371        i.index = resolved.segmented_text.snap_grapheme_boundary(i.index);
2372
2373        drop(resolved);
2374        caret = TEXT.resolve_caret();
2375
2376        if move_selection_index {
2377            caret.selection_index = Some(i);
2378        } else {
2379            caret.index = Some(i);
2380        }
2381    }
2382}
2383
2384fn rich_nearest_line_to(replace_selection: bool, window_point: DipPoint) -> TextSelectOp {
2385    TextSelectOp::new_rich(
2386        move |ctx| {
2387            if let Some(root) = ctx.root_info() {
2388                let window_point = window_point.to_px(root.tree().scale_factor());
2389                if let Some(nearest_leaf) = root.rich_text_nearest_leaf(window_point) {
2390                    let mut nearest = usize::MAX;
2391                    let mut nearest_dist = DistanceKey::NONE_MAX;
2392                    let mut rows_len = 0;
2393                    nearest_leaf.bounds_info().visit_inner_rects::<()>(|r, i, l| {
2394                        rows_len = l;
2395                        let dist = DistanceKey::from_rect_to_point(r, window_point);
2396                        if dist < nearest_dist {
2397                            nearest_dist = dist;
2398                            nearest = i;
2399                        }
2400                        ops::ControlFlow::Continue(())
2401                    });
2402
2403                    // returns the rich line end
2404                    if nearest < rows_len.saturating_sub(1) {
2405                        // rich line ends in the leaf widget
2406                        return (nearest_leaf.id(), Some(nearest));
2407                    } else {
2408                        // rich line starts in the leaf widget
2409                        let mut end = nearest_leaf.clone();
2410                        for next in nearest_leaf.rich_text_next() {
2411                            let line_info = next.rich_text_line_info();
2412                            if line_info.starts_new_line && !line_info.is_wrap_start {
2413                                return (
2414                                    end.id(),
2415                                    Some(end.bounds_info().inline().map(|i| i.rows.len().saturating_sub(1)).unwrap_or(0)),
2416                                );
2417                            }
2418                            end = next;
2419                            if line_info.ends_in_new_line {
2420                                break;
2421                            }
2422                        }
2423                        return (end.id(), Some(0));
2424                    }
2425                }
2426            }
2427            (WIDGET.id(), None)
2428        },
2429        move |rich_request| {
2430            if let Some(line_i) = rich_request {
2431                let local_ctx = TEXT.laidout();
2432                if let Some(line) = local_ctx.shaped_text.line(line_i) {
2433                    return (
2434                        CaretIndex {
2435                            index: line.actual_text_caret_range().end,
2436                            line: line_i,
2437                        },
2438                        line.actual_line_start().index() == 0,
2439                    );
2440                }
2441            }
2442            local_select_line_word_nearest_to(replace_selection, true, window_point);
2443            (TEXT.resolved().caret.index.unwrap(), false)
2444        },
2445        move |ctx, rich_select_line_start| {
2446            if rich_select_line_start {
2447                let id = WIDGET.id();
2448                if let Some(line_end) = ctx.leaf_info(id) {
2449                    let mut line_start = line_end;
2450                    let mut first = true;
2451                    for prev in line_start.rich_text_self_and_prev() {
2452                        let line_info = prev.rich_text_line_info();
2453                        line_start = prev;
2454                        if (line_info.starts_new_line && !line_info.is_wrap_start) || (line_info.ends_in_new_line && !first) {
2455                            break;
2456                        }
2457                        first = false;
2458                    }
2459                    if !replace_selection
2460                        && let Some(sel) = ctx.caret_selection_index_info()
2461                        && let Some(std::cmp::Ordering::Less) = sel.cmp_sibling_in(&line_start, &sel.root())
2462                    {
2463                        // rich line start already inside selection
2464                        return Some((sel.id(), false));
2465                    }
2466                    return Some((line_start.id(), line_start.id() != id));
2467                }
2468            }
2469            if replace_selection {
2470                return Some((WIDGET.id(), false));
2471            }
2472            Some((ctx.caret.selection_index.unwrap_or_else(|| WIDGET.id()), false))
2473        },
2474        move |start_of_last_line| {
2475            if replace_selection {
2476                let local_ctx = TEXT.laidout();
2477                let mut line_i = local_ctx.shaped_text.lines_len().saturating_sub(1);
2478                if !start_of_last_line && let Some(i) = TEXT.resolved().caret.index {
2479                    line_i = i.line;
2480                }
2481                if let Some(last_line) = local_ctx.shaped_text.line(line_i) {
2482                    return Some(CaretIndex {
2483                        index: last_line.actual_text_caret_range().start,
2484                        line: line_i,
2485                    });
2486                }
2487                None
2488            } else {
2489                let local_ctx = TEXT.resolved();
2490                Some(
2491                    local_ctx
2492                        .caret
2493                        .selection_index
2494                        .or(local_ctx.caret.index)
2495                        .unwrap_or(CaretIndex::ZERO),
2496                )
2497            }
2498        },
2499    )
2500}
2501fn local_select_line_word_nearest_to(replace_selection: bool, select_word: bool, window_point: DipPoint) {
2502    let mut caret = TEXT.resolve_caret();
2503
2504    //if there was at least one laidout
2505    let laidout = TEXT.laidout();
2506    if let Some(pos) = laidout
2507        .render_info
2508        .transform
2509        .inverse()
2510        .and_then(|t| t.project_point(window_point.to_px(laidout.render_info.scale_factor)))
2511    {
2512        //if has rendered
2513        if let Some(l) = laidout.shaped_text.nearest_line(pos.y) {
2514            let range = if select_word {
2515                let max_char = l.actual_text_caret_range().end;
2516                let mut r = l.nearest_seg(pos.x).map(|seg| seg.text_range()).unwrap_or_else(|| l.text_range());
2517                // don't select line-break at end of line
2518                r.start = r.start.min(max_char);
2519                r.end = r.end.min(max_char);
2520                r
2521            } else {
2522                l.actual_text_caret_range()
2523            };
2524
2525            let merge_with_selection = if replace_selection {
2526                None
2527            } else {
2528                caret.initial_selection.clone().map(|(s, _)| s).or_else(|| caret.selection_range())
2529            };
2530            if let Some(mut s) = merge_with_selection {
2531                let caret_at_start = range.start < s.start.index;
2532                s.start.index = s.start.index.min(range.start);
2533                s.end.index = s.end.index.max(range.end);
2534
2535                if caret_at_start {
2536                    caret.selection_index = Some(s.end);
2537                    caret.set_index(s.start);
2538                } else {
2539                    caret.selection_index = Some(s.start);
2540                    caret.set_index(s.end);
2541                }
2542            } else {
2543                let start = CaretIndex {
2544                    line: l.index(),
2545                    index: range.start,
2546                };
2547                let end = CaretIndex {
2548                    line: l.index(),
2549                    index: range.end,
2550                };
2551                caret.selection_index = Some(start);
2552                caret.set_index(end);
2553
2554                caret.initial_selection = Some((start..end, select_word));
2555            }
2556
2557            return;
2558        };
2559    }
2560
2561    if caret.index.is_none() {
2562        caret.set_index(CaretIndex::ZERO);
2563        caret.clear_selection();
2564    }
2565}