Skip to main content

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