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,
7    render::FontSynthesis,
8    update::UpdateOp,
9    widget::{
10        WIDGET,
11        info::WIDGET_TREE_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::Update { .. } => {
117                if FONT_CHANGED_EVENT.has_update(true) | FONT_FAMILY_VAR.is_new()
118                    || FONT_STYLE_VAR.is_new()
119                    || FONT_WEIGHT_VAR.is_new()
120                    || FONT_STRETCH_VAR.is_new()
121                {
122                    state = State::Reload;
123                } else if let State::Loading { response, .. } = &state {
124                    if let Some(f) = response.rsp() {
125                        tracing::trace!("text {:?} fonts finished loading", WIDGET.id());
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                    tracing::trace!("text {:?} begin load font list", WIDGET.id());
153                    FONTS.list(
154                        family,
155                        FONT_STYLE_VAR.get(),
156                        FONT_WEIGHT_VAR.get(),
157                        FONT_STRETCH_VAR.get(),
158                        lang.best(),
159                    )
160                })
161            });
162
163            if let Some(f) = font_list.rsp() {
164                tracing::trace!("font list already loaded");
165                let mut txt = TEXT.resolve();
166                txt.synthesis = FONT_SYNTHESIS_VAR.get() & f.best().synthesis_for(FONT_STYLE_VAR.get(), FONT_WEIGHT_VAR.get());
167                txt.faces = f;
168                state = State::Loaded;
169
170                WIDGET.layout();
171            } else {
172                tracing::trace!("font list loading");
173                state = State::Loading {
174                    _update_handle: font_list.subscribe(UpdateOp::Update, WIDGET.id()),
175                    response: font_list,
176                    _window_load_handle: WINDOW.loading_handle(1.secs(), "font-list"),
177                };
178            }
179        }
180    })
181}
182fn resolve_text_access(child: impl IntoUiNode) -> UiNode {
183    match_node(child, |child, op| match op {
184        UiNodeOp::Init => {
185            WIDGET
186                .sub_var_info(&TEXT.resolved().txt)
187                .sub_var_info(&TEXT_EDITABLE_VAR)
188                .sub_var_info(&TEXT_SELECTABLE_VAR)
189                .sub_var_info(&OBSCURE_TXT_VAR);
190        }
191        UiNodeOp::Info { info } => {
192            let editable = TEXT_EDITABLE_VAR.get();
193            if editable || TEXT_SELECTABLE_VAR.get() {
194                FocusInfoBuilder::new(info).focusable_passive(true);
195            }
196
197            child.info(info);
198
199            if !editable
200                && !OBSCURE_TXT_VAR.get()
201                && let Some(mut a) = info.access()
202            {
203                a.set_label(TEXT.resolved().segmented_text.text().clone());
204            }
205        }
206        _ => {}
207    })
208}
209fn resolve_text_segments(child: impl IntoUiNode) -> UiNode {
210    match_node(child, |_, op| {
211        let mut segment = false;
212        match op {
213            UiNodeOp::Init => {
214                WIDGET
215                    .sub_var(&TEXT.resolved().txt)
216                    .sub_var(&TEXT_TRANSFORM_VAR)
217                    .sub_var(&WHITE_SPACE_VAR)
218                    .sub_var(&DIRECTION_VAR)
219                    .sub_var(&TEXT_EDITABLE_VAR);
220
221                segment = true;
222            }
223            UiNodeOp::Update { .. } => {
224                segment = TEXT.resolved().txt.is_new()
225                    || TEXT_TRANSFORM_VAR.is_new()
226                    || WHITE_SPACE_VAR.is_new()
227                    || DIRECTION_VAR.is_new()
228                    || TEXT_EDITABLE_VAR.is_new();
229            }
230            _ => {}
231        }
232        if segment {
233            let mut ctx = TEXT.resolve();
234
235            let mut txt = ctx.txt.get();
236
237            if !TEXT_EDITABLE_VAR.get() {
238                TEXT_TRANSFORM_VAR.with(|t| {
239                    if let Cow::Owned(t) = t.transform(&txt) {
240                        txt = t;
241                    }
242                });
243                WHITE_SPACE_VAR.with(|t| {
244                    if let Cow::Owned(t) = t.transform(&txt) {
245                        txt = t;
246                    }
247                });
248            }
249
250            let direction = DIRECTION_VAR.get();
251            if ctx.segmented_text.text() != &txt || ctx.segmented_text.base_direction() != direction {
252                ctx.segmented_text = SegmentedText::new(txt, direction);
253
254                ctx.pending_layout = PendingLayout::RESHAPE;
255                WIDGET.layout();
256            }
257        }
258    })
259}
260fn resolve_text_edit(child: impl IntoUiNode) -> UiNode {
261    // Use `ResolveTextEdit::get` to access.
262    let mut edit = None::<Box<ResolveTextEdit>>;
263
264    match_node(child, move |child, op| {
265        let mut enable = false;
266        match op {
267            UiNodeOp::Init => {
268                WIDGET
269                    .sub_var(&TEXT_EDITABLE_VAR)
270                    .sub_var(&TEXT_SELECTABLE_VAR)
271                    .sub_var(&MAX_CHARS_COUNT_VAR);
272                enable = TEXT_EDITABLE_VAR.get() || TEXT_SELECTABLE_VAR.get();
273            }
274            UiNodeOp::Deinit => {
275                edit = None;
276            }
277            UiNodeOp::Update { updates } => {
278                if TEXT_EDITABLE_VAR.is_new() || TEXT_SELECTABLE_VAR.is_new() {
279                    enable = TEXT_EDITABLE_VAR.get() || TEXT_SELECTABLE_VAR.get();
280                    if !enable && edit.is_some() {
281                        edit = None;
282                        TEXT.resolve().caret.opacity = var(0.fct()).read_only();
283                    }
284                }
285
286                if let Some(edit) = &mut edit {
287                    child.update(updates);
288
289                    resolve_text_edit_update(edit);
290
291                    if TEXT_EDITABLE_VAR.get() && TEXT.resolved().txt.capabilities().can_modify() {
292                        resolve_text_edit_events(edit);
293                    }
294                    if TEXT_EDITABLE_VAR.get() || TEXT_SELECTABLE_VAR.get() {
295                        resolve_text_edit_or_select_events(edit);
296                    }
297
298                    let enable = !OBSCURE_TXT_VAR.get() && TEXT.resolved().caret.selection_range().is_some();
299                    edit.cut.enabled().set(enable);
300                    edit.copy.enabled().set(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.focus_changed = FOCUS_CHANGED_EVENT.subscribe(UpdateOp::Update, id);
313                edit.key_input = KEY_INPUT_EVENT.subscribe(UpdateOp::Update, id);
314                edit.access_text = ACCESS_TEXT_EVENT.subscribe(UpdateOp::Update, id);
315                edit.ime = IME_EVENT.subscribe(UpdateOp::Update, id);
316
317                let win_id = WINDOW.id();
318                let i = WIDGET_TREE_CHANGED_EVENT.var_map(
319                    move |args| {
320                        if args.tree.window_id() == win_id
321                            && let Some(wgt) = args.tree.get(id)
322                        {
323                            Some(wgt.interactivity())
324                        } else {
325                            None
326                        }
327                    },
328                    Interactivity::empty,
329                );
330                i.subscribe(UpdateOp::Update, id).perm();
331                edit.interactivity = Some(i);
332
333                edit.paste = PASTE_CMD.scoped(id).subscribe(true);
334                edit.edit = EDIT_CMD.scoped(id).subscribe(true);
335
336                edit.max_count = MAX_CHARS_COUNT_VAR.subscribe(UpdateOp::Update, id);
337
338                let mut ctx = TEXT.resolve();
339
340                enforce_max_count(&ctx.txt);
341
342                if FOCUS.is_focused(WIDGET.id()).get() {
343                    ctx.caret.opacity = KEYBOARD.caret_animation();
344                    edit.caret_animation = ctx.caret.opacity.subscribe(UpdateOp::Update, WIDGET.id());
345                }
346            }
347
348            if TEXT_SELECTABLE_VAR.get() {
349                let id = WIDGET.id();
350
351                edit.access_selection = ACCESS_SELECTION_EVENT.subscribe(UpdateOp::Update, id);
352
353                let enabled = !OBSCURE_TXT_VAR.get() && TEXT.resolved().caret.selection_range().is_some();
354                edit.copy = COPY_CMD.scoped(id).subscribe(enabled);
355                if editable {
356                    edit.cut = CUT_CMD.scoped(id).subscribe(enabled);
357                } else {
358                    // used in `render_selection`
359                    edit.focus_changed = FOCUS_CHANGED_EVENT.subscribe(UpdateOp::Update, id);
360
361                    edit.key_input = KEY_INPUT_EVENT.subscribe(UpdateOp::Update, id);
362                }
363            }
364        }
365    })
366}
367/// Data allocated only when `editable`.
368#[derive(Default)]
369struct ResolveTextEdit {
370    focus_changed: VarHandle,
371    key_input: VarHandle,
372    access_text: VarHandle,
373    access_selection: VarHandle,
374    ime: VarHandle,
375
376    interactivity: Option<Var<Interactivity>>,
377    caret_animation: VarHandle,
378
379    max_count: VarHandle,
380    cut: CommandHandle,
381    copy: CommandHandle,
382    paste: CommandHandle,
383    edit: CommandHandle,
384}
385impl ResolveTextEdit {
386    fn get(edit_data: &mut Option<Box<Self>>) -> &mut Self {
387        &mut *edit_data.get_or_insert_with(Default::default)
388    }
389}
390fn enforce_max_count(text: &Var<Txt>) {
391    let max_count = MAX_CHARS_COUNT_VAR.get();
392    if max_count > 0 {
393        let count = text.with(|t| t.chars().count());
394        if count > max_count {
395            tracing::debug!("txt var set to text longer than can be typed");
396            text.modify(move |t| {
397                if let Some((i, _)) = t.as_str().char_indices().nth(max_count) {
398                    t.to_mut().truncate(i);
399                }
400            });
401        }
402    }
403}
404fn resolve_text_edit_events(edit: &mut ResolveTextEdit) {
405    if let Some(i) = edit.interactivity.as_ref().unwrap().get_new()
406        && i.is_disabled()
407    {
408        edit.caret_animation = VarHandle::dummy();
409        TEXT.resolve().caret.opacity = var(0.fct()).read_only();
410    }
411
412    if TEXT.resolved().pending_edit {
413        return;
414    }
415    let widget = WIDGET.info();
416    if !widget.interactivity().is_enabled() {
417        return;
418    }
419
420    let prev_caret = {
421        let r = TEXT.resolved();
422        (r.caret.index, r.caret.index_version, r.caret.selection_index)
423    };
424
425    KEY_INPUT_EVENT.each_update(false, |args| {
426        let mut ctx = TEXT.resolve();
427        if args.state == KeyState::Pressed && args.target.widget_id() == widget.id() {
428            match &args.key {
429                Key::Backspace => {
430                    let caret = &mut ctx.caret;
431                    if caret.selection_index.is_some() || caret.index.unwrap_or(CaretIndex::ZERO).index > 0 {
432                        if args.modifiers.is_only_ctrl() {
433                            args.propagation.stop();
434                            ctx.selection_by = SelectionBy::Keyboard;
435                            drop(ctx);
436                            TextEditOp::backspace_word().call_edit_op();
437                        } else if args.modifiers.is_empty() {
438                            args.propagation.stop();
439                            ctx.selection_by = SelectionBy::Keyboard;
440                            drop(ctx);
441                            TextEditOp::backspace().call_edit_op();
442                        }
443                    }
444                }
445                Key::Delete => {
446                    let caret = &mut ctx.caret;
447                    let caret_idx = caret.index.unwrap_or(CaretIndex::ZERO);
448                    if caret.selection_index.is_some() || caret_idx.index < ctx.segmented_text.text().len() {
449                        if args.modifiers.is_only_ctrl() {
450                            args.propagation.stop();
451                            ctx.selection_by = SelectionBy::Keyboard;
452                            drop(ctx);
453                            TextEditOp::delete_word().call_edit_op();
454                        } else if args.modifiers.is_empty() {
455                            args.propagation.stop();
456                            ctx.selection_by = SelectionBy::Keyboard;
457                            drop(ctx);
458                            TextEditOp::delete().call_edit_op();
459                        }
460                    }
461                }
462                _ => {
463                    let insert = args.insert_str();
464                    if !insert.is_empty() {
465                        let skip = (args.is_tab() && !ACCEPTS_TAB_VAR.get()) || (args.is_line_break() && !ACCEPTS_ENTER_VAR.get());
466                        if !skip {
467                            args.propagation.stop();
468                            ctx.selection_by = SelectionBy::Keyboard;
469                            drop(ctx);
470                            TextEditOp::insert(Txt::from_str(insert)).call_edit_op();
471                        }
472                    }
473                }
474            }
475        }
476    });
477    FOCUS_CHANGED_EVENT.each_update(true, |args| {
478        let mut ctx = TEXT.resolve();
479        let caret = &mut ctx.caret;
480        let caret_index = &mut caret.index;
481
482        if args.is_focused(widget.id()) {
483            if caret_index.is_none() {
484                *caret_index = Some(CaretIndex::ZERO);
485            } else {
486                // restore animation when the caret_index did not change
487                caret.opacity = KEYBOARD.caret_animation();
488                edit.caret_animation = caret.opacity.subscribe(UpdateOp::RenderUpdate, widget.id());
489            }
490        } else {
491            edit.caret_animation = VarHandle::dummy();
492            caret.opacity = var(0.fct()).read_only();
493        }
494
495        let auto_select = AUTO_SELECTION_VAR.get();
496        if auto_select != AutoSelection::DISABLED && caret.selection_index.is_some() && TEXT_SELECTABLE_VAR.get() {
497            if auto_select.contains(AutoSelection::CLEAR_ON_BLUR) {
498                if let Some(rich) = TEXT.try_rich() {
499                    if args.is_focus_leave(rich.root_id) {
500                        // deselect if the ALT return and parent scope return are not inside the rich text context
501
502                        if let Some(rich_root) = rich.root_info() {
503                            let alt_return = FOCUS.alt_return().with(|p| p.as_ref().map(|p| p.widget_id()));
504                            if alt_return.is_none() || rich_root.descendants().all(|d| d.id() != alt_return.unwrap()) {
505                                // not ALT return
506                                if let Some(info) = WIDGET.info().into_focusable(true, true)
507                                    && let Some(scope) = info.scope()
508                                {
509                                    let parent_return = FOCUS.return_focused(scope.info().id()).with(|p| p.as_ref().map(|p| p.widget_id()));
510                                    if parent_return.is_none() || rich_root.descendants().all(|d| d.id() != alt_return.unwrap()) {
511                                        // not parent scope return
512                                        SELECT_CMD.scoped(widget.id()).notify_param(TextSelectOp::next());
513                                    }
514                                }
515                            }
516                        }
517                    }
518                } else if args.is_blur(widget.id()) {
519                    // deselect if the widget is not the ALT return focus and is not the parent scope return focus.
520
521                    let us = Some(widget.id());
522                    let alt_return = FOCUS.alt_return().with(|p| p.as_ref().map(|p| p.widget_id()));
523                    if alt_return != us {
524                        // not ALT return
525                        if let Some(info) = WIDGET.info().into_focusable(true, true)
526                            && let Some(scope) = info.scope()
527                        {
528                            let parent_return = FOCUS.return_focused(scope.info().id()).with(|p| p.as_ref().map(|p| p.widget_id()));
529                            if parent_return != us {
530                                // not parent scope return
531                                SELECT_CMD.scoped(widget.id()).notify_param(TextSelectOp::next());
532                            }
533                        }
534                    }
535                }
536            }
537
538            if auto_select.contains(AutoSelection::ALL_ON_FOCUS_KEYBOARD) && args.highlight && args.is_focus(widget.id()) {
539                // select all on keyboard caused focus
540                SELECT_ALL_CMD.scoped(widget.id()).notify();
541            }
542
543            // ALL_ON_FOCUS_POINTER handled by `layout_text_edit_events`
544        }
545    });
546
547    CUT_CMD.scoped(widget.id()).each_update(true, false, |args| {
548        let mut ctx = TEXT.resolve();
549        if let Some(range) = ctx.caret.selection_char_range() {
550            args.propagation.stop();
551            ctx.selection_by = SelectionBy::Command;
552            CLIPBOARD.set_text(Txt::from_str(&ctx.segmented_text.text()[range]));
553            drop(ctx);
554            TextEditOp::delete().call_edit_op();
555        }
556    });
557    PASTE_CMD.scoped(widget.id()).each_update(true, false, |args| {
558        if let Some(paste) = CLIPBOARD.text().ok().flatten()
559            && !paste.is_empty()
560        {
561            args.propagation.stop();
562            TEXT.resolve().selection_by = SelectionBy::Command;
563            TextEditOp::insert(paste).call_edit_op();
564        }
565    });
566    EDIT_CMD.scoped(widget.id()).each_update(true, false, |args| {
567        if let Some(op) = args.param::<UndoTextEditOp>() {
568            args.propagation.stop();
569
570            op.call();
571            if !TEXT.resolved().pending_edit {
572                TEXT.resolve().pending_edit = true;
573                WIDGET.update();
574            }
575        } else if let Some(op) = args.param::<TextEditOp>() {
576            args.propagation.stop();
577
578            op.clone().call_edit_op();
579        }
580    });
581    ACCESS_TEXT_EVENT.each_update(false, |args| {
582        if args.target.widget_id() == widget.id() {
583            args.propagation.stop();
584
585            if args.selection_only {
586                TextEditOp::insert(args.txt.clone())
587            } else {
588                let current_len = TEXT.resolved().txt.with(|t| t.len());
589                let new_len = args.txt.len();
590                TextEditOp::replace(0..current_len, args.txt.clone(), new_len..new_len)
591            }
592            .call_edit_op();
593        }
594    });
595    IME_EVENT.each_update(false, |args| {
596        let mut resegment = false;
597
598        if let Some((start, end)) = args.preview_caret {
599            // update preview txt
600
601            let mut ctx = TEXT.resolve();
602            let ctx = &mut *ctx;
603
604            if args.txt.is_empty() {
605                if let Some(preview) = ctx.ime_preview.take() {
606                    resegment = true;
607                    let caret = &mut ctx.caret;
608                    caret.set_index(preview.prev_caret);
609                    caret.selection_index = preview.prev_selection;
610                }
611            } else if let Some(preview) = &mut ctx.ime_preview {
612                resegment = preview.txt != args.txt;
613                if resegment {
614                    preview.txt = args.txt.clone();
615                }
616            } else {
617                resegment = true;
618                let caret = &mut ctx.caret;
619                ctx.ime_preview = Some(ImePreview {
620                    txt: args.txt.clone(),
621                    prev_caret: caret.index.unwrap_or(CaretIndex::ZERO),
622                    prev_selection: caret.selection_index,
623                });
624            }
625
626            // update preview caret/selection indexes.
627            if let Some(preview) = &ctx.ime_preview {
628                let caret = &mut ctx.caret;
629                let ime_start = if let Some(s) = preview.prev_selection {
630                    preview.prev_caret.index.min(s.index)
631                } else {
632                    preview.prev_caret.index
633                };
634                if start != end {
635                    let start = ime_start + start;
636                    let end = ime_start + end;
637                    resegment |= caret.selection_char_range() != Some(start..end);
638                    caret.set_char_selection(start, end);
639                } else {
640                    let start = ime_start + start;
641                    resegment |= caret.selection_index.is_some() || caret.index.map(|c| c.index) != Some(start);
642                    caret.set_char_index(start);
643                    caret.selection_index = None;
644                }
645            }
646        } else {
647            // commit IME insert
648
649            args.propagation.stop();
650            {
651                let mut ctx = TEXT.resolve();
652                if let Some(preview) = ctx.ime_preview.take() {
653                    // restore caret
654                    let caret = &mut ctx.caret;
655                    caret.set_index(preview.prev_caret);
656                    caret.selection_index = preview.prev_selection;
657
658                    if args.txt.is_empty() {
659                        // the actual insert already re-segments, except in this case
660                        // where there is nothing to insert.
661                        resegment = true;
662                    }
663                }
664            }
665
666            if !args.txt.is_empty() {
667                // actual insert
668                let mut ctx = TEXT.resolve();
669                ctx.selection_by = SelectionBy::Keyboard;
670
671                // if the committed text is equal the last preview reshape is skipped
672                // leaving behind the IME underline highlight.
673                ctx.pending_layout |= PendingLayout::UNDERLINE;
674                WIDGET.layout();
675
676                drop(ctx);
677                TextEditOp::insert(args.txt.clone()).call_edit_op();
678            }
679        }
680
681        if resegment {
682            let mut ctx = TEXT.resolve();
683
684            // re-segment text to insert or remove the preview
685            let mut text = ctx.txt.get();
686            if let Some(preview) = &ctx.ime_preview {
687                if let Some(s) = preview.prev_selection {
688                    let range = if preview.prev_caret.index < s.index {
689                        preview.prev_caret.index..s.index
690                    } else {
691                        s.index..preview.prev_caret.index
692                    };
693                    text.to_mut().replace_range(range, preview.txt.as_str());
694                } else {
695                    text.to_mut().insert_str(preview.prev_caret.index, preview.txt.as_str());
696                }
697                text.end_mut();
698            }
699            ctx.segmented_text = SegmentedText::new(text, DIRECTION_VAR.get());
700
701            ctx.pending_layout |= PendingLayout::RESHAPE;
702            WIDGET.layout();
703        }
704    });
705
706    let mut ctx = TEXT.resolve();
707    let caret = &mut ctx.caret;
708
709    if (caret.index, caret.index_version, caret.selection_index) != prev_caret {
710        caret.used_retained_x = false;
711        if caret.index.is_none() || !FOCUS.is_focused(widget.id()).get() {
712            edit.caret_animation = VarHandle::dummy();
713            caret.opacity = var(0.fct()).read_only();
714        } else {
715            caret.opacity = KEYBOARD.caret_animation();
716            edit.caret_animation = caret.opacity.subscribe(UpdateOp::RenderUpdate, widget.id());
717        }
718        ctx.pending_layout |= PendingLayout::CARET;
719        WIDGET.layout(); // update caret_origin
720    }
721}
722fn resolve_text_edit_or_select_events(_: &mut ResolveTextEdit) {
723    let widget_id = WIDGET.id();
724
725    COPY_CMD.scoped(widget_id).each_update(true, false, |args| {
726        let ctx = TEXT.resolved();
727        if let Some(range) = ctx.caret.selection_char_range() {
728            args.propagation.stop();
729            let txt = Txt::from_str(&ctx.segmented_text.text()[range]);
730            if let Some(rt) = args.param::<RichTextCopyParam>() {
731                rt.set_text(txt);
732            } else {
733                let _ = CLIPBOARD.set_text(txt);
734            }
735        }
736    });
737    ACCESS_SELECTION_EVENT.each_update(false, |args| {
738        if args.start.0.widget_id() == widget_id && args.caret.0.widget_id() == widget_id {
739            args.propagation.stop();
740
741            let mut ctx = TEXT.resolve();
742
743            ctx.caret.set_char_selection(args.start.1, args.caret.1);
744
745            ctx.pending_layout |= PendingLayout::CARET;
746            WIDGET.layout();
747        }
748    });
749}
750fn resolve_text_edit_update(_: &mut ResolveTextEdit) {
751    let mut ctx = TEXT.resolve();
752    let ctx = &mut *ctx;
753    if ctx.txt.is_new() {
754        if !ctx.pending_edit && UNDO.scope() == Some(WIDGET.id()) {
755            UNDO.clear();
756        }
757
758        if let Some(p) = ctx.ime_preview.take() {
759            ctx.caret.index = Some(p.prev_caret);
760            ctx.caret.selection_index = p.prev_selection;
761
762            CANCEL_IME_CMD.scoped(WINDOW.id()).notify();
763        }
764
765        enforce_max_count(&ctx.txt);
766
767        // prevent invalid indexes
768        let caret = &mut ctx.caret;
769        if let Some(i) = &mut caret.index {
770            i.index = ctx.segmented_text.snap_grapheme_boundary(i.index);
771        }
772        if let Some(i) = &mut caret.selection_index {
773            i.index = ctx.segmented_text.snap_grapheme_boundary(i.index);
774        }
775        if let Some((cr, _)) = &mut caret.initial_selection {
776            cr.start.index = ctx.segmented_text.snap_grapheme_boundary(cr.start.index);
777            cr.end.index = ctx.segmented_text.snap_grapheme_boundary(cr.end.index);
778        }
779    }
780
781    if TEXT_EDITABLE_VAR.get() && MAX_CHARS_COUNT_VAR.is_new() {
782        enforce_max_count(&TEXT.resolved().txt);
783    }
784
785    // either txt was new or the edit did not change the text.
786    ctx.pending_edit = false;
787}