Skip to main content

zng_wgt_text/
cmd.rs

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