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 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
96pub 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 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 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 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 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 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 inner_text: PxTransform,
333 x: Px,
335 y: Px,
336 shape: CaretShape,
338
339 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 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
518pub 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 }
539 CaretShape::SelectionRight => {
540 final_size.width *= 0.8;
541 caret_thickness / 2 }
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
588pub fn set_interactive_caret_spot(caret_line_spot: PxPoint) {
594 TOUCH_CARET_SPOT.get().store(caret_line_spot, Ordering::Relaxed);
595}