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