zng_wgt_text/node/
caret.rs

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