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
39pub 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
94pub 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 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 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 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 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 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 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 inner_text: PxTransform,
362 x: Px,
364 y: Px,
365 rich_text_hidden: bool,
366 shape: CaretShape,
368 deinit_on_capture_lost: bool,
370
371 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 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
573pub 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 }
594 CaretShape::SelectionRight => {
595 final_size.width *= 0.8;
596 caret_thickness / 2 }
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
643pub fn set_interactive_caret_spot(caret_line_spot: PxPoint) {
649 TOUCH_CARET_SPOT.get().store(caret_line_spot, Ordering::Relaxed);
650}