zng_wgt_text/node/
resolve.rs

1use std::{borrow::Cow, num::Wrapping, sync::Arc};
2
3use parking_lot::RwLock;
4use zng_app::{
5    access::{ACCESS_SELECTION_EVENT, ACCESS_TEXT_EVENT},
6    event::{CommandHandle, EventHandle},
7    render::FontSynthesis,
8    update::{EventUpdate, UpdateOp},
9    widget::{
10        WIDGET,
11        info::INTERACTIVITY_CHANGED_EVENT,
12        node::{UiNode, UiNodeOp, match_node},
13    },
14    window::WINDOW,
15};
16use zng_ext_clipboard::{CLIPBOARD, COPY_CMD, CUT_CMD, PASTE_CMD};
17use zng_ext_font::{CaretIndex, FONT_CHANGED_EVENT, FONTS, FontFaceList, SegmentedText};
18use zng_ext_input::{
19    focus::{FOCUS, FOCUS_CHANGED_EVENT, FocusInfoBuilder, WidgetInfoFocusExt as _},
20    keyboard::{KEY_INPUT_EVENT, KEYBOARD},
21};
22use zng_ext_l10n::LANG_VAR;
23use zng_ext_undo::UNDO;
24use zng_ext_window::{IME_EVENT, WINDOW_Ext as _, WindowLoadingHandle, cmd::CANCEL_IME_CMD};
25use zng_layout::context::{DIRECTION_VAR, LayoutDirection};
26use zng_view_api::keyboard::{Key, KeyState};
27use zng_wgt::prelude::*;
28
29use crate::{
30    ACCEPTS_ENTER_VAR, ACCEPTS_TAB_VAR, AUTO_SELECTION_VAR, AutoSelection, FONT_FAMILY_VAR, FONT_STRETCH_VAR, FONT_STYLE_VAR,
31    FONT_SYNTHESIS_VAR, FONT_WEIGHT_VAR, MAX_CHARS_COUNT_VAR, OBSCURE_TXT_VAR, TEXT_EDITABLE_VAR, TEXT_SELECTABLE_VAR, TEXT_TRANSFORM_VAR,
32    WHITE_SPACE_VAR,
33    cmd::{EDIT_CMD, SELECT_ALL_CMD, SELECT_CMD, TextEditOp, TextSelectOp, UndoTextEditOp},
34};
35
36use super::{CaretInfo, ImePreview, PendingLayout, RESOLVED_TEXT, ResolvedText, RichTextCopyParam, SelectionBy, TEXT};
37
38/// An UI node that resolves the text context vars, applies the text transform and white space correction and segments the `text`.
39///
40/// This node setups the [`ResolvedText`] for all inner nodes, the `Text!` widget includes this node in the [`NestGroup::EVENT`] group,
41/// so all properties except [`NestGroup::CONTEXT`] have access using the [`TEXT::resolved`] method.
42///
43/// This node also sets the accessibility label to the resolved text.
44///
45/// [`NestGroup::EVENT`]: zng_wgt::prelude::NestGroup::EVENT
46/// [`NestGroup::CONTEXT`]: zng_wgt::prelude::NestGroup::CONTEXT
47pub fn resolve_text(child: impl IntoUiNode, text: impl IntoVar<Txt>) -> UiNode {
48    let child = resolve_text_font(child);
49    let child = resolve_text_access(child);
50    let child = resolve_text_edit(child);
51    let child = resolve_text_segments(child);
52    resolve_text_context(child, text.into_var())
53}
54fn resolve_text_context(child: impl IntoUiNode, text: Var<Txt>) -> UiNode {
55    let mut resolved = None;
56    match_node(child, move |child, op| match op {
57        UiNodeOp::Init => {
58            resolved = Some(Arc::new(RwLock::new(ResolvedText {
59                txt: text.clone(),
60                ime_preview: None,
61                synthesis: FontSynthesis::empty(),
62                faces: FontFaceList::empty(),
63                segmented_text: SegmentedText::new(Txt::from_static(""), LayoutDirection::LTR),
64                pending_layout: PendingLayout::empty(),
65                pending_edit: false,
66                caret: CaretInfo {
67                    opacity: var(0.fct()).read_only(),
68                    index: None,
69                    selection_index: None,
70                    initial_selection: None,
71                    index_version: Wrapping(0),
72                    used_retained_x: false,
73                    skip_next_scroll: false,
74                },
75                selection_by: SelectionBy::Command,
76                selection_toolbar_is_open: false,
77            })));
78
79            RESOLVED_TEXT.with_context(&mut resolved, || child.init());
80        }
81        UiNodeOp::Deinit => {
82            RESOLVED_TEXT.with_context(&mut resolved, || child.deinit());
83
84            resolved = None;
85        }
86        UiNodeOp::Layout { wl, final_size } => RESOLVED_TEXT.with_context(&mut resolved, || {
87            *final_size = child.layout(wl);
88            TEXT.resolve().pending_layout = PendingLayout::empty();
89        }),
90        op => RESOLVED_TEXT.with_context(&mut resolved, || child.op(op)),
91    })
92}
93fn resolve_text_font(child: impl IntoUiNode) -> UiNode {
94    enum State {
95        Reload,
96        Loading {
97            response: ResponseVar<FontFaceList>,
98            _update_handle: VarHandle,
99            _window_load_handle: Option<WindowLoadingHandle>,
100        },
101        Loaded,
102    }
103    let mut state = State::Reload;
104
105    match_node(child, move |_, op| {
106        match op {
107            UiNodeOp::Init => {
108                WIDGET
109                    .sub_var(&FONT_FAMILY_VAR)
110                    .sub_var(&FONT_STYLE_VAR)
111                    .sub_var(&FONT_WEIGHT_VAR)
112                    .sub_var(&FONT_STRETCH_VAR)
113                    .sub_event(&FONT_CHANGED_EVENT)
114                    .sub_var(&FONT_SYNTHESIS_VAR);
115            }
116            UiNodeOp::Event { update } => {
117                if FONT_CHANGED_EVENT.has(update) {
118                    state = State::Reload;
119                }
120            }
121            UiNodeOp::Update { .. } => {
122                if FONT_FAMILY_VAR.is_new() || FONT_STYLE_VAR.is_new() || FONT_WEIGHT_VAR.is_new() || FONT_STRETCH_VAR.is_new() {
123                    state = State::Reload;
124                } else if let State::Loading { response, .. } = &state {
125                    if let Some(f) = response.rsp() {
126                        let mut txt = TEXT.resolve();
127                        txt.synthesis = FONT_SYNTHESIS_VAR.get() & f.best().synthesis_for(FONT_STYLE_VAR.get(), FONT_WEIGHT_VAR.get());
128                        txt.faces = f;
129                        state = State::Loaded;
130
131                        WIDGET.layout();
132                    }
133                } else if let State::Loaded = &state
134                    && FONT_SYNTHESIS_VAR.is_new()
135                {
136                    let mut txt = TEXT.resolve();
137                    txt.synthesis = FONT_SYNTHESIS_VAR.get() & txt.faces.best().synthesis_for(FONT_STYLE_VAR.get(), FONT_WEIGHT_VAR.get());
138
139                    WIDGET.render();
140                }
141            }
142            UiNodeOp::Deinit => {
143                state = State::Reload;
144                return;
145            }
146            _ => {}
147        }
148
149        if let State::Reload = &state {
150            let font_list = FONT_FAMILY_VAR.with(|family| {
151                LANG_VAR.with(|lang| {
152                    FONTS.list(
153                        family,
154                        FONT_STYLE_VAR.get(),
155                        FONT_WEIGHT_VAR.get(),
156                        FONT_STRETCH_VAR.get(),
157                        lang.best(),
158                    )
159                })
160            });
161
162            if let Some(f) = font_list.rsp() {
163                let mut txt = TEXT.resolve();
164                txt.synthesis = FONT_SYNTHESIS_VAR.get() & f.best().synthesis_for(FONT_STYLE_VAR.get(), FONT_WEIGHT_VAR.get());
165                txt.faces = f;
166                state = State::Loaded;
167
168                WIDGET.layout();
169            } else {
170                state = State::Loading {
171                    _update_handle: font_list.subscribe(UpdateOp::Update, WIDGET.id()),
172                    response: font_list,
173                    _window_load_handle: WINDOW.loading_handle(1.secs()),
174                };
175            }
176        }
177    })
178}
179fn resolve_text_access(child: impl IntoUiNode) -> UiNode {
180    match_node(child, |child, op| match op {
181        UiNodeOp::Init => {
182            WIDGET
183                .sub_var_info(&TEXT.resolved().txt)
184                .sub_var_info(&TEXT_EDITABLE_VAR)
185                .sub_var_info(&TEXT_SELECTABLE_VAR)
186                .sub_var_info(&OBSCURE_TXT_VAR);
187        }
188        UiNodeOp::Info { info } => {
189            let editable = TEXT_EDITABLE_VAR.get();
190            if editable || TEXT_SELECTABLE_VAR.get() {
191                FocusInfoBuilder::new(info).focusable_passive(true);
192            }
193
194            child.info(info);
195
196            if !editable
197                && !OBSCURE_TXT_VAR.get()
198                && let Some(mut a) = info.access()
199            {
200                a.set_label(TEXT.resolved().segmented_text.text().clone());
201            }
202        }
203        _ => {}
204    })
205}
206fn resolve_text_segments(child: impl IntoUiNode) -> UiNode {
207    match_node(child, |_, op| {
208        let mut segment = false;
209        match op {
210            UiNodeOp::Init => {
211                WIDGET
212                    .sub_var(&TEXT.resolved().txt)
213                    .sub_var(&TEXT_TRANSFORM_VAR)
214                    .sub_var(&WHITE_SPACE_VAR)
215                    .sub_var(&DIRECTION_VAR)
216                    .sub_var(&TEXT_EDITABLE_VAR);
217
218                segment = true;
219            }
220            UiNodeOp::Update { .. } => {
221                segment = TEXT.resolved().txt.is_new()
222                    || TEXT_TRANSFORM_VAR.is_new()
223                    || WHITE_SPACE_VAR.is_new()
224                    || DIRECTION_VAR.is_new()
225                    || TEXT_EDITABLE_VAR.is_new();
226            }
227            _ => {}
228        }
229        if segment {
230            let mut ctx = TEXT.resolve();
231
232            let mut txt = ctx.txt.get();
233
234            if !TEXT_EDITABLE_VAR.get() {
235                TEXT_TRANSFORM_VAR.with(|t| {
236                    if let Cow::Owned(t) = t.transform(&txt) {
237                        txt = t;
238                    }
239                });
240                WHITE_SPACE_VAR.with(|t| {
241                    if let Cow::Owned(t) = t.transform(&txt) {
242                        txt = t;
243                    }
244                });
245            }
246
247            let direction = DIRECTION_VAR.get();
248            if ctx.segmented_text.text() != &txt || ctx.segmented_text.base_direction() != direction {
249                ctx.segmented_text = SegmentedText::new(txt, direction);
250
251                ctx.pending_layout = PendingLayout::RESHAPE;
252                WIDGET.layout();
253            }
254        }
255    })
256}
257fn resolve_text_edit(child: impl IntoUiNode) -> UiNode {
258    // Use `ResolveTextEdit::get` to access.
259    let mut edit = None::<Box<ResolveTextEdit>>;
260
261    match_node(child, move |child, op| {
262        let mut enable = false;
263        match op {
264            UiNodeOp::Init => {
265                WIDGET
266                    .sub_var(&TEXT_EDITABLE_VAR)
267                    .sub_var(&TEXT_SELECTABLE_VAR)
268                    .sub_var(&MAX_CHARS_COUNT_VAR);
269                enable = TEXT_EDITABLE_VAR.get() || TEXT_SELECTABLE_VAR.get();
270            }
271            UiNodeOp::Deinit => {
272                edit = None;
273            }
274            UiNodeOp::Update { .. } => {
275                if TEXT_EDITABLE_VAR.is_new() || TEXT_SELECTABLE_VAR.is_new() {
276                    enable = TEXT_EDITABLE_VAR.get() || TEXT_SELECTABLE_VAR.get();
277                    if !enable && edit.is_some() {
278                        edit = None;
279                        TEXT.resolve().caret.opacity = var(0.fct()).read_only();
280                    }
281                }
282
283                if let Some(edit) = &mut edit {
284                    resolve_text_edit_update(edit);
285                }
286            }
287            UiNodeOp::Event { update } => {
288                child.event(update);
289
290                if let Some(edit) = &mut edit {
291                    if TEXT_EDITABLE_VAR.get() && TEXT.resolved().txt.capabilities().can_modify() {
292                        resolve_text_edit_events(update, edit);
293                    }
294                    if TEXT_EDITABLE_VAR.get() || TEXT_SELECTABLE_VAR.get() {
295                        resolve_text_edit_or_select_events(update, edit);
296                    }
297
298                    let enable = !OBSCURE_TXT_VAR.get() && TEXT.resolved().caret.selection_range().is_some();
299                    edit.cut.set_enabled(enable);
300                    edit.copy.set_enabled(enable);
301                }
302            }
303            _ => {}
304        }
305        if enable {
306            let edit = ResolveTextEdit::get(&mut edit);
307
308            let editable = TEXT_EDITABLE_VAR.get();
309            if editable {
310                let id = WIDGET.id();
311
312                edit.events[0] = FOCUS_CHANGED_EVENT.subscribe(id);
313                edit.events[1] = INTERACTIVITY_CHANGED_EVENT.subscribe(id);
314                edit.events[2] = KEY_INPUT_EVENT.subscribe(id);
315                edit.events[3] = ACCESS_TEXT_EVENT.subscribe(id);
316                edit.events[5] = IME_EVENT.subscribe(id);
317
318                edit.paste = PASTE_CMD.scoped(id).subscribe(true);
319                edit.edit = EDIT_CMD.scoped(id).subscribe(true);
320
321                edit.max_count = MAX_CHARS_COUNT_VAR.subscribe(UpdateOp::Update, id);
322
323                let mut ctx = TEXT.resolve();
324
325                enforce_max_count(&ctx.txt);
326
327                if FOCUS.is_focused(WIDGET.id()).get() {
328                    ctx.caret.opacity = KEYBOARD.caret_animation();
329                    edit.caret_animation = ctx.caret.opacity.subscribe(UpdateOp::Update, WIDGET.id());
330                }
331            }
332
333            if TEXT_SELECTABLE_VAR.get() {
334                let id = WIDGET.id();
335
336                edit.events[4] = ACCESS_SELECTION_EVENT.subscribe(id);
337
338                let enabled = !OBSCURE_TXT_VAR.get() && TEXT.resolved().caret.selection_range().is_some();
339                edit.copy = COPY_CMD.scoped(id).subscribe(enabled);
340                if editable {
341                    edit.cut = CUT_CMD.scoped(id).subscribe(enabled);
342                } else {
343                    // used in `render_selection`
344                    edit.events[0] = FOCUS_CHANGED_EVENT.subscribe(id);
345
346                    edit.events[2] = KEY_INPUT_EVENT.subscribe(id);
347                }
348            }
349        }
350    })
351}
352/// Data allocated only when `editable`.
353#[derive(Default)]
354struct ResolveTextEdit {
355    events: [EventHandle; 6],
356    caret_animation: VarHandle,
357    max_count: VarHandle,
358    cut: CommandHandle,
359    copy: CommandHandle,
360    paste: CommandHandle,
361    edit: CommandHandle,
362}
363impl ResolveTextEdit {
364    fn get(edit_data: &mut Option<Box<Self>>) -> &mut Self {
365        &mut *edit_data.get_or_insert_with(Default::default)
366    }
367}
368fn enforce_max_count(text: &Var<Txt>) {
369    let max_count = MAX_CHARS_COUNT_VAR.get();
370    if max_count > 0 {
371        let count = text.with(|t| t.chars().count());
372        if count > max_count {
373            tracing::debug!("txt var set to text longer than can be typed");
374            text.modify(move |t| {
375                if let Some((i, _)) = t.as_str().char_indices().nth(max_count) {
376                    t.to_mut().truncate(i);
377                }
378            });
379        }
380    }
381}
382fn resolve_text_edit_events(update: &EventUpdate, edit: &mut ResolveTextEdit) {
383    if let Some(args) = INTERACTIVITY_CHANGED_EVENT.on(update)
384        && args.is_disable(WIDGET.id())
385    {
386        edit.caret_animation = VarHandle::dummy();
387        TEXT.resolve().caret.opacity = var(0.fct()).read_only();
388    }
389
390    if TEXT.resolved().pending_edit {
391        return;
392    }
393    let widget = WIDGET.info();
394    if !widget.interactivity().is_enabled() {
395        return;
396    }
397
398    let prev_caret = {
399        let r = TEXT.resolved();
400        (r.caret.index, r.caret.index_version, r.caret.selection_index)
401    };
402
403    if let Some(args) = KEY_INPUT_EVENT.on_unhandled(update) {
404        let mut ctx = TEXT.resolve();
405        if args.state == KeyState::Pressed && args.target.widget_id() == widget.id() {
406            match &args.key {
407                Key::Backspace => {
408                    let caret = &mut ctx.caret;
409                    if caret.selection_index.is_some() || caret.index.unwrap_or(CaretIndex::ZERO).index > 0 {
410                        if args.modifiers.is_only_ctrl() {
411                            args.propagation().stop();
412                            ctx.selection_by = SelectionBy::Keyboard;
413                            drop(ctx);
414                            TextEditOp::backspace_word().call_edit_op();
415                        } else if args.modifiers.is_empty() {
416                            args.propagation().stop();
417                            ctx.selection_by = SelectionBy::Keyboard;
418                            drop(ctx);
419                            TextEditOp::backspace().call_edit_op();
420                        }
421                    }
422                }
423                Key::Delete => {
424                    let caret = &mut ctx.caret;
425                    let caret_idx = caret.index.unwrap_or(CaretIndex::ZERO);
426                    if caret.selection_index.is_some() || caret_idx.index < ctx.segmented_text.text().len() {
427                        if args.modifiers.is_only_ctrl() {
428                            args.propagation().stop();
429                            ctx.selection_by = SelectionBy::Keyboard;
430                            drop(ctx);
431                            TextEditOp::delete_word().call_edit_op();
432                        } else if args.modifiers.is_empty() {
433                            args.propagation().stop();
434                            ctx.selection_by = SelectionBy::Keyboard;
435                            drop(ctx);
436                            TextEditOp::delete().call_edit_op();
437                        }
438                    }
439                }
440                _ => {
441                    let insert = args.insert_str();
442                    if !insert.is_empty() {
443                        let skip = (args.is_tab() && !ACCEPTS_TAB_VAR.get()) || (args.is_line_break() && !ACCEPTS_ENTER_VAR.get());
444                        if !skip {
445                            args.propagation().stop();
446                            ctx.selection_by = SelectionBy::Keyboard;
447                            drop(ctx);
448                            TextEditOp::insert(Txt::from_str(insert)).call_edit_op();
449                        }
450                    }
451                }
452            }
453        }
454    } else if let Some(args) = FOCUS_CHANGED_EVENT.on(update) {
455        let mut ctx = TEXT.resolve();
456        let caret = &mut ctx.caret;
457        let caret_index = &mut caret.index;
458
459        if args.is_focused(widget.id()) {
460            if caret_index.is_none() {
461                *caret_index = Some(CaretIndex::ZERO);
462            } else {
463                // restore animation when the caret_index did not change
464                caret.opacity = KEYBOARD.caret_animation();
465                edit.caret_animation = caret.opacity.subscribe(UpdateOp::RenderUpdate, widget.id());
466            }
467        } else {
468            edit.caret_animation = VarHandle::dummy();
469            caret.opacity = var(0.fct()).read_only();
470        }
471
472        let auto_select = AUTO_SELECTION_VAR.get();
473        if auto_select != AutoSelection::DISABLED && caret.selection_index.is_some() && TEXT_SELECTABLE_VAR.get() {
474            if auto_select.contains(AutoSelection::CLEAR_ON_BLUR) {
475                if let Some(rich) = TEXT.try_rich() {
476                    if args.is_focus_leave(rich.root_id) {
477                        // deselect if the ALT return and parent scope return are not inside the rich text context
478
479                        if let Some(rich_root) = rich.root_info() {
480                            let alt_return = FOCUS.alt_return().with(|p| p.as_ref().map(|p| p.widget_id()));
481                            if alt_return.is_none() || rich_root.descendants().all(|d| d.id() != alt_return.unwrap()) {
482                                // not ALT return
483                                if let Some(info) = WIDGET.info().into_focusable(true, true)
484                                    && let Some(scope) = info.scope()
485                                {
486                                    let parent_return = FOCUS.return_focused(scope.info().id()).with(|p| p.as_ref().map(|p| p.widget_id()));
487                                    if parent_return.is_none() || rich_root.descendants().all(|d| d.id() != alt_return.unwrap()) {
488                                        // not parent scope return
489                                        SELECT_CMD.scoped(widget.id()).notify_param(TextSelectOp::next());
490                                    }
491                                }
492                            }
493                        }
494                    }
495                } else if args.is_blur(widget.id()) {
496                    // deselect if the widget is not the ALT return focus and is not the parent scope return focus.
497
498                    let us = Some(widget.id());
499                    let alt_return = FOCUS.alt_return().with(|p| p.as_ref().map(|p| p.widget_id()));
500                    if alt_return != us {
501                        // not ALT return
502                        if let Some(info) = WIDGET.info().into_focusable(true, true)
503                            && let Some(scope) = info.scope()
504                        {
505                            let parent_return = FOCUS.return_focused(scope.info().id()).with(|p| p.as_ref().map(|p| p.widget_id()));
506                            if parent_return != us {
507                                // not parent scope return
508                                SELECT_CMD.scoped(widget.id()).notify_param(TextSelectOp::next());
509                            }
510                        }
511                    }
512                }
513            }
514
515            if auto_select.contains(AutoSelection::ALL_ON_FOCUS_KEYBOARD) && args.highlight && args.is_focus(widget.id()) {
516                // select all on keyboard caused focus
517                SELECT_ALL_CMD.scoped(widget.id()).notify();
518            }
519
520            // ALL_ON_FOCUS_POINTER handled by `layout_text_edit_events`
521        }
522    } else if let Some(args) = CUT_CMD.scoped(widget.id()).on_unhandled(update) {
523        let mut ctx = TEXT.resolve();
524        if let Some(range) = ctx.caret.selection_char_range() {
525            args.propagation().stop();
526            ctx.selection_by = SelectionBy::Command;
527            CLIPBOARD.set_text(Txt::from_str(&ctx.segmented_text.text()[range]));
528            drop(ctx);
529            TextEditOp::delete().call_edit_op();
530        }
531    } else if let Some(args) = PASTE_CMD.scoped(widget.id()).on_unhandled(update) {
532        if let Some(paste) = CLIPBOARD.text().ok().flatten()
533            && !paste.is_empty()
534        {
535            args.propagation().stop();
536            TEXT.resolve().selection_by = SelectionBy::Command;
537            TextEditOp::insert(paste).call_edit_op();
538        }
539    } else if let Some(args) = EDIT_CMD.scoped(widget.id()).on_unhandled(update) {
540        if let Some(op) = args.param::<UndoTextEditOp>() {
541            args.propagation().stop();
542
543            op.call();
544            if !TEXT.resolved().pending_edit {
545                TEXT.resolve().pending_edit = true;
546                WIDGET.update();
547            }
548        } else if let Some(op) = args.param::<TextEditOp>() {
549            args.propagation().stop();
550
551            op.clone().call_edit_op();
552        }
553    } else if let Some(args) = ACCESS_TEXT_EVENT.on_unhandled(update) {
554        if args.widget_id == widget.id() {
555            args.propagation().stop();
556
557            if args.selection_only {
558                TextEditOp::insert(args.txt.clone())
559            } else {
560                let current_len = TEXT.resolved().txt.with(|t| t.len());
561                let new_len = args.txt.len();
562                TextEditOp::replace(0..current_len, args.txt.clone(), new_len..new_len)
563            }
564            .call_edit_op();
565        }
566    } else if let Some(args) = IME_EVENT.on_unhandled(update) {
567        let mut resegment = false;
568
569        if let Some((start, end)) = args.preview_caret {
570            // update preview txt
571
572            let mut ctx = TEXT.resolve();
573            let ctx = &mut *ctx;
574
575            if args.txt.is_empty() {
576                if let Some(preview) = ctx.ime_preview.take() {
577                    resegment = true;
578                    let caret = &mut ctx.caret;
579                    caret.set_index(preview.prev_caret);
580                    caret.selection_index = preview.prev_selection;
581                }
582            } else if let Some(preview) = &mut ctx.ime_preview {
583                resegment = preview.txt != args.txt;
584                if resegment {
585                    preview.txt = args.txt.clone();
586                }
587            } else {
588                resegment = true;
589                let caret = &mut ctx.caret;
590                ctx.ime_preview = Some(ImePreview {
591                    txt: args.txt.clone(),
592                    prev_caret: caret.index.unwrap_or(CaretIndex::ZERO),
593                    prev_selection: caret.selection_index,
594                });
595            }
596
597            // update preview caret/selection indexes.
598            if let Some(preview) = &ctx.ime_preview {
599                let caret = &mut ctx.caret;
600                let ime_start = if let Some(s) = preview.prev_selection {
601                    preview.prev_caret.index.min(s.index)
602                } else {
603                    preview.prev_caret.index
604                };
605                if start != end {
606                    let start = ime_start + start;
607                    let end = ime_start + end;
608                    resegment |= caret.selection_char_range() != Some(start..end);
609                    caret.set_char_selection(start, end);
610                } else {
611                    let start = ime_start + start;
612                    resegment |= caret.selection_index.is_some() || caret.index.map(|c| c.index) != Some(start);
613                    caret.set_char_index(start);
614                    caret.selection_index = None;
615                }
616            }
617        } else {
618            // commit IME insert
619
620            args.propagation().stop();
621            {
622                let mut ctx = TEXT.resolve();
623                if let Some(preview) = ctx.ime_preview.take() {
624                    // restore caret
625                    let caret = &mut ctx.caret;
626                    caret.set_index(preview.prev_caret);
627                    caret.selection_index = preview.prev_selection;
628
629                    if args.txt.is_empty() {
630                        // the actual insert already re-segments, except in this case
631                        // where there is nothing to insert.
632                        resegment = true;
633                    }
634                }
635            }
636
637            if !args.txt.is_empty() {
638                // actual insert
639                let mut ctx = TEXT.resolve();
640                ctx.selection_by = SelectionBy::Keyboard;
641
642                // if the committed text is equal the last preview reshape is skipped
643                // leaving behind the IME underline highlight.
644                ctx.pending_layout |= PendingLayout::UNDERLINE;
645                WIDGET.layout();
646
647                drop(ctx);
648                TextEditOp::insert(args.txt.clone()).call_edit_op();
649            }
650        }
651
652        if resegment {
653            let mut ctx = TEXT.resolve();
654
655            // re-segment text to insert or remove the preview
656            let mut text = ctx.txt.get();
657            if let Some(preview) = &ctx.ime_preview {
658                if let Some(s) = preview.prev_selection {
659                    let range = if preview.prev_caret.index < s.index {
660                        preview.prev_caret.index..s.index
661                    } else {
662                        s.index..preview.prev_caret.index
663                    };
664                    text.to_mut().replace_range(range, preview.txt.as_str());
665                } else {
666                    text.to_mut().insert_str(preview.prev_caret.index, preview.txt.as_str());
667                }
668                text.end_mut();
669            }
670            ctx.segmented_text = SegmentedText::new(text, DIRECTION_VAR.get());
671
672            ctx.pending_layout |= PendingLayout::RESHAPE;
673            WIDGET.layout();
674        }
675    }
676
677    let mut ctx = TEXT.resolve();
678    let caret = &mut ctx.caret;
679
680    if (caret.index, caret.index_version, caret.selection_index) != prev_caret {
681        caret.used_retained_x = false;
682        if caret.index.is_none() || !FOCUS.is_focused(widget.id()).get() {
683            edit.caret_animation = VarHandle::dummy();
684            caret.opacity = var(0.fct()).read_only();
685        } else {
686            caret.opacity = KEYBOARD.caret_animation();
687            edit.caret_animation = caret.opacity.subscribe(UpdateOp::RenderUpdate, widget.id());
688        }
689        ctx.pending_layout |= PendingLayout::CARET;
690        WIDGET.layout(); // update caret_origin
691    }
692}
693fn resolve_text_edit_or_select_events(update: &EventUpdate, _: &mut ResolveTextEdit) {
694    let widget_id = WIDGET.id();
695
696    if let Some(args) = COPY_CMD.scoped(widget_id).on_unhandled(update) {
697        let ctx = TEXT.resolved();
698        if let Some(range) = ctx.caret.selection_char_range() {
699            args.propagation().stop();
700            let txt = Txt::from_str(&ctx.segmented_text.text()[range]);
701            if let Some(rt) = args.param::<RichTextCopyParam>() {
702                rt.set_text(txt);
703            } else {
704                let _ = CLIPBOARD.set_text(txt);
705            }
706        }
707    } else if let Some(args) = ACCESS_SELECTION_EVENT.on_unhandled(update)
708        && args.start.0 == widget_id
709        && args.caret.0 == widget_id
710    {
711        args.propagation().stop();
712
713        let mut ctx = TEXT.resolve();
714
715        ctx.caret.set_char_selection(args.start.1, args.caret.1);
716
717        ctx.pending_layout |= PendingLayout::CARET;
718        WIDGET.layout();
719    }
720}
721fn resolve_text_edit_update(_: &mut ResolveTextEdit) {
722    let mut ctx = TEXT.resolve();
723    let ctx = &mut *ctx;
724    if ctx.txt.is_new() {
725        if !ctx.pending_edit && UNDO.scope() == Some(WIDGET.id()) {
726            UNDO.clear();
727        }
728
729        if let Some(p) = ctx.ime_preview.take() {
730            ctx.caret.index = Some(p.prev_caret);
731            ctx.caret.selection_index = p.prev_selection;
732
733            CANCEL_IME_CMD.scoped(WINDOW.id()).notify();
734        }
735
736        enforce_max_count(&ctx.txt);
737
738        // prevent invalid indexes
739        let caret = &mut ctx.caret;
740        if let Some(i) = &mut caret.index {
741            i.index = ctx.segmented_text.snap_grapheme_boundary(i.index);
742        }
743        if let Some(i) = &mut caret.selection_index {
744            i.index = ctx.segmented_text.snap_grapheme_boundary(i.index);
745        }
746        if let Some((cr, _)) = &mut caret.initial_selection {
747            cr.start.index = ctx.segmented_text.snap_grapheme_boundary(cr.start.index);
748            cr.end.index = ctx.segmented_text.snap_grapheme_boundary(cr.end.index);
749        }
750    }
751
752    if TEXT_EDITABLE_VAR.get() && MAX_CHARS_COUNT_VAR.is_new() {
753        enforce_max_count(&TEXT.resolved().txt);
754    }
755
756    // either txt was new or the edit did not change the text.
757    ctx.pending_edit = false;
758}