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                (
985                    CaretIndex {
986                        index: TEXT.resolved().segmented_text.text().len(),
987                        line: 0,
988                    },
989                    (),
990                )
991            },
992            |ctx, ()| Some((ctx.leaves().next().map(|w| w.id()).unwrap_or_else(|| WIDGET.id()), ())),
993            |()| Some(CaretIndex::ZERO),
994        )
995    }
996
997    /// Clear selection and keep the caret at the same position.
998    ///
999    /// This is the `Esc` shortcut operation.
1000    pub fn clear_selection() -> Self {
1001        Self::new_rich(
1002            |ctx| (ctx.caret.index.unwrap_or_else(|| WIDGET.id()), ()),
1003            |()| (TEXT.resolved().caret.index.unwrap_or(CaretIndex::ZERO), ()),
1004            |_, ()| None,
1005            |()| None,
1006        )
1007    }
1008}
1009
1010/// Operations that ignore the rich text context, for internal use only.
1011impl TextSelectOp {
1012    /// Like [`next`] but stays within the same text widget, ignores rich text context.
1013    ///
1014    /// [`next`]: Self::next
1015    pub fn local_next() -> Self {
1016        Self::new(|| {
1017            local_clear_next_prev(true, false);
1018        })
1019    }
1020
1021    /// Like [`select_next`] but stays within the same text widget, ignores rich text context.
1022    ///
1023    /// [`select_next`]: Self::select_next
1024    pub fn local_select_next() -> Self {
1025        Self::new(|| {
1026            local_select_next_prev(true, false);
1027        })
1028    }
1029
1030    /// Like [`prev`] but stays within the same text widget, ignores rich text context.
1031    ///
1032    /// [`prev`]: Self::prev
1033    pub fn local_prev() -> Self {
1034        Self::new(|| {
1035            local_clear_next_prev(false, false);
1036        })
1037    }
1038
1039    /// Like [`select_prev`] but stays within the same text widget, ignores rich text context.
1040    ///
1041    /// [`select_prev`]: Self::select_prev
1042    pub fn local_select_prev() -> Self {
1043        Self::new(|| {
1044            local_select_next_prev(false, false);
1045        })
1046    }
1047
1048    /// Like [`next_word`] but stays within the same text widget, ignores rich text context.
1049    ///
1050    /// [`next_word`]: Self::next_word
1051    pub fn local_next_word() -> Self {
1052        Self::new(|| {
1053            local_clear_next_prev(true, true);
1054        })
1055    }
1056
1057    /// Like [`select_next_word`] but stays within the same text widget, ignores rich text context.
1058    ///
1059    /// [`select_next_word`]: Self::select_next_word
1060    pub fn local_select_next_word() -> Self {
1061        Self::new(|| {
1062            local_select_next_prev(true, true);
1063        })
1064    }
1065
1066    /// Like [`prev_word`] but stays within the same text widget, ignores rich text context.
1067    ///
1068    /// [`prev_word`]: Self::prev_word
1069    pub fn local_prev_word() -> Self {
1070        Self::new(|| {
1071            local_clear_next_prev(false, true);
1072        })
1073    }
1074
1075    /// Like [`select_prev_word`] but stays within the same text widget, ignores rich text context.
1076    ///
1077    /// [`select_prev_word`]: Self::select_prev_word
1078    pub fn local_select_prev_word() -> Self {
1079        Self::new(|| {
1080            local_select_next_prev(false, true);
1081        })
1082    }
1083
1084    /// Like [`line_start`] but stays within the same text widget, ignores rich text context.
1085    ///
1086    /// [`line_start`]: Self::line_start
1087    pub fn local_line_start() -> Self {
1088        Self::new(|| local_line_start_end(true, false))
1089    }
1090
1091    /// Like [`select_line_start`] but stays within the same text widget, ignores rich text context.
1092    ///
1093    /// [`select_line_start`]: Self::select_line_start
1094    pub fn local_select_line_start() -> Self {
1095        Self::new(|| local_line_start_end(false, false))
1096    }
1097
1098    /// Like [`line_end`] but stays within the same text widget, ignores rich text context.
1099    ///
1100    /// [`line_end`]: Self::line_end
1101    pub fn local_line_end() -> Self {
1102        Self::new(|| local_line_start_end(true, true))
1103    }
1104
1105    /// Like [`select_line_end`] but stays within the same text widget, ignores rich text context.
1106    ///
1107    /// [`select_line_end`]: Self::select_line_end
1108    pub fn local_select_line_end() -> Self {
1109        Self::new(|| local_line_start_end(false, true))
1110    }
1111
1112    /// Like [`text_start`] but stays within the same text widget, ignores rich text context.
1113    ///
1114    /// [`text_start`]: Self::text_start
1115    pub fn local_text_start() -> Self {
1116        Self::new(|| local_text_start_end(true, false))
1117    }
1118
1119    /// Like [`select_text_start`] but stays within the same text widget, ignores rich text context.
1120    ///
1121    /// [`select_text_start`]: Self::select_text_start
1122    pub fn local_select_text_start() -> Self {
1123        Self::new(|| local_text_start_end(false, false))
1124    }
1125
1126    /// Like [`text_end`] but stays within the same text widget, ignores rich text context.
1127    ///
1128    /// [`text_end`]: Self::text_end
1129    pub fn local_text_end() -> Self {
1130        Self::new(|| local_text_start_end(true, true))
1131    }
1132
1133    /// Like [`select_text_end`] but stays within the same text widget, ignores rich text context.
1134    ///
1135    /// [`select_text_end`]: Self::select_text_end
1136    pub fn local_select_text_end() -> Self {
1137        Self::new(|| local_text_start_end(false, true))
1138    }
1139
1140    /// Like [`select_all`]  but stays within the same text widget, ignores rich text context.
1141    ///
1142    /// [`select_all`]: Self::select_all
1143    pub fn local_select_all() -> Self {
1144        Self::new(|| {
1145            let len = TEXT.resolved().segmented_text.text().len();
1146            let mut caret = TEXT.resolve_caret();
1147            caret.set_char_selection(0, len);
1148            caret.skip_next_scroll = true;
1149        })
1150    }
1151
1152    /// Like [`clear_selection`]  but stays within the same text widget, ignores rich text context.
1153    ///
1154    /// [`clear_selection`]: Self::clear_selection
1155    pub fn local_clear_selection() -> Self {
1156        Self::new(|| {
1157            let mut ctx = TEXT.resolve_caret();
1158            ctx.clear_selection();
1159        })
1160    }
1161
1162    /// Like [`line_up`]  but stays within the same text widget, ignores rich text context.
1163    ///
1164    /// [`line_up`]: Self::line_up
1165    pub fn local_line_up() -> Self {
1166        Self::new(|| local_line_up_down(true, -1))
1167    }
1168
1169    /// Like [`select_line_up`]  but stays within the same text widget, ignores rich text context.
1170    ///
1171    /// [`select_line_up`]: Self::select_line_up
1172    pub fn local_select_line_up() -> Self {
1173        Self::new(|| local_line_up_down(false, -1))
1174    }
1175
1176    /// Like [`line_down`]  but stays within the same text widget, ignores rich text context.
1177    ///
1178    /// [`line_down`]: Self::line_down
1179    pub fn local_line_down() -> Self {
1180        Self::new(|| local_line_up_down(true, 1))
1181    }
1182
1183    /// Like [`select_line_down`]  but stays within the same text widget, ignores rich text context.
1184    ///
1185    /// [`select_line_down`]: Self::select_line_down
1186    pub fn local_select_line_down() -> Self {
1187        Self::new(|| local_line_up_down(false, 1))
1188    }
1189
1190    /// Like [`page_up`]  but stays within the same text widget, ignores rich text context.
1191    ///
1192    /// [`page_up`]: Self::page_up
1193    pub fn local_page_up() -> Self {
1194        Self::new(|| local_page_up_down(true, -1))
1195    }
1196
1197    /// Like [`select_page_up`]  but stays within the same text widget, ignores rich text context.
1198    ///
1199    /// [`select_page_up`]: Self::select_page_up
1200    pub fn local_select_page_up() -> Self {
1201        Self::new(|| local_page_up_down(false, -1))
1202    }
1203
1204    /// Like [`page_down`]  but stays within the same text widget, ignores rich text context.
1205    ///
1206    /// [`page_down`]: Self::page_down
1207    pub fn local_page_down() -> Self {
1208        Self::new(|| local_page_up_down(true, 1))
1209    }
1210
1211    /// Like [`select_page_down`]  but stays within the same text widget, ignores rich text context.
1212    ///
1213    /// [`select_page_down`]: Self::select_page_down
1214    pub fn local_select_page_down() -> Self {
1215        Self::new(|| local_page_up_down(false, 1))
1216    }
1217
1218    /// Like [`nearest_to`]  but stays within the same text widget, ignores rich text context.
1219    ///
1220    /// [`nearest_to`]: Self::nearest_to
1221    pub fn local_nearest_to(window_point: DipPoint) -> Self {
1222        Self::new(move || {
1223            local_nearest_to(true, window_point);
1224        })
1225    }
1226
1227    /// Like [`select_nearest_to`]  but stays within the same text widget, ignores rich text context.
1228    ///
1229    /// [`select_nearest_to`]: Self::select_nearest_to
1230    pub fn local_select_nearest_to(window_point: DipPoint) -> Self {
1231        Self::new(move || {
1232            local_nearest_to(false, window_point);
1233        })
1234    }
1235
1236    /// Like [`select_index_nearest_to`]  but stays within the same text widget, ignores rich text context.
1237    ///
1238    /// [`select_index_nearest_to`]: Self::select_index_nearest_to
1239    pub fn local_select_index_nearest_to(window_point: DipPoint, move_selection_index: bool) -> Self {
1240        Self::new(move || {
1241            local_select_index_nearest_to(window_point, move_selection_index);
1242        })
1243    }
1244
1245    /// Like [`select_word_nearest_to`]  but stays within the same text widget, ignores rich text context.
1246    ///
1247    /// [`select_word_nearest_to`]: Self::select_word_nearest_to
1248    pub fn local_select_word_nearest_to(replace_selection: bool, window_point: DipPoint) -> Self {
1249        Self::new(move || local_select_line_word_nearest_to(replace_selection, true, window_point))
1250    }
1251
1252    /// Like [`select_line_nearest_to`]  but stays within the same text widget, ignores rich text context.
1253    ///
1254    /// [`select_line_nearest_to`]: Self::select_line_nearest_to
1255    pub fn local_select_line_nearest_to(replace_selection: bool, window_point: DipPoint) -> Self {
1256        Self::new(move || local_select_line_word_nearest_to(replace_selection, false, window_point))
1257    }
1258}
1259
1260impl TextSelectOp {
1261    /// New text select operation.
1262    ///
1263    /// The editable text widget that handles [`SELECT_CMD`] will call `op` during event handling in
1264    /// the [`node::layout_text`] context. You can position the caret using [`TEXT.resolve_caret`] and [`TEXT.resolve_rich_caret`],
1265    /// the text widget will detect changes to it and react accordingly (updating caret position and animation),
1266    /// the caret index is also snapped to the nearest grapheme start.
1267    ///
1268    /// [`TEXT.resolve_caret`]: super::node::TEXT::resolve_caret
1269    /// [`TEXT.resolve_rich_caret`]: super::node::TEXT::resolve_rich_caret
1270    pub fn new(op: impl FnMut() + Send + 'static) -> Self {
1271        Self {
1272            op: Arc::new(Mutex::new(op)),
1273        }
1274    }
1275
1276    /// New text selection operation with helpers for implementing rich selection.
1277    ///
1278    /// The input closures are:
1279    ///
1280    /// * `rich_caret_index` - Called if the op executes inside a rich text, must return the leaf widget that will contain the rich text caret.
1281    /// * `local_caret_index` - Called in the caret widget context, must return the local caret index.
1282    /// * `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.
1283    /// * `local_selection_index` - Called in selection end widget context, must return the local selection end index.
1284    ///
1285    /// Data can be passed between each stage with types `D0` from `rich_caret_index` to `local_caret_index`, `D1` from `local_caret_index` to
1286    /// `rich_selection_index` and `D2` from `rich_selection_index` to `local_selection_index`.
1287    ///
1288    /// 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.
1289    ///
1290    /// 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
1291    /// to the end of the previous rich leaf.
1292    pub fn new_rich<D0, D1, D2>(
1293        rich_caret_index: impl FnOnce(&RichText) -> (WidgetId, D0) + Send + 'static,
1294        local_caret_index: impl FnOnce(D0) -> (CaretIndex, D1) + Send + 'static,
1295        rich_selection_index: impl FnOnce(&RichText, D1) -> Option<(WidgetId, D2)> + Send + 'static,
1296        local_selection_index: impl FnOnce(D2) -> Option<CaretIndex> + Send + 'static,
1297    ) -> Self
1298    where
1299        D0: Default + Send + 'static,
1300        D1: Send + 'static,
1301        D2: Default + Send + 'static,
1302    {
1303        let mut f0 = Some(rich_caret_index);
1304        let mut f1 = Some(local_caret_index);
1305        let mut f2 = Some(rich_selection_index);
1306        let mut f3 = Some(local_selection_index);
1307        Self::new(move || {
1308            if let Some(ctx) = TEXT.try_rich() {
1309                rich_select_op_start(ctx, f0.take().unwrap(), f1.take().unwrap(), f2.take().unwrap(), f3.take().unwrap());
1310            } else {
1311                let (index, _) = f1.take().unwrap()(D0::default());
1312                let selection_index = f3.take().unwrap()(D2::default());
1313                let mut ctx = TEXT.resolve_caret();
1314                ctx.selection_index = selection_index;
1315                ctx.set_index(index);
1316            }
1317        })
1318    }
1319
1320    pub(super) fn call(self) {
1321        (self.op.lock())();
1322    }
1323}
1324
1325fn rich_select_op_start<D0: Send + 'static, D1: Send + 'static, D2: Send + 'static>(
1326    ctx: zng_app_context::RwLockReadGuardOwned<RichText>,
1327    rich_caret_index: impl FnOnce(&RichText) -> (WidgetId, D0),
1328    local_caret_index: impl FnOnce(D0) -> (CaretIndex, D1) + Send + 'static,
1329    rich_selection_index: impl FnOnce(&RichText, D1) -> Option<(WidgetId, D2)> + Send + 'static,
1330    local_selection_index: impl FnOnce(D2) -> Option<CaretIndex> + Send + 'static,
1331) {
1332    let (index, d0) = rich_caret_index(&ctx);
1333    if index == WIDGET.id() {
1334        rich_select_op_get_caret(ctx, index, d0, local_caret_index, rich_selection_index, local_selection_index);
1335    } else {
1336        let mut d0 = Some(d0);
1337        let mut f0 = Some(local_caret_index);
1338        let mut f1 = Some(rich_selection_index);
1339        let mut f2 = Some(local_selection_index);
1340        notify_leaf_select_op(
1341            index,
1342            TextSelectOp::new(move || {
1343                if let Some(ctx) = TEXT.try_rich()
1344                    && index == WIDGET.id()
1345                {
1346                    rich_select_op_get_caret(
1347                        ctx,
1348                        index,
1349                        d0.take().unwrap(),
1350                        f0.take().unwrap(),
1351                        f1.take().unwrap(),
1352                        f2.take().unwrap(),
1353                    );
1354                }
1355            }),
1356        );
1357    }
1358}
1359fn rich_select_op_get_caret<D0, D1, D2: Send + 'static>(
1360    ctx: zng_app_context::RwLockReadGuardOwned<RichText>,
1361    rich_caret_index: WidgetId,
1362    d0: D0,
1363    local_caret_index: impl FnOnce(D0) -> (CaretIndex, D1),
1364    rich_selection_index: impl FnOnce(&RichText, D1) -> Option<(WidgetId, D2)>,
1365    local_selection_index: impl FnOnce(D2) -> Option<CaretIndex> + Send + 'static,
1366) {
1367    let (index, d1) = local_caret_index(d0);
1368    {
1369        let mut ctx = TEXT.resolve_caret();
1370        ctx.set_index(index);
1371    }
1372
1373    match rich_selection_index(&ctx, d1) {
1374        Some((selection_index, d2)) => {
1375            if selection_index == WIDGET.id() {
1376                rich_select_op_get_selection(ctx, (rich_caret_index, index), selection_index, d2, local_selection_index);
1377            } else {
1378                let mut d2 = Some(d2);
1379                let mut f0 = Some(local_selection_index);
1380                notify_leaf_select_op(
1381                    selection_index,
1382                    TextSelectOp::new(move || {
1383                        if let Some(ctx) = TEXT.try_rich()
1384                            && selection_index == WIDGET.id()
1385                        {
1386                            rich_select_op_get_selection(
1387                                ctx,
1388                                (rich_caret_index, index),
1389                                selection_index,
1390                                d2.take().unwrap(),
1391                                f0.take().unwrap(),
1392                            );
1393                        }
1394                    }),
1395                );
1396            }
1397        }
1398        None => rich_select_op_finish(ctx, (rich_caret_index, index), None),
1399    }
1400}
1401fn rich_select_op_get_selection<D2>(
1402    ctx: zng_app_context::RwLockReadGuardOwned<RichText>,
1403    rich_caret_index: (WidgetId, CaretIndex),
1404    rich_selection_index: WidgetId,
1405    d2: D2,
1406    local_selection_index: impl FnOnce(D2) -> Option<CaretIndex>,
1407) {
1408    if let Some(index) = local_selection_index(d2) {
1409        let mut local_ctx = TEXT.resolve_caret();
1410        local_ctx.selection_index = Some(index);
1411        local_ctx.index_version += 1;
1412        rich_select_op_finish(ctx, rich_caret_index, Some((rich_selection_index, index)));
1413    } else {
1414        rich_select_op_finish(ctx, rich_caret_index, None);
1415    }
1416}
1417fn rich_select_op_finish(
1418    ctx: zng_app_context::RwLockReadGuardOwned<RichText>,
1419    rich_caret_index: (WidgetId, CaretIndex),
1420    rich_selection_index: Option<(WidgetId, CaretIndex)>,
1421) {
1422    if let Some(mut index) = ctx.leaf_info(rich_caret_index.0) {
1423        if rich_caret_index.1.index == 0 {
1424            // index 0 is the end of previous leaf
1425            if let Some(prev) = index.rich_text_prev().next() {
1426                index = prev;
1427                notify_leaf_select_op(
1428                    index.id(),
1429                    TextSelectOp::new(move || {
1430                        let end = TEXT.resolved().segmented_text.text().len();
1431                        TEXT.resolve_caret().set_char_index(end);
1432                    }),
1433                );
1434            }
1435        }
1436        if let Some(rich_selection_index) = rich_selection_index {
1437            if let Some(mut selection) = ctx.leaf_info(rich_selection_index.0) {
1438                if rich_selection_index.1.index == 0 {
1439                    // selection 0 is the end of the previous leaf
1440                    if let Some(prev) = selection.rich_text_prev().next() {
1441                        selection = prev;
1442                        notify_leaf_select_op(
1443                            selection.id(),
1444                            TextSelectOp::new(move || {
1445                                let end = TEXT.resolved().segmented_text.text().len();
1446                                TEXT.resolve_caret().set_char_index(end);
1447                            }),
1448                        );
1449                    }
1450                }
1451
1452                drop(ctx);
1453                TEXT.resolve_rich_caret().update_selection(&index, Some(&selection), false, false);
1454            }
1455        } else {
1456            // no selection
1457
1458            drop(ctx);
1459            TEXT.resolve_rich_caret().update_selection(&index, None, false, false);
1460        }
1461    }
1462}
1463
1464fn rich_clear_next_prev(is_next: bool, is_word: bool) -> TextSelectOp {
1465    TextSelectOp::new_rich(
1466        // get prev/next leaf widget
1467        move |ctx| {
1468            if let Some(i) = ctx.caret_index_info()
1469                && let Some(s) = ctx.caret_selection_index_info()
1470            {
1471                // clear selection, next places caret at end of selection, prev at start
1472
1473                let (a, b) = match i.cmp_sibling_in(&s, &i.root()).unwrap() {
1474                    cmp::Ordering::Less | cmp::Ordering::Equal => (&i, &s),
1475                    cmp::Ordering::Greater => (&s, &i),
1476                };
1477
1478                let c = if is_next { b } else { a };
1479
1480                (c.id(), false) // false to just collapse to selection
1481            } else {
1482                // no selection, actually move caret
1483
1484                let local_ctx = TEXT.resolved();
1485                if is_next {
1486                    let index = local_ctx.caret.index.unwrap_or(CaretIndex::ZERO).index;
1487                    if index == local_ctx.segmented_text.text().len() {
1488                        // next from end, check if has next sibling
1489                        if let Some(info) = ctx.leaf_info(WIDGET.id())
1490                            && let Some(next) = info.rich_text_next().next()
1491                        {
1492                            return (next.id(), true);
1493                        }
1494                    }
1495
1496                    // caret stays inside
1497                    (WIDGET.id(), false)
1498                } else {
1499                    // !is_next
1500
1501                    let cutout = if is_word { local_ctx.segmented_text.next_word_index(0) } else { 1 };
1502                    if local_ctx.caret.index.unwrap_or(CaretIndex::ZERO).index <= cutout {
1503                        // next moves to the start (or is already in start)
1504
1505                        if let Some(info) = ctx.leaf_info(WIDGET.id())
1506                            && let Some(prev) = info.rich_text_prev().next()
1507                        {
1508                            return (prev.id(), true);
1509                        }
1510                    }
1511
1512                    (WIDGET.id(), false)
1513                }
1514            }
1515        },
1516        // get caret in the prev/next widget
1517        move |is_from_sibling| {
1518            if is_from_sibling {
1519                if is_next {
1520                    (CaretIndex { index: 1, line: 0 }, ())
1521                } else {
1522                    let local_ctx = TEXT.resolved();
1523                    (
1524                        CaretIndex {
1525                            index: local_ctx.segmented_text.text().len(),
1526                            line: 0,
1527                        },
1528                        (),
1529                    )
1530                }
1531            } else {
1532                local_clear_next_prev(is_next, is_word);
1533                (TEXT.resolved().caret.index.unwrap_or(CaretIndex::ZERO), ())
1534            }
1535        },
1536        |_, _| None,
1537        |()| None,
1538    )
1539}
1540fn local_clear_next_prev(is_next: bool, is_word: bool) {
1541    // compute next caret position
1542    let ctx = TEXT.resolved();
1543    let current_index = ctx.caret.index.unwrap_or(CaretIndex::ZERO);
1544    let mut next_index = current_index;
1545    if let Some(selection) = ctx.caret.selection_range() {
1546        next_index.index = if is_next { selection.end.index } else { selection.start.index };
1547    } else {
1548        next_index.index = if is_next {
1549            let from = current_index.index;
1550            if is_word {
1551                ctx.segmented_text.next_word_index(from)
1552            } else {
1553                ctx.segmented_text.next_insert_index(from)
1554            }
1555        } else {
1556            let from = current_index.index;
1557            if is_word {
1558                ctx.segmented_text.prev_word_index(from)
1559            } else {
1560                ctx.segmented_text.prev_insert_index(from)
1561            }
1562        };
1563    }
1564
1565    drop(ctx);
1566
1567    let mut ctx = TEXT.resolve_caret();
1568    ctx.clear_selection();
1569    ctx.set_index(next_index);
1570    ctx.used_retained_x = false;
1571}
1572
1573fn rich_select_next_prev(is_next: bool, is_word: bool) -> TextSelectOp {
1574    TextSelectOp::new_rich(
1575        // get prev/next leaf widget
1576        move |ctx| {
1577            let local_ctx = TEXT.resolved();
1578
1579            let index = local_ctx.caret.index.unwrap_or(CaretIndex::ZERO).index;
1580
1581            if is_next {
1582                if index == local_ctx.segmented_text.text().len() {
1583                    // next from end
1584                    if let Some(info) = ctx.leaf_info(WIDGET.id())
1585                        && let Some(next) = info.rich_text_next().next()
1586                    {
1587                        return (next.id(), true);
1588                    }
1589                }
1590            } else {
1591                // !is_next
1592
1593                let cutout = if is_word { local_ctx.segmented_text.next_word_index(0) } else { 1 };
1594                if local_ctx.caret.index.unwrap_or(CaretIndex::ZERO).index <= cutout {
1595                    // next moves to the start (or is already in start)
1596                    if let Some(info) = ctx.leaf_info(WIDGET.id())
1597                        && let Some(prev) = info.rich_text_prev().next()
1598                    {
1599                        return (prev.id(), true);
1600                    }
1601                }
1602            }
1603            (WIDGET.id(), false)
1604        },
1605        // get caret in the prev/next widget
1606        move |is_from_sibling| {
1607            let id = WIDGET.id();
1608            if is_from_sibling {
1609                if is_next {
1610                    // caret was at sibling end
1611                    (CaretIndex { index: 1, line: 0 }, id)
1612                } else {
1613                    // caret was at sibling start or moves to sibling start (that is the same as our end)
1614                    let len = TEXT.resolved().segmented_text.text().len();
1615                    (CaretIndex { index: len, line: 0 }, id)
1616                }
1617            } else {
1618                local_select_next_prev(is_next, is_word);
1619                (TEXT.resolved().caret.index.unwrap_or(CaretIndex::ZERO), id)
1620            }
1621        },
1622        // get selection_index leaf widget
1623        |ctx, index| Some((ctx.caret.selection_index.unwrap_or(index), ())),
1624        // get local selection_index
1625        |()| {
1626            let local_ctx = TEXT.resolved();
1627            Some(
1628                local_ctx
1629                    .caret
1630                    .selection_index
1631                    .unwrap_or(local_ctx.caret.index.unwrap_or(CaretIndex::ZERO)),
1632            )
1633        },
1634    )
1635}
1636fn local_select_next_prev(is_next: bool, is_word: bool) {
1637    // compute next caret position
1638    let ctx = TEXT.resolved();
1639    let current_index = ctx.caret.index.unwrap_or(CaretIndex::ZERO);
1640    let mut next_index = current_index;
1641    next_index.index = if is_next {
1642        if is_word {
1643            ctx.segmented_text.next_word_index(current_index.index)
1644        } else {
1645            ctx.segmented_text.next_insert_index(current_index.index)
1646        }
1647    } else {
1648        // is_prev
1649        if is_word {
1650            ctx.segmented_text.prev_word_index(current_index.index)
1651        } else {
1652            ctx.segmented_text.prev_insert_index(current_index.index)
1653        }
1654    };
1655    drop(ctx);
1656
1657    let mut ctx = TEXT.resolve_caret();
1658    if ctx.selection_index.is_none() {
1659        ctx.selection_index = Some(current_index);
1660    }
1661    ctx.set_index(next_index);
1662    ctx.used_retained_x = false;
1663}
1664
1665fn rich_up_down(clear_selection: bool, is_down: bool, is_page: bool) -> TextSelectOp {
1666    TextSelectOp::new_rich(
1667        move |ctx| {
1668            let resolved = TEXT.resolved();
1669            let laidout = TEXT.laidout();
1670
1671            let local_line_i = resolved.caret.index.unwrap_or(CaretIndex::ZERO).line;
1672            let last_line_i = laidout.shaped_text.lines_len().saturating_sub(1);
1673            let next_local_line_i = local_line_i.saturating_add_signed(if is_down { 1 } else { -1 }).min(last_line_i);
1674
1675            let page_h = if is_page { laidout.viewport.height } else { Px(0) };
1676
1677            let mut need_spatial_search = local_line_i == next_local_line_i; // if already at first/last line
1678
1679            if !need_spatial_search {
1680                if is_page {
1681                    if let Some(local_line) = laidout.shaped_text.line(local_line_i) {
1682                        if is_down {
1683                            if let Some(last_line) = laidout.shaped_text.line(last_line_i) {
1684                                let max_local_y = last_line.rect().max_y() - local_line.rect().min_y();
1685                                need_spatial_search = max_local_y < page_h; // if page distance is greater than maximum down distance
1686                            }
1687                        } else if let Some(first_line) = laidout.shaped_text.line(0) {
1688                            let max_local_y = local_line.rect().max_y() - first_line.rect().min_y();
1689                            need_spatial_search = max_local_y < page_h; // if page distance is greater than maximum up distance
1690                        }
1691                    }
1692                } else if let Some(next_local_line) = laidout.shaped_text.line(next_local_line_i) {
1693                    let r = next_local_line.rect();
1694                    let x = laidout.caret_retained_x;
1695                    need_spatial_search = r.min_x() > x || r.max_x() < x; // if next local line does not contain ideal caret horizontally
1696                }
1697            }
1698
1699            if need_spatial_search
1700                && let Some(local_line) = laidout.shaped_text.line(local_line_i)
1701                && let Some(root_info) = ctx.root_info()
1702            {
1703                // line ok, rich context ok
1704                let r = local_line.rect();
1705                let local_point = PxPoint::new(laidout.caret_retained_x, r.origin.y + r.size.height / Px(2));
1706                let local_info = WIDGET.info();
1707                let local_to_window = local_info.inner_transform();
1708
1709                let local_cut_y = if is_down { local_point.y + page_h } else { local_point.y - page_h };
1710                let window_cut_y = local_to_window
1711                    .transform_point(PxPoint::new(Px(0), local_cut_y))
1712                    .unwrap_or_default()
1713                    .y;
1714
1715                if let Some(window_point) = local_to_window.transform_point(local_point) {
1716                    // transform ok
1717
1718                    // find the nearest sibling considering only the prev/next rich lines
1719                    let local_line_info = local_info.rich_text_line_info();
1720                    let filter = |other: &WidgetInfo, rect: PxRect, row_i, rows_len| {
1721                        if is_down {
1722                            if rect.max_y() < window_cut_y {
1723                                // rectangle is before the page y line or the the current line
1724                                return false;
1725                            }
1726                        } else if rect.min_y() > window_cut_y {
1727                            // rectangle is after the page y line or the current line
1728                            return false;
1729                        }
1730
1731                        match local_info.cmp_sibling_in(other, &root_info).unwrap() {
1732                            cmp::Ordering::Less => {
1733                                // other is after local
1734
1735                                if !is_down {
1736                                    return false;
1737                                }
1738                                if local_line_i < last_line_i {
1739                                    return true; // local started next line
1740                                }
1741                                for next in local_info.rich_text_next() {
1742                                    let line_info = next.rich_text_line_info();
1743                                    if line_info.starts_new_line {
1744                                        return true; // `other` starts new line or is after this line break
1745                                    }
1746                                    if line_info.ends_in_new_line {
1747                                        if &next == other {
1748                                            return row_i > 0; // `other` rect is not in the same line
1749                                        }
1750                                        return true; // `other` starts after this line break
1751                                    }
1752                                    if &next == other {
1753                                        return false; // `other` is in same line
1754                                    }
1755                                }
1756                                unreachable!() // filter only called if is sibling, cmp ensures that is next sibling
1757                            }
1758                            cmp::Ordering::Greater => {
1759                                // other is before local
1760
1761                                if is_down {
1762                                    return false;
1763                                }
1764                                if local_line_i > 0 || local_line_info.starts_new_line {
1765                                    return true; // local started line, all prev wgt in prev lines
1766                                }
1767                                for prev in local_info.rich_text_prev() {
1768                                    let line_info = prev.rich_text_line_info();
1769                                    if line_info.ends_in_new_line {
1770                                        if &prev == other {
1771                                            return row_i < rows_len - 1; // `other` rect is not in the same line
1772                                        }
1773                                        return true; // `other` ends before this linebreak
1774                                    }
1775                                    if line_info.starts_new_line {
1776                                        return &prev != other; // `other` starts the line (same line) or not (is before)
1777                                    }
1778
1779                                    if &prev == other {
1780                                        return false; // `other` is in same line
1781                                    }
1782                                }
1783                                unreachable!()
1784                            }
1785                            cmp::Ordering::Equal => false,
1786                        }
1787                    };
1788                    if let Some(next) = root_info.rich_text_nearest_leaf_filtered(window_point, filter) {
1789                        // found nearest sibling on the next/prev rich lines
1790
1791                        let next_info = next.clone();
1792
1793                        // get the next(wgt) local line that is in the next/prev rich line
1794                        let mut next_line = 0;
1795                        if let Some(next_inline_rows_len) = next_info.bounds_info().inline().map(|i| i.rows.len())
1796                            && next_inline_rows_len > 1
1797                        {
1798                            if is_down {
1799                                // next is logical next
1800
1801                                if local_line_i == last_line_i {
1802                                    // local did not start next line
1803
1804                                    for l_next in local_info.rich_text_next() {
1805                                        let line_info = l_next.rich_text_line_info();
1806                                        if line_info.starts_new_line || line_info.ends_in_new_line {
1807                                            // found rich line end
1808                                            if l_next == next {
1809                                                // its inside the `next`, meaning it starts on the same rich line
1810                                                next_line = 1;
1811                                            }
1812                                            break;
1813                                        }
1814                                    }
1815                                }
1816                            } else {
1817                                // next is up (logical prev)
1818                                next_line = next_inline_rows_len - 1;
1819
1820                                if local_line_i == 0 && !local_line_info.starts_new_line {
1821                                    // local did not start current line
1822
1823                                    for l_prev in local_info.rich_text_prev() {
1824                                        let line_info = l_prev.rich_text_line_info();
1825                                        if line_info.starts_new_line || line_info.ends_in_new_line {
1826                                            // found rich line start
1827                                            if l_prev == next {
1828                                                // its inside the `next`, meaning it ends on the same rich line
1829                                                next_line -= 1;
1830                                            }
1831                                            break;
1832                                        }
1833                                    }
1834                                }
1835                            }
1836                        }
1837                        return (next.id(), Some((window_point.x, next_line)));
1838                    }
1839                }
1840            }
1841
1842            // when can't go down within local goes to text start/end
1843            let mut cant_go_down_up = if is_down {
1844                // if already at last line
1845                local_line_i == last_line_i
1846            } else {
1847                // if already at first line
1848                local_line_i == 0
1849            };
1850            if is_page
1851                && !cant_go_down_up
1852                && let Some(local_line) = laidout.shaped_text.line(local_line_i)
1853            {
1854                if is_down {
1855                    if let Some(last_line) = laidout.shaped_text.line(last_line_i) {
1856                        // if page down distance greater than distance to last line
1857                        let max_local_y = last_line.rect().max_y() - local_line.rect().min_y();
1858                        cant_go_down_up = max_local_y < page_h;
1859                    }
1860                } else if let Some(first_line) = laidout.shaped_text.line(0) {
1861                    // if page up distance greater than distance to first line
1862                    let max_local_y = local_line.rect().max_y() - first_line.rect().min_y();
1863                    cant_go_down_up = max_local_y < page_h;
1864                }
1865            }
1866            if cant_go_down_up {
1867                if is_down {
1868                    if let Some(end) = ctx.leaves_rev().next() {
1869                        return (end.id(), None);
1870                    }
1871                } else if let Some(start) = ctx.leaves().next() {
1872                    return (start.id(), None);
1873                }
1874            }
1875
1876            (WIDGET.id(), None) // only local nav
1877        },
1878        move |rich_request| {
1879            if let Some((window_x, line_i)) = rich_request {
1880                let local_x = WIDGET
1881                    .info()
1882                    .inner_transform()
1883                    .inverse()
1884                    .and_then(|t| t.transform_point(PxPoint::new(window_x, Px(0))))
1885                    .unwrap_or_default()
1886                    .x;
1887                TEXT.set_caret_retained_x(local_x);
1888                let local_ctx = TEXT.laidout();
1889                if let Some(line) = local_ctx.shaped_text.line(line_i) {
1890                    let index = match line.nearest_seg(local_x) {
1891                        Some(s) => s.nearest_char_index(local_x, TEXT.resolved().segmented_text.text()),
1892                        None => line.text_range().end,
1893                    };
1894                    let index = CaretIndex { index, line: line_i };
1895                    TEXT.resolve_caret().used_retained_x = true; // new_rich does not set this
1896                    return (index, ());
1897                }
1898            }
1899            let diff = if is_down { 1 } else { -1 };
1900            if is_page {
1901                local_page_up_down(clear_selection, diff);
1902            } else {
1903                local_line_up_down(clear_selection, diff);
1904            }
1905            (TEXT.resolved().caret.index.unwrap(), ())
1906        },
1907        move |ctx, ()| {
1908            if clear_selection {
1909                None
1910            } else {
1911                Some((ctx.caret.selection_index.or(ctx.caret.index).unwrap_or_else(|| WIDGET.id()), ()))
1912            }
1913        },
1914        move |()| {
1915            if clear_selection {
1916                None
1917            } else {
1918                let local_ctx = TEXT.resolved();
1919                Some(
1920                    local_ctx
1921                        .caret
1922                        .selection_index
1923                        .or(local_ctx.caret.index)
1924                        .unwrap_or(CaretIndex::ZERO),
1925                )
1926            }
1927        },
1928    )
1929}
1930fn local_line_up_down(clear_selection: bool, diff: i8) {
1931    let diff = diff as isize;
1932
1933    let mut caret = TEXT.resolve_caret();
1934    let mut i = caret.index.unwrap_or(CaretIndex::ZERO);
1935    if clear_selection {
1936        caret.clear_selection();
1937    } else if caret.selection_index.is_none() {
1938        caret.selection_index = Some(i);
1939    }
1940    caret.used_retained_x = true;
1941
1942    let laidout = TEXT.laidout();
1943
1944    if laidout.caret_origin.is_some() {
1945        let last_line = laidout.shaped_text.lines_len().saturating_sub(1);
1946        let li = i.line;
1947        let next_li = li.saturating_add_signed(diff).min(last_line);
1948        if li != next_li {
1949            drop(caret);
1950            let resolved = TEXT.resolved();
1951            match laidout.shaped_text.line(next_li) {
1952                Some(l) => {
1953                    i.line = next_li;
1954                    i.index = match l.nearest_seg(laidout.caret_retained_x) {
1955                        Some(s) => s.nearest_char_index(laidout.caret_retained_x, resolved.segmented_text.text()),
1956                        None => l.text_range().end,
1957                    }
1958                }
1959                None => i = CaretIndex::ZERO,
1960            };
1961            i.index = resolved.segmented_text.snap_grapheme_boundary(i.index);
1962            drop(resolved);
1963            caret = TEXT.resolve_caret();
1964            caret.set_index(i);
1965        } else if diff == -1 {
1966            caret.set_char_index(0);
1967        } else if diff == 1 {
1968            drop(caret);
1969            let len = TEXT.resolved().segmented_text.text().len();
1970            caret = TEXT.resolve_caret();
1971            caret.set_char_index(len);
1972        }
1973    }
1974
1975    if caret.index.is_none() {
1976        caret.set_index(CaretIndex::ZERO);
1977        caret.clear_selection();
1978    }
1979}
1980fn local_page_up_down(clear_selection: bool, diff: i8) {
1981    let diff = diff as i32;
1982
1983    let mut caret = TEXT.resolve_caret();
1984    let mut i = caret.index.unwrap_or(CaretIndex::ZERO);
1985    if clear_selection {
1986        caret.clear_selection();
1987    } else if caret.selection_index.is_none() {
1988        caret.selection_index = Some(i);
1989    }
1990
1991    let laidout = TEXT.laidout();
1992
1993    let page_y = laidout.viewport.height * Px(diff);
1994    caret.used_retained_x = true;
1995    if laidout.caret_origin.is_some() {
1996        let li = i.line;
1997        if diff == -1 && li == 0 {
1998            caret.set_char_index(0);
1999        } else if diff == 1 && li == laidout.shaped_text.lines_len() - 1 {
2000            drop(caret);
2001            let len = TEXT.resolved().segmented_text.text().len();
2002            caret = TEXT.resolve_caret();
2003            caret.set_char_index(len);
2004        } else if let Some(li) = laidout.shaped_text.line(li) {
2005            drop(caret);
2006            let resolved = TEXT.resolved();
2007
2008            let target_line_y = li.rect().origin.y + page_y;
2009            match laidout.shaped_text.nearest_line(target_line_y) {
2010                Some(l) => {
2011                    i.line = l.index();
2012                    i.index = match l.nearest_seg(laidout.caret_retained_x) {
2013                        Some(s) => s.nearest_char_index(laidout.caret_retained_x, resolved.segmented_text.text()),
2014                        None => l.text_range().end,
2015                    }
2016                }
2017                None => i = CaretIndex::ZERO,
2018            };
2019            i.index = resolved.segmented_text.snap_grapheme_boundary(i.index);
2020
2021            drop(resolved);
2022            caret = TEXT.resolve_caret();
2023
2024            caret.set_index(i);
2025        }
2026    }
2027
2028    if caret.index.is_none() {
2029        caret.set_index(CaretIndex::ZERO);
2030        caret.clear_selection();
2031    }
2032}
2033
2034fn rich_line_start_end(clear_selection: bool, is_end: bool) -> TextSelectOp {
2035    TextSelectOp::new_rich(
2036        // get caret widget, rich line start/end
2037        move |ctx| {
2038            let from_id = WIDGET.id();
2039            if let Some(c) = ctx.leaf_info(WIDGET.id()) {
2040                let local_line = TEXT.resolved().caret.index.unwrap_or(CaretIndex::ZERO).line;
2041                if is_end {
2042                    let last_line = TEXT.laidout().shaped_text.lines_len() - 1;
2043                    if local_line == last_line {
2044                        // current line can end in a next sibling
2045
2046                        let mut prev_id = c.id();
2047                        for c in c.rich_text_next() {
2048                            let line_info = c.rich_text_line_info();
2049                            if line_info.starts_new_line && !line_info.is_wrap_start {
2050                                return (prev_id, Some(from_id));
2051                            } else if line_info.ends_in_new_line {
2052                                return (c.id(), Some(from_id));
2053                            }
2054                            prev_id = c.id();
2055                        }
2056
2057                        // text end
2058                        return (prev_id, Some(from_id));
2059                    }
2060                } else {
2061                    // !is_end
2062
2063                    if local_line == 0 {
2064                        // current line can start in a prev sibling
2065
2066                        let mut last_id = c.id();
2067                        let mut first = true;
2068                        for c in c.rich_text_self_and_prev() {
2069                            let line_info = c.rich_text_line_info();
2070                            if (line_info.starts_new_line && !line_info.is_wrap_start) || (line_info.ends_in_new_line && !first) {
2071                                return (c.id(), Some(from_id));
2072                            }
2073                            last_id = c.id();
2074                            first = false;
2075                        }
2076
2077                        // text start
2078                        return (last_id, Some(from_id));
2079                    }
2080                }
2081            }
2082            (from_id, None)
2083        },
2084        // get local caret index in the rich line start/end widget
2085        move |from_id| {
2086            if let Some(from_id) = from_id
2087                && from_id != WIDGET.id()
2088            {
2089                // ensure the caret is at a start/end from the other sibling for `local_line_start_end`
2090                if is_end {
2091                    TEXT.resolve_caret().index = Some(CaretIndex::ZERO);
2092                } else {
2093                    let local_ctx = TEXT.laidout();
2094                    let line = local_ctx.shaped_text.lines_len() - 1;
2095                    let index = local_ctx.shaped_text.line(line).unwrap().text_caret_range().end;
2096                    drop(local_ctx);
2097                    TEXT.resolve_caret().index = Some(CaretIndex { index, line })
2098                }
2099            }
2100            local_line_start_end(clear_selection, is_end);
2101
2102            (TEXT.resolved().caret.index.unwrap(), from_id)
2103        },
2104        // get the selection index widget, line selection always updates from the caret
2105        move |ctx, from_id| {
2106            if clear_selection {
2107                return None;
2108            }
2109            Some((ctx.caret.selection_index.or(from_id).unwrap_or_else(|| WIDGET.id()), ()))
2110        },
2111        // get the selection index
2112        move |()| {
2113            if clear_selection {
2114                return None;
2115            }
2116            let local_ctx = TEXT.resolved();
2117            Some(
2118                local_ctx
2119                    .caret
2120                    .selection_index
2121                    .or(local_ctx.caret.index)
2122                    .unwrap_or(CaretIndex::ZERO),
2123            )
2124        },
2125    )
2126}
2127fn local_line_start_end(clear_selection: bool, is_end: bool) {
2128    let mut ctx = TEXT.resolve_caret();
2129    let mut i = ctx.index.unwrap_or(CaretIndex::ZERO);
2130
2131    if clear_selection {
2132        ctx.clear_selection();
2133    } else if ctx.selection_index.is_none() {
2134        ctx.selection_index = Some(i);
2135    }
2136
2137    if let Some(li) = TEXT.laidout().shaped_text.line(i.line) {
2138        i.index = if is_end {
2139            li.actual_text_caret_range().end
2140        } else {
2141            li.actual_text_range().start
2142        };
2143        ctx.set_index(i);
2144        ctx.used_retained_x = false;
2145    }
2146}
2147
2148fn rich_text_start_end(clear_selection: bool, is_end: bool) -> TextSelectOp {
2149    TextSelectOp::new_rich(
2150        move |ctx| {
2151            let from_id = WIDGET.id();
2152            let id = if is_end { ctx.leaves_rev().next() } else { ctx.leaves().next() }.map(|w| w.id());
2153            (id.unwrap_or(from_id), Some(from_id))
2154        },
2155        move |from_id| {
2156            local_text_start_end(clear_selection, is_end);
2157            (TEXT.resolved().caret.index.unwrap(), from_id)
2158        },
2159        // get the selection index widget, line selection always updates from the caret
2160        move |ctx, from_id| {
2161            if clear_selection {
2162                return None;
2163            }
2164            Some((ctx.caret.selection_index.or(from_id).unwrap_or_else(|| WIDGET.id()), ()))
2165        },
2166        // get the selection index
2167        move |()| {
2168            if clear_selection {
2169                return None;
2170            }
2171            let local_ctx = TEXT.resolved();
2172            Some(
2173                local_ctx
2174                    .caret
2175                    .selection_index
2176                    .or(local_ctx.caret.index)
2177                    .unwrap_or(CaretIndex::ZERO),
2178            )
2179        },
2180    )
2181}
2182fn local_text_start_end(clear_selection: bool, is_end: bool) {
2183    let idx = if is_end { TEXT.resolved().segmented_text.text().len() } else { 0 };
2184
2185    let mut ctx = TEXT.resolve_caret();
2186    let mut i = ctx.index.unwrap_or(CaretIndex::ZERO);
2187    if clear_selection {
2188        ctx.clear_selection();
2189    } else if ctx.selection_index.is_none() {
2190        ctx.selection_index = Some(i);
2191    }
2192    i.index = idx;
2193    ctx.set_index(i);
2194    ctx.used_retained_x = false;
2195}
2196
2197/// `clear_selection` is `replace_selection` for `is_word` mode.
2198fn rich_nearest_char_word_to(clear_selection: bool, window_point: DipPoint, is_word: bool) -> TextSelectOp {
2199    TextSelectOp::new_rich(
2200        move |ctx| {
2201            if let Some(root) = ctx.root_info()
2202                && let Some(nearest_leaf) = root.rich_text_nearest_leaf(window_point.to_px(root.tree().scale_factor()))
2203            {
2204                return (nearest_leaf.id(), ());
2205            }
2206            (WIDGET.id(), ())
2207        },
2208        move |()| {
2209            if is_word {
2210                local_select_line_word_nearest_to(clear_selection, true, window_point)
2211            } else {
2212                local_nearest_to(clear_selection, window_point)
2213            }
2214            (TEXT.resolved().caret.index.unwrap(), ())
2215        },
2216        move |ctx, ()| {
2217            if clear_selection {
2218                if is_word && TEXT.resolved().caret.selection_index.is_some() {
2219                    Some((WIDGET.id(), ()))
2220                } else {
2221                    None
2222                }
2223            } else {
2224                Some((ctx.caret.selection_index.unwrap_or_else(|| WIDGET.id()), ()))
2225            }
2226        },
2227        move |()| {
2228            if clear_selection {
2229                if is_word { TEXT.resolved().caret.selection_index } else { None }
2230            } else {
2231                let local_ctx = TEXT.resolved();
2232                Some(
2233                    local_ctx
2234                        .caret
2235                        .selection_index
2236                        .or(local_ctx.caret.index)
2237                        .unwrap_or(CaretIndex::ZERO),
2238                )
2239            }
2240        },
2241    )
2242}
2243fn local_nearest_to(clear_selection: bool, window_point: DipPoint) {
2244    let mut caret = TEXT.resolve_caret();
2245    let mut i = caret.index.unwrap_or(CaretIndex::ZERO);
2246
2247    if clear_selection {
2248        caret.clear_selection();
2249    } else if caret.selection_index.is_none() {
2250        caret.selection_index = Some(i);
2251    } else if let Some((_, is_word)) = caret.initial_selection.clone() {
2252        drop(caret);
2253        return local_select_line_word_nearest_to(false, is_word, window_point);
2254    }
2255
2256    caret.used_retained_x = false;
2257
2258    //if there was at least one layout
2259    let laidout = TEXT.laidout();
2260    if let Some(pos) = laidout
2261        .render_info
2262        .transform
2263        .inverse()
2264        .and_then(|t| t.project_point(window_point.to_px(laidout.render_info.scale_factor)))
2265    {
2266        drop(caret);
2267        let resolved = TEXT.resolved();
2268
2269        //if has rendered
2270        i = match laidout.shaped_text.nearest_line(pos.y) {
2271            Some(l) => CaretIndex {
2272                line: l.index(),
2273                index: match l.nearest_seg(pos.x) {
2274                    Some(s) => s.nearest_char_index(pos.x, resolved.segmented_text.text()),
2275                    None => l.text_range().end,
2276                },
2277            },
2278            None => CaretIndex::ZERO,
2279        };
2280        i.index = resolved.segmented_text.snap_grapheme_boundary(i.index);
2281
2282        drop(resolved);
2283        caret = TEXT.resolve_caret();
2284
2285        caret.set_index(i);
2286    }
2287
2288    if caret.index.is_none() {
2289        caret.set_index(CaretIndex::ZERO);
2290        caret.clear_selection();
2291    }
2292}
2293
2294fn rich_selection_index_nearest_to(window_point: DipPoint, move_selection_index: bool) -> TextSelectOp {
2295    TextSelectOp::new_rich(
2296        move |ctx| {
2297            if move_selection_index {
2298                return (ctx.caret.index.unwrap_or_else(|| WIDGET.id()), ());
2299            }
2300
2301            if let Some(root) = ctx.root_info()
2302                && let Some(nearest_leaf) = root.rich_text_nearest_leaf(window_point.to_px(root.tree().scale_factor()))
2303            {
2304                return (nearest_leaf.id(), ());
2305            }
2306            (WIDGET.id(), ())
2307        },
2308        move |()| {
2309            if !move_selection_index {
2310                local_select_index_nearest_to(window_point, false);
2311            }
2312            (TEXT.resolved().caret.index.unwrap_or(CaretIndex::ZERO), ())
2313        },
2314        move |ctx, ()| {
2315            if !move_selection_index {
2316                return Some((ctx.caret.selection_index.unwrap_or_else(|| WIDGET.id()), ()));
2317            }
2318
2319            if let Some(root) = ctx.root_info()
2320                && let Some(nearest_leaf) = root.rich_text_nearest_leaf(window_point.to_px(root.tree().scale_factor()))
2321            {
2322                return Some((nearest_leaf.id(), ()));
2323            }
2324            Some((WIDGET.id(), ()))
2325        },
2326        move |()| {
2327            if move_selection_index {
2328                local_select_index_nearest_to(window_point, true);
2329            }
2330            Some(TEXT.resolved().caret.selection_index.unwrap_or(CaretIndex::ZERO))
2331        },
2332    )
2333}
2334fn local_select_index_nearest_to(window_point: DipPoint, move_selection_index: bool) {
2335    let mut caret = TEXT.resolve_caret();
2336
2337    if caret.index.is_none() {
2338        caret.index = Some(CaretIndex::ZERO);
2339    }
2340    if caret.selection_index.is_none() {
2341        caret.selection_index = Some(caret.index.unwrap());
2342    }
2343
2344    caret.used_retained_x = false;
2345    caret.index_version += 1;
2346
2347    let laidout = TEXT.laidout();
2348    if let Some(pos) = laidout
2349        .render_info
2350        .transform
2351        .inverse()
2352        .and_then(|t| t.project_point(window_point.to_px(laidout.render_info.scale_factor)))
2353    {
2354        drop(caret);
2355        let resolved = TEXT.resolved();
2356
2357        let mut i = match laidout.shaped_text.nearest_line(pos.y) {
2358            Some(l) => CaretIndex {
2359                line: l.index(),
2360                index: match l.nearest_seg(pos.x) {
2361                    Some(s) => s.nearest_char_index(pos.x, resolved.segmented_text.text()),
2362                    None => l.text_range().end,
2363                },
2364            },
2365            None => CaretIndex::ZERO,
2366        };
2367        i.index = resolved.segmented_text.snap_grapheme_boundary(i.index);
2368
2369        drop(resolved);
2370        caret = TEXT.resolve_caret();
2371
2372        if move_selection_index {
2373            caret.selection_index = Some(i);
2374        } else {
2375            caret.index = Some(i);
2376        }
2377    }
2378}
2379
2380fn rich_nearest_line_to(replace_selection: bool, window_point: DipPoint) -> TextSelectOp {
2381    TextSelectOp::new_rich(
2382        move |ctx| {
2383            if let Some(root) = ctx.root_info() {
2384                let window_point = window_point.to_px(root.tree().scale_factor());
2385                if let Some(nearest_leaf) = root.rich_text_nearest_leaf(window_point) {
2386                    let mut nearest = usize::MAX;
2387                    let mut nearest_dist = DistanceKey::NONE_MAX;
2388                    let mut rows_len = 0;
2389                    nearest_leaf.bounds_info().visit_inner_rects::<()>(|r, i, l| {
2390                        rows_len = l;
2391                        let dist = DistanceKey::from_rect_to_point(r, window_point);
2392                        if dist < nearest_dist {
2393                            nearest_dist = dist;
2394                            nearest = i;
2395                        }
2396                        ops::ControlFlow::Continue(())
2397                    });
2398
2399                    // returns the rich line end
2400                    if nearest < rows_len.saturating_sub(1) {
2401                        // rich line ends in the leaf widget
2402                        return (nearest_leaf.id(), Some(nearest));
2403                    } else {
2404                        // rich line starts in the leaf widget
2405                        let mut end = nearest_leaf.clone();
2406                        for next in nearest_leaf.rich_text_next() {
2407                            let line_info = next.rich_text_line_info();
2408                            if line_info.starts_new_line && !line_info.is_wrap_start {
2409                                return (
2410                                    end.id(),
2411                                    Some(end.bounds_info().inline().map(|i| i.rows.len().saturating_sub(1)).unwrap_or(0)),
2412                                );
2413                            }
2414                            end = next;
2415                            if line_info.ends_in_new_line {
2416                                break;
2417                            }
2418                        }
2419                        return (end.id(), Some(0));
2420                    }
2421                }
2422            }
2423            (WIDGET.id(), None)
2424        },
2425        move |rich_request| {
2426            if let Some(line_i) = rich_request {
2427                let local_ctx = TEXT.laidout();
2428                if let Some(line) = local_ctx.shaped_text.line(line_i) {
2429                    return (
2430                        CaretIndex {
2431                            index: line.actual_text_caret_range().end,
2432                            line: line_i,
2433                        },
2434                        line.actual_line_start().index() == 0,
2435                    );
2436                }
2437            }
2438            local_select_line_word_nearest_to(replace_selection, true, window_point);
2439            (TEXT.resolved().caret.index.unwrap(), false)
2440        },
2441        move |ctx, rich_select_line_start| {
2442            if rich_select_line_start {
2443                let id = WIDGET.id();
2444                if let Some(line_end) = ctx.leaf_info(id) {
2445                    let mut line_start = line_end;
2446                    let mut first = true;
2447                    for prev in line_start.rich_text_self_and_prev() {
2448                        let line_info = prev.rich_text_line_info();
2449                        line_start = prev;
2450                        if (line_info.starts_new_line && !line_info.is_wrap_start) || (line_info.ends_in_new_line && !first) {
2451                            break;
2452                        }
2453                        first = false;
2454                    }
2455                    if !replace_selection
2456                        && let Some(sel) = ctx.caret_selection_index_info()
2457                        && let Some(std::cmp::Ordering::Less) = sel.cmp_sibling_in(&line_start, &sel.root())
2458                    {
2459                        // rich line start already inside selection
2460                        return Some((sel.id(), false));
2461                    }
2462                    return Some((line_start.id(), line_start.id() != id));
2463                }
2464            }
2465            if replace_selection {
2466                return Some((WIDGET.id(), false));
2467            }
2468            Some((ctx.caret.selection_index.unwrap_or_else(|| WIDGET.id()), false))
2469        },
2470        move |start_of_last_line| {
2471            if replace_selection {
2472                let local_ctx = TEXT.laidout();
2473                let mut line_i = local_ctx.shaped_text.lines_len().saturating_sub(1);
2474                if !start_of_last_line && let Some(i) = TEXT.resolved().caret.index {
2475                    line_i = i.line;
2476                }
2477                if let Some(last_line) = local_ctx.shaped_text.line(line_i) {
2478                    return Some(CaretIndex {
2479                        index: last_line.actual_text_caret_range().start,
2480                        line: line_i,
2481                    });
2482                }
2483                None
2484            } else {
2485                let local_ctx = TEXT.resolved();
2486                Some(
2487                    local_ctx
2488                        .caret
2489                        .selection_index
2490                        .or(local_ctx.caret.index)
2491                        .unwrap_or(CaretIndex::ZERO),
2492                )
2493            }
2494        },
2495    )
2496}
2497fn local_select_line_word_nearest_to(replace_selection: bool, select_word: bool, window_point: DipPoint) {
2498    let mut caret = TEXT.resolve_caret();
2499
2500    //if there was at least one laidout
2501    let laidout = TEXT.laidout();
2502    if let Some(pos) = laidout
2503        .render_info
2504        .transform
2505        .inverse()
2506        .and_then(|t| t.project_point(window_point.to_px(laidout.render_info.scale_factor)))
2507    {
2508        //if has rendered
2509        if let Some(l) = laidout.shaped_text.nearest_line(pos.y) {
2510            let range = if select_word {
2511                let max_char = l.actual_text_caret_range().end;
2512                let mut r = l.nearest_seg(pos.x).map(|seg| seg.text_range()).unwrap_or_else(|| l.text_range());
2513                // don't select line-break at end of line
2514                r.start = r.start.min(max_char);
2515                r.end = r.end.min(max_char);
2516                r
2517            } else {
2518                l.actual_text_caret_range()
2519            };
2520
2521            let merge_with_selection = if replace_selection {
2522                None
2523            } else {
2524                caret.initial_selection.clone().map(|(s, _)| s).or_else(|| caret.selection_range())
2525            };
2526            if let Some(mut s) = merge_with_selection {
2527                let caret_at_start = range.start < s.start.index;
2528                s.start.index = s.start.index.min(range.start);
2529                s.end.index = s.end.index.max(range.end);
2530
2531                if caret_at_start {
2532                    caret.selection_index = Some(s.end);
2533                    caret.set_index(s.start);
2534                } else {
2535                    caret.selection_index = Some(s.start);
2536                    caret.set_index(s.end);
2537                }
2538            } else {
2539                let start = CaretIndex {
2540                    line: l.index(),
2541                    index: range.start,
2542                };
2543                let end = CaretIndex {
2544                    line: l.index(),
2545                    index: range.end,
2546                };
2547                caret.selection_index = Some(start);
2548                caret.set_index(end);
2549
2550                caret.initial_selection = Some((start..end, select_word));
2551            }
2552
2553            return;
2554        };
2555    }
2556
2557    if caret.index.is_none() {
2558        caret.set_index(CaretIndex::ZERO);
2559        caret.clear_selection();
2560    }
2561}