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
40pub 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
95pub 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 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 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 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 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 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 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 inner_text: PxTransform,
364 x: Px,
366 y: Px,
367 rich_text_hidden: bool,
368 shape: CaretShape,
370 deinit_on_capture_lost: bool,
372
373 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 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
571pub 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 }
592 CaretShape::SelectionRight => {
593 final_size.width *= 0.8;
594 caret_thickness / 2 }
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
641pub fn set_interactive_caret_spot(caret_line_spot: PxPoint) {
647 TOUCH_CARET_SPOT.get().store(caret_line_spot, Ordering::Relaxed);
648}