zng_wgt_text/node/
caret.rs

1use std::{fmt, sync::Arc};
2
3use atomic::{Atomic, Ordering};
4use parking_lot::Mutex;
5use zng_app::{
6    property_id,
7    render::FrameValueKey,
8    update::UPDATES,
9    widget::{
10        WIDGET, WidgetId,
11        base::WidgetBase,
12        node::{UiNode, UiNodeOp, match_node, match_node_leaf},
13        property, widget,
14    },
15};
16use zng_app_context::{LocalContext, context_local};
17use zng_color::colors;
18use zng_ext_input::{
19    focus::{FOCUS, FOCUS_CHANGED_EVENT},
20    mouse::{MOUSE_INPUT_EVENT, MOUSE_MOVE_EVENT},
21    pointer_capture::{POINTER_CAPTURE, POINTER_CAPTURE_EVENT},
22    touch::{TOUCH_INPUT_EVENT, TOUCH_MOVE_EVENT},
23};
24use zng_layout::{
25    context::LAYOUT,
26    unit::{Dip, DipToPx as _, DipVector, Px, PxCornerRadius, PxPoint, PxRect, PxSize, PxTransform, PxVector},
27};
28use zng_view_api::{display_list::FrameValue, touch::TouchId};
29use zng_wgt::{WidgetFn, prelude::*};
30use zng_wgt_layer::{AnchorMode, LAYERS, LayerIndex};
31
32use crate::{
33    CARET_COLOR_VAR, CaretShape, INTERACTIVE_CARET_VAR, INTERACTIVE_CARET_VISUAL_VAR, TEXT_EDITABLE_VAR,
34    cmd::{SELECT_CMD, TextSelectOp},
35};
36
37use super::TEXT;
38
39/// An Ui node that renders the edit caret visual.
40///
41/// The caret is rendered after `child`, over it.
42///
43/// The `Text!` widgets introduces this node in `new_child`, around the [`render_text`] node.
44///
45/// [`render_text`]: super::render_text
46pub fn non_interactive_caret(child: impl IntoUiNode) -> UiNode {
47    let color_key = FrameValueKey::new_unique();
48
49    match_node(child, move |child, op| match op {
50        UiNodeOp::Init => {
51            WIDGET
52                .sub_var_render_update(&CARET_COLOR_VAR)
53                .sub_var_render_update(&INTERACTIVE_CARET_VAR);
54        }
55        UiNodeOp::Render { frame } => {
56            child.render(frame);
57
58            if TEXT_EDITABLE_VAR.get() {
59                let t = TEXT.laidout();
60                let resolved = TEXT.resolved();
61
62                if !resolved.selection_by.matches_interactive_mode(INTERACTIVE_CARET_VAR.get())
63                    && let Some(mut origin) = t.caret_origin
64                {
65                    let mut c = CARET_COLOR_VAR.get();
66                    c.alpha = resolved.caret.opacity.get().0;
67
68                    let caret_thickness = Dip::new(1).to_px(frame.scale_factor());
69                    origin.x -= caret_thickness / 2;
70
71                    let clip_rect = PxRect::new(origin, PxSize::new(caret_thickness, t.shaped_text.line_height()));
72                    frame.push_color(clip_rect, color_key.bind(c, true));
73                }
74            }
75        }
76        UiNodeOp::RenderUpdate { update } => {
77            child.render_update(update);
78
79            if TEXT_EDITABLE_VAR.get() {
80                let resolved = TEXT.resolved();
81
82                if !resolved.selection_by.matches_interactive_mode(INTERACTIVE_CARET_VAR.get()) {
83                    let mut c = CARET_COLOR_VAR.get();
84                    c.alpha = TEXT.resolved().caret.opacity.get().0;
85
86                    update.update_color(color_key.update(c, true))
87                }
88            }
89        }
90        _ => {}
91    })
92}
93
94/// An Ui node that implements interaction and renders the interactive carets.
95///
96/// Caret visuals defined by [`INTERACTIVE_CARET_VISUAL_VAR`].
97pub fn interactive_carets(child: impl IntoUiNode) -> UiNode {
98    let mut carets: Vec<Caret> = vec![];
99    let mut is_focused = false;
100
101    struct Caret {
102        id: WidgetId,
103        input: Arc<Mutex<InteractiveCaretInputMut>>,
104    }
105    match_node(child, move |c, op| match op {
106        UiNodeOp::Init => {
107            WIDGET.sub_var(&INTERACTIVE_CARET_VISUAL_VAR).sub_var_layout(&INTERACTIVE_CARET_VAR);
108            is_focused = false;
109        }
110        UiNodeOp::Deinit => {
111            for caret in carets.drain(..) {
112                LAYERS.remove(caret.id);
113            }
114        }
115        UiNodeOp::Update { .. } => {
116            if !carets.is_empty() && INTERACTIVE_CARET_VISUAL_VAR.is_new() {
117                for caret in carets.drain(..) {
118                    LAYERS.remove(caret.id);
119                }
120                WIDGET.layout();
121            }
122
123            FOCUS_CHANGED_EVENT.each_update(true, |args| {
124                let new_is_focused;
125                if let Some(ctx) = TEXT.try_rich() {
126                    new_is_focused = FOCUS.is_focus_within(ctx.root_id).get();
127                } else {
128                    new_is_focused = args.is_focus_within(WIDGET.id());
129                }
130                if is_focused != new_is_focused {
131                    WIDGET.layout();
132                    is_focused = new_is_focused;
133                }
134            });
135        }
136        UiNodeOp::Layout { wl, final_size } => {
137            *final_size = c.layout(wl);
138
139            let mut expected_len = 0;
140
141            let r_txt = TEXT.resolved();
142            let line_height_half = TEXT.laidout().shaped_text.line_height() / Px(2);
143
144            if r_txt.caret.index.is_some()
145                && (is_focused || r_txt.selection_toolbar_is_open)
146                && r_txt.selection_by.matches_interactive_mode(INTERACTIVE_CARET_VAR.get())
147            {
148                if r_txt.caret.selection_index.is_some() {
149                    expected_len = 2;
150                } else if TEXT_EDITABLE_VAR.get() {
151                    expected_len = 1;
152                }
153            }
154
155            if expected_len != carets.len() {
156                let keep_capture = TEXT
157                    .try_rich()
158                    .and_then(|_| POINTER_CAPTURE.current_capture().with(|c| c.as_ref().map(|c| c.target.widget_id())));
159                for caret in carets.drain(..) {
160                    if Some(caret.id) == keep_capture {
161                        // keep dragging caret alive, in rich texts select ops that use window point are automatically passed to the
162                        // nearest sibling leaf, so the caret logic is still sound even tough a different caret will be visible now.
163                        let mut l = caret.input.lock();
164                        l.deinit_on_capture_lost = true;
165                        if !l.rich_text_hidden {
166                            l.rich_text_hidden = true;
167                            UPDATES.render(caret.id);
168                        }
169                    } else {
170                        LAYERS.remove(caret.id);
171                    }
172                }
173                carets.reserve_exact(expected_len);
174
175                // caret shape node, inserted as ADORNER+1, anchored, propagates LocalContext and collects size+caret mid
176                for i in 0..expected_len {
177                    let input = Arc::new(Mutex::new(InteractiveCaretInputMut {
178                        inner_text: PxTransform::identity(),
179                        x: Px::MIN,
180                        y: Px::MIN,
181                        rich_text_hidden: false,
182                        deinit_on_capture_lost: false,
183                        shape: CaretShape::Insert,
184                        width: Px::MIN,
185                        spot: PxPoint::zero(),
186                    }));
187                    let id = WidgetId::new_unique();
188
189                    let caret = InteractiveCaret! {
190                        id;
191                        interactive_caret_input = InteractiveCaretInput {
192                            ctx: LocalContext::capture(),
193                            parent_id: WIDGET.id(),
194                            visual_fn: INTERACTIVE_CARET_VISUAL_VAR.get(),
195                            is_selection_index: i == 1,
196                            m: input.clone(),
197                        };
198                    };
199
200                    LAYERS.insert_anchored(LayerIndex::ADORNER + 1, WIDGET.id(), AnchorMode::foreground(), caret);
201                    carets.push(Caret { id, input })
202                }
203            }
204
205            if carets.is_empty() {
206                // no caret.
207                return;
208            }
209
210            let t = TEXT.laidout();
211            let Some(origin) = t.caret_origin else {
212                tracing::error!("caret instance, but no caret in context");
213                return;
214            };
215
216            if carets.len() == 1 {
217                // no selection, one caret rendered.
218
219                let mut l = carets[0].input.lock();
220                if l.shape != CaretShape::Insert {
221                    l.shape = CaretShape::Insert;
222                    UPDATES.update(carets[0].id);
223                }
224
225                let mut origin = origin;
226                origin.x -= l.spot.x;
227                origin.y += line_height_half - l.spot.y;
228
229                if l.x != origin.x || l.y != origin.y || l.rich_text_hidden {
230                    l.x = origin.x;
231                    l.y = origin.y;
232                    l.rich_text_hidden = false;
233
234                    UPDATES.render(carets[0].id);
235                }
236            } else {
237                // selection, two carets rendered, but if text is bidirectional the two can have the same shape.
238
239                let (Some(index), Some(s_index), Some(s_origin)) =
240                    (r_txt.caret.index, r_txt.caret.selection_index, t.caret_selection_origin)
241                else {
242                    tracing::error!("caret instance, but no caret in context");
243                    return;
244                };
245
246                let mut index_hidden = false;
247                let mut s_index_hidden = false;
248                if let Some(rr_ctx) = TEXT.try_rich() {
249                    let id = WIDGET.id();
250                    index_hidden = rr_ctx.caret.index != Some(id);
251                    s_index_hidden = rr_ctx.caret.selection_index != Some(id);
252                }
253
254                let mut index_is_left = index.index <= s_index.index;
255                let seg_txt = &r_txt.segmented_text;
256                if let Some((_, seg)) = seg_txt.get(seg_txt.seg_from_char(index.index)) {
257                    if seg.direction().is_rtl() {
258                        index_is_left = !index_is_left;
259                    }
260                } else if seg_txt.base_direction().is_rtl() {
261                    index_is_left = !index_is_left;
262                }
263
264                let mut s_index_is_left = s_index.index < index.index;
265                if let Some((_, seg)) = seg_txt.get(seg_txt.seg_from_char(s_index.index)) {
266                    if seg.direction().is_rtl() {
267                        s_index_is_left = !s_index_is_left;
268                    }
269                } else if seg_txt.base_direction().is_rtl() {
270                    s_index_is_left = !s_index_is_left;
271                }
272
273                let mut l = [carets[0].input.lock(), carets[1].input.lock()];
274
275                let mut delay = false;
276
277                let shapes = [
278                    if index_is_left {
279                        CaretShape::SelectionLeft
280                    } else {
281                        CaretShape::SelectionRight
282                    },
283                    if s_index_is_left {
284                        CaretShape::SelectionLeft
285                    } else {
286                        CaretShape::SelectionRight
287                    },
288                ];
289
290                for i in 0..2 {
291                    if l[i].shape != shapes[i] {
292                        l[i].shape = shapes[i];
293                        l[i].width = Px::MIN;
294                        UPDATES.update(carets[i].id);
295                        delay = true;
296                    } else if l[i].width == Px::MIN {
297                        delay = true;
298                    }
299                }
300
301                if delay {
302                    // wait first layout of shape.
303                    return;
304                }
305
306                let mut origins = [origin, s_origin];
307                let hidden = [index_hidden, s_index_hidden];
308                for i in 0..2 {
309                    origins[i].x -= l[i].spot.x;
310                    origins[i].y += line_height_half - l[i].spot.y;
311                    if l[i].x != origins[i].x || l[i].y != origins[i].y || l[i].rich_text_hidden != hidden[i] {
312                        l[i].x = origins[i].x;
313                        l[i].y = origins[i].y;
314                        l[i].rich_text_hidden = hidden[i];
315                        UPDATES.render(carets[i].id);
316                    }
317                }
318            }
319        }
320        UiNodeOp::Render { .. } | UiNodeOp::RenderUpdate { .. } => {
321            if let Some(inner_rev) = WIDGET.info().inner_transform().inverse() {
322                let text = TEXT.laidout().render_info.transform.then(&inner_rev);
323
324                for c in &carets {
325                    let mut l = c.input.lock();
326                    if l.inner_text != text {
327                        l.inner_text = text;
328
329                        if l.x > Px::MIN && l.y > Px::MIN {
330                            UPDATES.render(c.id);
331                        }
332                    }
333                }
334            }
335        }
336        _ => {}
337    })
338}
339
340#[derive(Clone)]
341struct InteractiveCaretInput {
342    visual_fn: WidgetFn<CaretShape>,
343    ctx: LocalContext,
344    parent_id: WidgetId,
345    is_selection_index: bool,
346    m: Arc<Mutex<InteractiveCaretInputMut>>,
347}
348impl fmt::Debug for InteractiveCaretInput {
349    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
350        write!(f, "InteractiveCaretInput")
351    }
352}
353impl PartialEq for InteractiveCaretInput {
354    fn eq(&self, other: &Self) -> bool {
355        self.visual_fn == other.visual_fn && Arc::ptr_eq(&self.m, &other.m)
356    }
357}
358
359struct InteractiveCaretInputMut {
360    // # set by Text:
361    inner_text: PxTransform,
362    // ## request render for Caret after changing:
363    x: Px,
364    y: Px,
365    rich_text_hidden: bool,
366    // ## request update for Caret after changing:
367    shape: CaretShape,
368    // ## no request needed:
369    deinit_on_capture_lost: bool,
370
371    // # set by Caret:
372    // ## request layout for Text after changing:
373    width: Px,
374    spot: PxPoint,
375}
376
377fn interactive_caret_shape_node(input: Arc<Mutex<InteractiveCaretInputMut>>, visual_fn: WidgetFn<CaretShape>) -> UiNode {
378    let mut shape = CaretShape::Insert;
379
380    match_node(UiNode::nil(), move |visual, op| match op {
381        UiNodeOp::Init => {
382            shape = input.lock().shape;
383            *visual.node() = visual_fn(shape);
384            visual.init();
385        }
386        UiNodeOp::Deinit => {
387            visual.deinit();
388            *visual.node() = UiNode::nil();
389        }
390        UiNodeOp::Update { .. } => {
391            let new_shape = input.lock().shape;
392            if new_shape != shape {
393                shape = new_shape;
394                visual.deinit();
395                *visual.node() = visual_fn(shape);
396                visual.init();
397                WIDGET.layout().render();
398            }
399        }
400        _ => {}
401    })
402}
403
404fn interactive_caret_node(
405    child: impl IntoUiNode,
406    parent_id: WidgetId,
407    is_selection_index: bool,
408    input: Arc<Mutex<InteractiveCaretInputMut>>,
409) -> UiNode {
410    let mut caret_spot_buf = Some(Arc::new(Atomic::new(PxPoint::zero())));
411    let mut touch_move = None::<(TouchId, VarHandles)>;
412    let mut mouse_move = VarHandles::dummy();
413    let mut move_start_to_spot = DipVector::zero();
414
415    match_node(child, move |visual, op| match op {
416        UiNodeOp::Init => {
417            WIDGET.sub_event(&TOUCH_INPUT_EVENT).sub_event(&MOUSE_INPUT_EVENT);
418        }
419        UiNodeOp::Deinit => {
420            touch_move = None;
421            mouse_move.clear();
422        }
423        UiNodeOp::Update { updates } => {
424            visual.update(updates);
425
426            TOUCH_INPUT_EVENT.each_update(false, |args| {
427                FOCUS.focus_widget(parent_id, false);
428                if args.is_touch_start() {
429                    let wgt_info = WIDGET.info();
430                    let wgt_id = wgt_info.id();
431                    move_start_to_spot = wgt_info
432                        .inner_transform()
433                        .transform_vector(input.lock().spot.to_vector())
434                        .to_dip(wgt_info.tree().scale_factor())
435                        - args.position.to_vector();
436
437                    let mut handles = VarHandles::dummy();
438                    handles.push(TOUCH_MOVE_EVENT.subscribe(UpdateOp::Update, wgt_id));
439                    handles.push(POINTER_CAPTURE_EVENT.subscribe(UpdateOp::Update, wgt_id));
440                    touch_move = Some((args.touch, handles));
441                    POINTER_CAPTURE.capture_subtree(wgt_id);
442                } else if touch_move.is_some() {
443                    touch_move = None;
444                    POINTER_CAPTURE.release_capture();
445                }
446            });
447            if let Some((id, _)) = &touch_move {
448                TOUCH_MOVE_EVENT.each_update(false, |args| {
449                    for t in &args.touches {
450                        if t.touch == *id {
451                            let spot = t.position() + move_start_to_spot;
452
453                            let op = match input.lock().shape {
454                                CaretShape::Insert => TextSelectOp::nearest_to(spot),
455                                _ => TextSelectOp::select_index_nearest_to(spot, is_selection_index),
456                            };
457                            SELECT_CMD.scoped(parent_id).notify_param(op);
458                            break;
459                        }
460                    }
461                });
462            }
463            MOUSE_INPUT_EVENT.each_update(false, |args| {
464                // keep focus on the text input, a click outside the are can move focus to window
465                FOCUS.focus_widget(parent_id, false);
466                if !args.is_click && args.is_mouse_down() && args.is_primary() {
467                    let wgt_info = WIDGET.info();
468                    let wgt_id = wgt_info.id();
469                    move_start_to_spot = wgt_info
470                        .inner_transform()
471                        .transform_vector(input.lock().spot.to_vector())
472                        .to_dip(wgt_info.tree().scale_factor())
473                        - args.position.to_vector();
474
475                    mouse_move.push(MOUSE_MOVE_EVENT.subscribe(UpdateOp::Update, wgt_id));
476                    mouse_move.push(POINTER_CAPTURE_EVENT.subscribe(UpdateOp::Update, wgt_id));
477                    POINTER_CAPTURE.capture_subtree(wgt_id);
478                } else if !mouse_move.is_dummy() {
479                    POINTER_CAPTURE.release_capture();
480                    mouse_move.clear();
481                }
482            });
483            if !mouse_move.is_dummy() {
484                MOUSE_MOVE_EVENT.each_update(true, |args| {
485                    let spot = args.position + move_start_to_spot;
486
487                    let op = match input.lock().shape {
488                        CaretShape::Insert => TextSelectOp::nearest_to(spot),
489                        _ => TextSelectOp::select_index_nearest_to(spot, is_selection_index),
490                    };
491                    SELECT_CMD.scoped(parent_id).notify_param(op);
492                });
493            }
494            POINTER_CAPTURE_EVENT.each_update(false, |args| {
495                let id = WIDGET.id();
496                if args.is_lost(id) {
497                    touch_move = None;
498                    mouse_move.clear();
499
500                    if input.lock().deinit_on_capture_lost {
501                        LAYERS.remove(id);
502                    }
503                }
504            });
505        }
506        UiNodeOp::Layout { wl, final_size } => {
507            *final_size = TOUCH_CARET_SPOT.with_context(&mut caret_spot_buf, || visual.layout(wl));
508            let spot = caret_spot_buf.as_ref().unwrap().load(Ordering::Relaxed);
509
510            let mut input_m = input.lock();
511
512            if input_m.width != final_size.width || input_m.spot != spot {
513                UPDATES.layout(parent_id);
514                input_m.width = final_size.width;
515                input_m.spot = spot;
516            }
517        }
518        UiNodeOp::Render { frame } => {
519            let input_m = input.lock();
520
521            visual.delegated();
522
523            let mut transform = input_m.inner_text;
524
525            if input_m.x > Px::MIN && input_m.y > Px::MIN {
526                transform = transform.then(&PxTransform::from(PxVector::new(input_m.x, input_m.y)));
527
528                let mut render = |frame: &mut FrameBuilder| {
529                    frame.push_inner_transform(&transform, |frame| {
530                        visual.render(frame);
531                    });
532                };
533
534                if input_m.rich_text_hidden {
535                    frame.hide(render);
536                } else {
537                    render(frame);
538                }
539            }
540        }
541        _ => {}
542    })
543}
544
545#[widget($crate::node::caret::InteractiveCaret)]
546struct InteractiveCaret(WidgetBase);
547impl InteractiveCaret {
548    fn widget_intrinsic(&mut self) {
549        widget_set! {
550            self;
551            zng_wgt::hit_test_mode = zng_wgt::HitTestMode::Detailed;
552        };
553        self.widget_builder().push_build_action(|b| {
554            let input = b
555                .capture_value::<InteractiveCaretInput>(property_id!(interactive_caret_input))
556                .unwrap();
557
558            b.set_child(interactive_caret_shape_node(input.m.clone(), input.visual_fn));
559
560            b.push_intrinsic(NestGroup::SIZE, "interactive_caret", move |child| {
561                let child = interactive_caret_node(child, input.parent_id, input.is_selection_index, input.m);
562                with_context_blend(input.ctx, false, child)
563            });
564        });
565    }
566}
567#[property(CONTEXT, widget_impl(InteractiveCaret))]
568fn interactive_caret_input(wgt: &mut WidgetBuilding, input: impl IntoValue<InteractiveCaretInput>) {
569    let _ = input;
570    wgt.expect_property_capture();
571}
572
573/// Default interactive caret visual.
574///
575/// See [`interactive_caret_visual`] for more details.
576///
577/// [`interactive_caret_visual`]: fn@super::interactive_caret_visual
578pub fn default_interactive_caret_visual(shape: CaretShape) -> UiNode {
579    match_node_leaf(move |op| match op {
580        UiNodeOp::Layout { final_size, .. } => {
581            let factor = LAYOUT.scale_factor();
582            let size = Dip::new(16).to_px(factor);
583            *final_size = PxSize::splat(size);
584            let line_height = TEXT.laidout().shaped_text.line_height();
585            final_size.height += line_height;
586
587            let caret_thickness = Dip::new(1).to_px(factor);
588
589            let caret_offset = match shape {
590                CaretShape::SelectionLeft => {
591                    final_size.width *= 0.8;
592                    final_size.width - caret_thickness / 2.0 // rounds .5 to 1, to match `render_caret`
593                }
594                CaretShape::SelectionRight => {
595                    final_size.width *= 0.8;
596                    caret_thickness / 2 // rounds .5 to 0
597                }
598                CaretShape::Insert => final_size.width / 2 - caret_thickness / 2,
599            };
600            set_interactive_caret_spot(PxPoint::new(caret_offset, line_height / Px(2)));
601        }
602        UiNodeOp::Render { frame } => {
603            let size = Dip::new(16).to_px(frame.scale_factor());
604            let mut size = PxSize::splat(size);
605
606            let corners = match shape {
607                CaretShape::SelectionLeft => PxCornerRadius::new(size, PxSize::zero(), PxSize::zero(), size),
608                CaretShape::Insert => PxCornerRadius::new_all(size),
609                CaretShape::SelectionRight => PxCornerRadius::new(PxSize::zero(), size, size, PxSize::zero()),
610            };
611
612            if !matches!(shape, CaretShape::Insert) {
613                size.width *= 0.8;
614            }
615
616            let line_height = TEXT.laidout().shaped_text.line_height();
617
618            let rect = PxRect::new(PxPoint::new(Px(0), line_height), size);
619            frame.push_clip_rounded_rect(rect, corners, false, false, |frame| {
620                frame.push_color(rect, FrameValue::Value(colors::AZURE));
621            });
622
623            let caret_thickness = Dip::new(1).to_px(frame.scale_factor());
624
625            let line_pos = match shape {
626                CaretShape::SelectionLeft => PxPoint::new(size.width - caret_thickness, Px(0)),
627                CaretShape::Insert => PxPoint::new(size.width / 2 - caret_thickness / 2, Px(0)),
628                CaretShape::SelectionRight => PxPoint::zero(),
629            };
630            let rect = PxRect::new(line_pos, PxSize::new(caret_thickness, line_height));
631            frame.with_hit_tests_disabled(|frame| {
632                frame.push_color(rect, FrameValue::Value(colors::AZURE));
633            });
634        }
635        _ => {}
636    })
637}
638
639context_local! {
640    static TOUCH_CARET_SPOT: Atomic<PxPoint> = Atomic::new(PxPoint::zero());
641}
642
643/// Set the caret *hotspot* that marks the middle of the caret on the text line.
644///
645/// See [`interactive_caret_visual`] for more details.
646///
647/// [`interactive_caret_visual`]: fn@super::interactive_caret_visual
648pub fn set_interactive_caret_spot(caret_line_spot: PxPoint) {
649    TOUCH_CARET_SPOT.get().store(caret_line_spot, Ordering::Relaxed);
650}