1use std::sync::Arc;
4
5use crate::engine::layouter::types::TextFlowStyle;
6
7use super::layouter::types::{ContainerStyle, InfoNode, NodeKind, TextStyle};
8use super::renderer_model::{AffineTransform, StickyViewport, is_scrollport, sticky_offset};
9use super::ui::PointerEvent;
10use super::ui::custom_node::CustomNode;
11use super::ui::input_text_types::InputTextEvent;
12use ui_layout::{BoxModel, LayoutNode, Position};
13#[derive(Clone, Debug)]
15pub struct HitItem<'a> {
16 pub layout: &'a LayoutNode,
17 pub info: &'a InfoNode,
18}
19
20pub type HitPath<'a> = Vec<HitItem<'a>>;
22
23pub fn hit_custom_node<'a>(path: &'a HitPath<'a>) -> Option<&'a Arc<dyn CustomNode>> {
28 path.iter().find_map(|hit| match &hit.info.kind {
29 NodeKind::Custom { node, .. } => Some(node),
30 _ => None,
31 })
32}
33
34pub fn hit_dom_id(path: &HitPath<'_>) -> Option<u32> {
39 path.iter().find_map(|hit| hit.info.dom_id)
40}
41
42pub fn hit_dom_path(path: &HitPath<'_>) -> Vec<u32> {
48 let mut out: Vec<u32> = path
49 .iter()
50 .rev()
51 .filter_map(|hit| hit.info.dom_id)
52 .collect();
53 out.dedup();
54 out
55}
56
57fn is_inline(layout: &LayoutNode) -> bool {
58 matches!(layout.layout_box, ui_layout::LayoutBox::InlineBox(_))
59}
60
61fn is_inline_container(layout: &LayoutNode, info: &InfoNode) -> bool {
62 is_inline(layout) && matches!(&info.kind, NodeKind::Container { .. })
63}
64
65fn node_style(info: &InfoNode) -> ContainerStyle {
66 match &info.kind {
67 NodeKind::Container { style, .. }
68 | NodeKind::Custom { style, .. }
69 | NodeKind::Svg { style, .. } => style.clone(),
70 _ => ContainerStyle::default(),
71 }
72}
73
74fn establishes_inline_box_space(layout: &LayoutNode) -> bool {
75 is_inline(layout)
76 && layout.style.display.outer() == Some(ui_layout::OuterDisplay::Inline)
77 && layout.style.display.inner() == Some(ui_layout::InnerDisplay::FlowRoot)
78}
79
80fn child_viewport(
81 layout: &LayoutNode,
82 info: &InfoNode,
83 parent_viewport: StickyViewport,
84 is_root: bool,
85) -> StickyViewport {
86 if is_root || is_scrollport(&info.kind) {
87 let bm = layout.layout_box.iter().next();
88 let (scroll_x, scroll_y) = info.kind.scroll_offsets();
89 StickyViewport {
90 top_left: bm.as_ref().map_or((0.0, 0.0), |b| {
91 (
92 b.padding_box.x - b.content_box.x - scroll_x,
93 b.padding_box.y - b.content_box.y + scroll_y,
94 )
95 }),
96 size: if is_root {
97 parent_viewport.size
98 } else {
99 bm.map_or(parent_viewport.size, |b| {
100 (b.padding_box.width, b.padding_box.height)
101 })
102 },
103 }
104 } else {
105 let bm = layout.layout_box.iter().next();
106 StickyViewport {
107 top_left: bm.map_or(parent_viewport.top_left, |b| {
108 (
109 parent_viewport.top_left.0 - b.content_box.x,
110 parent_viewport.top_left.1 - b.content_box.y,
111 )
112 }),
113 size: parent_viewport.size,
114 }
115 }
116}
117
118fn child_containing(layout: &LayoutNode, parent_containing: (f32, f32)) -> (f32, f32) {
119 layout
120 .layout_box
121 .iter()
122 .next()
123 .map_or(parent_containing, |b| {
124 (b.content_box.width, b.content_box.height)
125 })
126}
127
128fn element_transform(
129 style: &ContainerStyle,
130 border_box: &ui_layout::Rect,
131) -> Option<AffineTransform> {
132 let t = style.transform?;
133 if t == AffineTransform::identity() {
134 return None;
135 }
136 let center_x = border_box.width / 2.0;
137 let center_y = border_box.height / 2.0;
138 Some(
139 AffineTransform::translate(center_x, center_y)
140 .then(&t)
141 .then(&AffineTransform::translate(-center_x, -center_y)),
142 )
143}
144
145fn map_to_box_local(
152 point: (f32, f32),
153 box_model: &BoxModel,
154 style: &ContainerStyle,
155 scroll: (f32, f32),
156 is_inline_box: bool,
157) -> Option<(f32, f32)> {
158 let mut p = point;
159 if !is_inline_box {
160 p.0 -= box_model.border_box.x;
162 p.1 -= box_model.border_box.y;
163 if let Some(t) = element_transform(style, &box_model.border_box) {
165 let inv = t.inverse()?;
166 p = inv.apply(p.0, p.1);
167 }
168 }
169 let rect = if is_inline_box {
171 box_model.padding_box
172 } else {
173 ui_layout::Rect {
174 x: box_model.padding_box.x - box_model.border_box.x,
175 y: box_model.padding_box.y - box_model.border_box.y,
176 width: box_model.padding_box.width,
177 height: box_model.padding_box.height,
178 }
179 };
180 if p.0 < rect.x || p.1 < rect.y || p.0 > rect.x + rect.width || p.1 > rect.y + rect.height {
181 return None;
182 }
183 if is_inline_box {
184 Some(p)
185 } else {
186 let dx = box_model.content_box.x - box_model.border_box.x;
188 let dy = box_model.content_box.y - box_model.border_box.y;
189 Some((p.0 - dx + scroll.0, p.1 - dy + scroll.1))
190 }
191}
192
193fn map_to_content_local(
197 point: (f32, f32),
198 box_model: &BoxModel,
199 style: &ContainerStyle,
200 scroll: (f32, f32),
201) -> Option<(f32, f32)> {
202 let mut p = point;
203 p.0 -= box_model.border_box.x;
204 p.1 -= box_model.border_box.y;
205 if let Some(t) = element_transform(style, &box_model.border_box) {
206 let inv = t.inverse()?;
207 p = inv.apply(p.0, p.1);
208 }
209 let dx = box_model.content_box.x - box_model.border_box.x;
210 let dy = box_model.content_box.y - box_model.border_box.y;
211 p.0 = p.0 - dx + scroll.0;
212 p.1 = p.1 - dy + scroll.1;
213 Some(p)
214}
215
216fn local_pointer_coords(path: &HitPath<'_>, target: usize, x: f32, y: f32) -> (f32, f32) {
222 let mut p = (x, y);
223 let mut accumulated_scroll = (0.0f32, 0.0f32);
224 let mut viewport = StickyViewport {
225 top_left: (0.0, 0.0),
226 size: (f32::INFINITY, f32::INFINITY),
227 };
228 let mut containing = (f32::INFINITY, f32::INFINITY);
229 if let Some(root) = path.last() {
230 let size = root
231 .layout
232 .layout_box
233 .iter()
234 .next()
235 .map_or((f32::INFINITY, f32::INFINITY), |b| {
236 (b.content_box.width, b.content_box.height)
237 });
238 let (sx, sy) = root.info.kind.scroll_offsets();
239 viewport = StickyViewport {
240 top_left: (-sx, sy),
241 size,
242 };
243 containing = size;
244 }
245
246 for step in 0..path.len() {
248 let index = path.len() - 1 - step; if index < target {
250 break;
251 }
252 let hit = &path[index];
253 let is_root = step == 0;
254 let is_fixed = hit.layout.style.position.kind == Position::Fixed;
255 let is_sticky = hit.layout.style.position.kind == Position::Sticky;
256 let shares_parent_layout =
257 index + 1 < path.len() && std::ptr::eq(hit.layout, path[index + 1].layout);
258 let inline_container = is_inline_container(hit.layout, hit.info);
259
260 if is_fixed {
261 p.0 -= accumulated_scroll.0;
262 p.1 -= accumulated_scroll.1;
263 }
264 if is_sticky
265 && !is_fixed
266 && let Some(bm) = hit.layout.layout_box.iter().next()
267 && let Some(edges) = bm.sticky_edges
268 {
269 let (dx, dy) = sticky_offset(&edges, &bm.border_box, viewport, containing);
270 p.0 -= dx;
271 p.1 -= dy;
272 }
273
274 let own_scroll = hit.info.kind.scroll_offsets();
275 let style = node_style(hit.info);
276
277 if index == target {
278 if !shares_parent_layout
279 && !inline_container
280 && let Some(bm) = hit.layout.layout_box.iter().next()
281 && let Some(lp) = map_to_content_local(p, &bm, &style, own_scroll)
282 {
283 p = lp;
284 }
285 return p;
286 }
287
288 if !shares_parent_layout
290 && !inline_container
291 && let Some(bm) = hit.layout.layout_box.iter().next()
292 && let Some(lp) = map_to_content_local(p, &bm, &style, own_scroll)
293 {
294 p = lp;
295 }
296
297 let next_index = index - 1;
298 if next_index < target {
299 break;
300 }
301 let next = &path[next_index];
302 let next_shares = std::ptr::eq(next.layout, hit.layout);
303 if !next_shares && establishes_inline_box_space(hit.layout) && inline_container {
304 if let Some(bm) = hit.layout.layout_box.iter().next() {
306 p.0 -= bm.content_box.x;
307 }
308 }
309
310 let child_scroll = if is_fixed {
311 own_scroll
312 } else {
313 (
314 accumulated_scroll.0 + own_scroll.0,
315 accumulated_scroll.1 + own_scroll.1,
316 )
317 };
318 viewport = child_viewport(hit.layout, hit.info, viewport, is_root);
319 containing = child_containing(hit.layout, containing);
320 accumulated_scroll = child_scroll;
321 let _ = next;
322 }
323
324 p
325}
326
327pub fn dispatch_pointer(path: &HitPath<'_>, event: PointerEvent) -> bool {
332 for (target, hit) in path.iter().enumerate() {
333 if let NodeKind::Custom {
334 node,
335 text_style,
336 text_flow_style,
337 ..
338 } = &hit.info.kind
339 {
340 let (px, py) = match event {
341 PointerEvent::Move { x, y } => (x, y),
342 PointerEvent::Down { x, y } => (x, y),
343 PointerEvent::Up { x, y } => (x, y),
344 PointerEvent::Leave => (0.0, 0.0), };
346 let (lx, ly) = local_pointer_coords(path, target, px, py);
347 let local_event = match event {
348 PointerEvent::Move { .. } => PointerEvent::Move { x: lx, y: ly },
349 PointerEvent::Down { .. } => PointerEvent::Down { x: lx, y: ly },
350 PointerEvent::Up { .. } => PointerEvent::Up { x: lx, y: ly },
351 PointerEvent::Leave => PointerEvent::Leave,
352 };
353
354 if let Some(popup) = node.popup(text_style, text_flow_style) {
357 let in_popup = lx >= popup.rect.x
358 && ly >= popup.rect.y
359 && lx <= popup.rect.x + popup.rect.width
360 && ly <= popup.rect.y + popup.rect.height;
361 let popup_event = match local_event {
364 PointerEvent::Move { x, y } => PointerEvent::Move {
365 x: x - popup.rect.x,
366 y: y - popup.rect.y,
367 },
368 PointerEvent::Down { x, y } => PointerEvent::Down {
369 x: x - popup.rect.x,
370 y: y - popup.rect.y,
371 },
372 PointerEvent::Up { x, y } => PointerEvent::Up {
373 x: x - popup.rect.x,
374 y: y - popup.rect.y,
375 },
376 PointerEvent::Leave => PointerEvent::Leave,
377 };
378
379 if in_popup {
380 return node.on_popup_pointer_event(popup_event);
381 }
382 }
383 return node.on_pointer_event(local_event);
384 }
385 }
386 false
387}
388
389pub fn dismiss_open_popups(info: &InfoNode, path: &HitPath<'_>, px: f32, py: f32) -> bool {
396 let mut dismissed = false;
397 dismiss_open_popups_inner(info, path, px, py, &mut dismissed);
398 dismissed
399}
400
401fn dismiss_open_popups_inner(
402 info: &InfoNode,
403 path: &HitPath<'_>,
404 px: f32,
405 py: f32,
406 dismissed: &mut bool,
407) {
408 if let NodeKind::Custom { node, .. } = &info.kind
409 && let Some(popup) = node.popup(&TextStyle::default(), &TextFlowStyle::default())
410 {
411 let owner_in_path = path.iter().any(|hit| {
412 matches!(&hit.info.kind, NodeKind::Custom { node: owner, .. } if Arc::ptr_eq(node, owner))
413 });
414 let press_in_popup = if let Some((target, _)) = path.iter().enumerate().find(|(_, hit)| {
415 matches!(&hit.info.kind, NodeKind::Custom { node: owner, .. } if Arc::ptr_eq(node, owner))
416 }) {
417 let (lx, ly) = local_pointer_coords(path, target, px, py);
418 lx >= popup.rect.x
419 && ly >= popup.rect.y
420 && lx <= popup.rect.x + popup.rect.width
421 && ly <= popup.rect.y + popup.rect.height
422 } else {
423 false
424 };
425 if !owner_in_path && !press_in_popup {
426 node.dismiss_popup();
427 *dismissed = true;
428 }
429 }
430 for child in &info.children {
431 dismiss_open_popups_inner(child, path, px, py, dismissed);
432 }
433}
434
435pub fn update_hover(path: &HitPath<'_>, previous: Option<&Arc<dyn CustomNode>>) -> bool {
440 let current = hit_custom_node(path);
441 match (previous, current) {
442 (Some(prev), Some(curr)) if Arc::ptr_eq(prev, curr) => false,
443 (Some(prev), _) => {
444 prev.set_hovered(false);
445 if let Some(curr) = current {
446 curr.set_hovered(true);
447 }
448 true
449 }
450 (None, Some(curr)) => {
451 curr.set_hovered(true);
452 true
453 }
454 (None, None) => false,
455 }
456}
457
458pub fn hit_test<'a>(layout: &'a LayoutNode, info: &'a InfoNode, x: f32, y: f32) -> HitPath<'a> {
459 if let Some(path) = hit_test_popup(layout, info, x, y) {
463 return path;
464 }
465 let size = layout
466 .layout_box
467 .iter()
468 .next()
469 .map_or((f32::INFINITY, f32::INFINITY), |b| {
470 (b.content_box.width, b.content_box.height)
471 });
472 let (sx, sy) = info.kind.scroll_offsets();
473 let root_viewport = StickyViewport {
474 top_left: (-sx, sy),
475 size,
476 };
477 hit_test_inner(layout, info, (x, y), (0.0, 0.0), root_viewport, size, true)
478}
479
480#[allow(clippy::too_many_arguments)]
481fn hit_test_inner<'a>(
482 layout: &'a LayoutNode,
483 info: &'a InfoNode,
484 point: (f32, f32),
485 accumulated_scroll: (f32, f32),
486 viewport: StickyViewport,
487 containing: (f32, f32),
488 is_root: bool,
489) -> HitPath<'a> {
490 if layout.layout_box.is_empty() {
491 return Vec::new();
492 }
493
494 let inline = is_inline(layout);
495 let inline_container = is_inline_container(layout, info);
496
497 let is_fixed = layout.style.position.kind == Position::Fixed;
498 let mut p = point;
499 if is_fixed {
500 p.0 -= accumulated_scroll.0;
501 p.1 -= accumulated_scroll.1;
502 }
503 let is_sticky = layout.style.position.kind == Position::Sticky;
504 let mut sticky_shift = (0.0, 0.0);
505 if is_sticky
506 && !is_fixed
507 && let Some(bm) = layout.layout_box.iter().next()
508 && let Some(edges) = bm.sticky_edges
509 {
510 sticky_shift = sticky_offset(&edges, &bm.border_box, viewport, containing);
511 p.0 -= sticky_shift.0;
512 p.1 -= sticky_shift.1;
513 }
514
515 let own_scroll = info.kind.scroll_offsets();
516 let child_scroll = if is_fixed {
517 own_scroll
518 } else {
519 (
520 accumulated_scroll.0 + own_scroll.0,
521 accumulated_scroll.1 + own_scroll.1,
522 )
523 };
524 let child_vp = child_viewport(layout, info, viewport, is_root);
525 let child_cont = child_containing(layout, containing);
526 let style = node_style(info);
527
528 let boxes: Vec<_> = layout.layout_box.iter().collect();
530 for box_model in boxes.into_iter().rev() {
531 let Some((local_x, local_y)) = map_to_box_local(p, &box_model, &style, own_scroll, inline)
532 else {
533 continue;
534 };
535
536 let mut child_x = if inline_container { p.0 } else { local_x };
538 let child_y = if inline_container { p.1 } else { local_y };
539
540 if establishes_inline_box_space(layout) && !inline_container {
541 child_x -= box_model.content_box.x;
542 }
543
544 for (child_layout, child_info) in layout.children.iter().zip(&info.children).rev() {
546 if let Some(child_node) = child_layout.node() {
547 let mut path = hit_test_inner(
548 child_node,
549 child_info,
550 (child_x, child_y),
551 child_scroll,
552 child_vp,
553 child_cont,
554 false,
555 );
556 if !path.is_empty() {
557 path.push(HitItem { layout, info });
558 return path;
559 }
560 } else if let Some(result) = child_layout.custom_result()
561 && result.spans.iter().any(|span| {
562 child_x >= span.line_pos.0
563 && child_x <= span.line_pos.0 + span.width()
564 && child_y >= span.line_pos.1
565 && child_y <= span.line_pos.1 + result.box_model.content_box.height
566 })
567 {
568 return vec![
569 HitItem {
570 layout,
571 info: child_info,
572 },
573 HitItem { layout, info },
574 ];
575 }
576 }
577
578 let _ = sticky_shift;
580 return vec![HitItem { layout, info }];
581 }
582
583 Vec::new()
584}
585
586fn hit_test_popup<'a>(
589 layout: &'a LayoutNode,
590 info: &'a InfoNode,
591 x: f32,
592 y: f32,
593) -> Option<HitPath<'a>> {
594 let mut best: Option<HitPath<'a>> = None;
595 let mut prefix: HitPath<'a> = Vec::new();
596 let size = layout
597 .layout_box
598 .iter()
599 .next()
600 .map_or((f32::INFINITY, f32::INFINITY), |b| {
601 (b.content_box.width, b.content_box.height)
602 });
603 let (sx, sy) = info.kind.scroll_offsets();
604 let root_viewport = StickyViewport {
605 top_left: (-sx, sy),
606 size,
607 };
608 hit_test_popup_inner(
609 layout,
610 info,
611 (x, y),
612 (0.0, 0.0),
613 root_viewport,
614 size,
615 true,
616 &mut prefix,
617 &mut best,
618 );
619 best
620}
621
622#[allow(clippy::too_many_arguments)]
625fn hit_test_popup_inner<'a>(
626 layout: &'a LayoutNode,
627 info: &'a InfoNode,
628 point: (f32, f32),
629 accumulated_scroll: (f32, f32),
630 viewport: StickyViewport,
631 containing: (f32, f32),
632 is_root: bool,
633 prefix: &mut HitPath<'a>,
634 best: &mut Option<HitPath<'a>>,
635) {
636 if layout.layout_box.is_empty() {
637 return;
638 }
639
640 let inline_container = is_inline_container(layout, info);
641
642 let is_fixed = layout.style.position.kind == Position::Fixed;
643 let mut p = point;
644 if is_fixed {
645 p.0 -= accumulated_scroll.0;
646 p.1 -= accumulated_scroll.1;
647 }
648 let is_sticky = layout.style.position.kind == Position::Sticky;
649 if is_sticky
650 && !is_fixed
651 && let Some(bm) = layout.layout_box.iter().next()
652 && let Some(edges) = bm.sticky_edges
653 {
654 let (dx, dy) = sticky_offset(&edges, &bm.border_box, viewport, containing);
655 p.0 -= dx;
656 p.1 -= dy;
657 }
658
659 let own_scroll = info.kind.scroll_offsets();
660 let child_scroll = if is_fixed {
661 own_scroll
662 } else {
663 (
664 accumulated_scroll.0 + own_scroll.0,
665 accumulated_scroll.1 + own_scroll.1,
666 )
667 };
668 let child_vp = child_viewport(layout, info, viewport, is_root);
669 let child_cont = child_containing(layout, containing);
670 let style = node_style(info);
671
672 let (local_x, local_y) = if inline_container {
673 p
674 } else if let Some(bm) = layout.layout_box.iter().next() {
675 map_to_content_local(p, &bm, &style, own_scroll).unwrap_or(p)
676 } else {
677 p
678 };
679
680 if let NodeKind::Custom {
681 node,
682 text_style,
683 text_flow_style,
684 ..
685 } = &info.kind
686 && let Some(popup) = node.popup(text_style, text_flow_style)
687 && local_x >= popup.rect.x
688 && local_y >= popup.rect.y
689 && local_x <= popup.rect.x + popup.rect.width
690 && local_y <= popup.rect.y + popup.rect.height
691 {
692 let mut path = prefix.clone();
693 path.push(HitItem { layout, info });
694 path.reverse();
695 *best = Some(path);
696 }
697
698 let mut child_x = if inline_container { p.0 } else { local_x };
700 let child_y = if inline_container { p.1 } else { local_y };
701 if establishes_inline_box_space(layout)
702 && inline_container
703 && let Some(bm) = layout.layout_box.iter().next()
704 {
705 child_x -= bm.content_box.x;
706 }
707
708 prefix.push(HitItem { layout, info });
709 for (child_layout, child_info) in layout.children.iter().zip(&info.children) {
710 if let Some(child_node) = child_layout.node() {
711 hit_test_popup_inner(
712 child_node,
713 child_info,
714 (child_x, child_y),
715 child_scroll,
716 child_vp,
717 child_cont,
718 false,
719 prefix,
720 best,
721 );
722 }
723 }
724 prefix.pop();
725}
726
727pub const NO_SCROLL_DOM_ID: u32 = u32::MAX;
731
732pub fn scroll_at(
734 layout: &LayoutNode,
735 info: &mut InfoNode,
736 viewport: (f32, f32),
737 x: f32,
738 y: f32,
739 dx: f32,
740 dy: f32,
741) -> Option<u32> {
742 let size = layout
743 .layout_box
744 .iter()
745 .next()
746 .map_or((f32::INFINITY, f32::INFINITY), |b| {
747 (b.content_box.width, b.content_box.height)
748 });
749 let (sx, sy) = info.kind.scroll_offsets();
750 let root_viewport = StickyViewport {
751 top_left: (-sx, sy),
752 size,
753 };
754 scroll_at_inner(
755 layout,
756 info,
757 viewport,
758 (x, y),
759 dx,
760 dy,
761 (0.0, 0.0),
762 root_viewport,
763 size,
764 true,
765 )
766}
767
768#[allow(clippy::too_many_arguments)]
769fn scroll_at_inner(
770 layout: &LayoutNode,
771 info: &mut InfoNode,
772 window_viewport: (f32, f32),
773 point: (f32, f32),
774 dx: f32,
775 dy: f32,
776 accumulated_scroll: (f32, f32),
777 viewport: StickyViewport,
778 containing: (f32, f32),
779 is_root: bool,
780) -> Option<u32> {
781 if layout.layout_box.is_empty() {
782 return None;
783 }
784
785 let inline = is_inline(layout);
786 let inline_container = is_inline_container(layout, info);
787
788 let is_fixed = layout.style.position.kind == Position::Fixed;
789 let mut p = point;
790 if is_fixed {
791 p.0 -= accumulated_scroll.0;
792 p.1 -= accumulated_scroll.1;
793 }
794 let is_sticky = layout.style.position.kind == Position::Sticky;
795 if is_sticky
796 && !is_fixed
797 && let Some(bm) = layout.layout_box.iter().next()
798 && let Some(edges) = bm.sticky_edges
799 {
800 let (sdx, sdy) = sticky_offset(&edges, &bm.border_box, viewport, containing);
801 p.0 -= sdx;
802 p.1 -= sdy;
803 }
804
805 let own_scroll = info.kind.scroll_offsets();
806 let child_scroll = if is_fixed {
807 own_scroll
808 } else {
809 (
810 accumulated_scroll.0 + own_scroll.0,
811 accumulated_scroll.1 + own_scroll.1,
812 )
813 };
814 let child_vp = child_viewport(layout, info, viewport, is_root);
815 let child_cont = child_containing(layout, containing);
816 let style = node_style(info);
817
818 let boxes: Vec<_> = layout.layout_box.iter().collect();
819 for box_model in boxes.into_iter().rev() {
820 let Some((local_x, local_y)) = map_to_box_local(p, &box_model, &style, own_scroll, inline)
821 else {
822 continue;
823 };
824 let mut child_x = if inline_container { p.0 } else { local_x };
825 let child_y = if inline_container { p.1 } else { local_y };
826
827 if establishes_inline_box_space(layout) && !inline_container {
828 child_x -= box_model.content_box.x;
829 }
830
831 let mut i = info.children.len();
833 while i > 0 {
834 i -= 1;
835 let Some(child_node) = layout.children.get(i).and_then(|c| c.node()) else {
836 continue;
837 };
838 if let Some(scrolled_id) = scroll_at_inner(
839 child_node,
840 &mut info.children[i],
841 window_viewport,
842 (child_x, child_y),
843 dx,
844 dy,
845 child_scroll,
846 child_vp,
847 child_cont,
848 false,
849 ) {
850 return Some(scrolled_id);
851 }
852 }
853
854 let scrolled = match &mut info.kind {
855 NodeKind::Container {
856 scroll_x,
857 scroll_y,
858 scroll_offset_x,
859 scroll_offset_y,
860 ..
861 }
862 | NodeKind::Custom {
863 scroll_x,
864 scroll_y,
865 scroll_offset_x,
866 scroll_offset_y,
867 ..
868 } => {
869 let mut changed = false;
870 let (vw, vh) = window_viewport;
871
872 if *scroll_y {
873 let max_scroll = (box_model.children_box.height
874 - box_model.content_box.height.min(vh))
875 .max(0.0);
876 let next = (*scroll_offset_y + dy).clamp(0.0, max_scroll);
877 if (next - *scroll_offset_y).abs() > f32::EPSILON {
878 changed = true;
879 }
880 *scroll_offset_y = next;
881 }
882 if *scroll_x {
883 let max_scroll = (box_model.children_box.width
884 - box_model.content_box.width.min(vw))
885 .max(0.0);
886 let next = (*scroll_offset_x + dx).clamp(0.0, max_scroll);
887 if (next - *scroll_offset_x).abs() > f32::EPSILON {
888 changed = true;
889 }
890 *scroll_offset_x = next;
891 }
892 changed
893 }
894 _ => false,
895 };
896 if scrolled {
897 return Some(info.dom_id.unwrap_or(NO_SCROLL_DOM_ID));
898 }
899 }
900
901 None
902}
903
904pub fn focus_text_input(info: &InfoNode, target: Option<&Arc<dyn CustomNode>>) -> bool {
908 let mut focused = false;
909 if let NodeKind::Custom { node, .. } = &info.kind
910 && node.accepts_text_input()
911 {
912 let is_target = target.is_some_and(|target| Arc::ptr_eq(node, target));
913 node.set_focused(is_target);
914 focused |= is_target;
915 }
916 for child in &info.children {
917 focused |= focus_text_input(child, target);
918 }
919 focused
920}
921
922pub fn dispatch_text_input(info: &InfoNode, event: InputTextEvent) -> bool {
924 if let NodeKind::Custom { node, .. } = &info.kind
925 && node.accepts_text_input()
926 && node.is_focused()
927 {
928 return node.handle_text_input(event);
929 }
930 for child in &info.children {
931 if dispatch_text_input(child, event.clone()) {
932 return true;
933 }
934 }
935 false
936}
937
938pub fn focused_text_input_is_composing(info: &InfoNode) -> bool {
940 if let NodeKind::Custom { node, .. } = &info.kind
941 && node.accepts_text_input()
942 && node.is_focused()
943 {
944 return node.is_composing();
945 }
946 info.children.iter().any(focused_text_input_is_composing)
947}
948
949pub fn any_custom_node_needs_repaint(info: &InfoNode) -> bool {
954 if let NodeKind::Custom { node, .. } = &info.kind
955 && node.needs_repaint()
956 {
957 return true;
958 }
959 info.children.iter().any(any_custom_node_needs_repaint)
960}
961
962#[cfg(test)]
963mod tests {
964 use super::*;
965 use crate::engine::bridge::text::{FallbackTextMeasurer, TextMeasurer};
966 use crate::engine::layouter::types::{ContainerRole, ContainerStyle, TextStyle};
967 use crate::engine::renderer_model::{DrawCommand, Rect};
968 use crate::engine::ui::button::ButtonComponent;
969 use crate::engine::ui::input_text::InputTextComponent;
970 use crate::engine::ui::input_text_types::InputTextEvent;
971 use crate::engine::ui::{ContentSize, Popup};
972 use std::sync::Arc;
973 use std::sync::Mutex;
974 use std::sync::atomic::{AtomicBool, Ordering};
975 use ui_layout::{LayoutChild, Style};
976
977 const VIEWPORT_WIDTH: f32 = 800.0;
978 const VIEWPORT_HEIGHT: f32 = 600.0;
979
980 fn input_info(node: Arc<dyn CustomNode>) -> InfoNode {
981 InfoNode {
982 kind: NodeKind::Custom {
983 node,
984 scroll_x: false,
985 scroll_y: false,
986 scroll_offset_x: 0.0,
987 scroll_offset_y: 0.0,
988 style: ContainerStyle::default(),
989 layout_style: ui_layout::Style::default(),
990 text_style: TextStyle::default(),
991 text_flow_style: TextFlowStyle::default(),
992 },
993 children: Vec::new(),
994 dom_id: None,
995 }
996 }
997
998 #[test]
999 fn focus_and_dispatch_target_one_input() {
1000 let measurer: Arc<dyn TextMeasurer> = Arc::new(FallbackTextMeasurer);
1001 let first: Arc<dyn CustomNode> =
1002 Arc::new(InputTextComponent::new("", "", Arc::clone(&measurer)));
1003 let second: Arc<dyn CustomNode> = Arc::new(InputTextComponent::new("", "", measurer));
1004 let root = InfoNode {
1005 kind: NodeKind::LineBreak,
1006 children: vec![
1007 input_info(Arc::clone(&first)),
1008 input_info(Arc::clone(&second)),
1009 ],
1010 dom_id: None,
1011 };
1012
1013 assert!(focus_text_input(&root, Some(&second)));
1014 assert!(!first.is_focused());
1015 assert!(second.is_focused());
1016 assert!(dispatch_text_input(
1017 &root,
1018 InputTextEvent::Commit("日本".into())
1019 ));
1020 }
1021
1022 #[test]
1023 fn hit_custom_node_finds_innermost_custom() {
1024 let node: Arc<dyn CustomNode> = Arc::new(InputTextComponent::new(
1025 "",
1026 "",
1027 Arc::new(FallbackTextMeasurer),
1028 ));
1029 let info = input_info(Arc::clone(&node));
1030 let layout = LayoutNode::new(ui_layout::Style::default());
1031 let path = vec![
1032 HitItem {
1033 layout: &layout,
1034 info: &info,
1035 },
1036 HitItem {
1037 layout: &layout,
1038 info: &info,
1039 },
1040 ];
1041 assert!(Arc::ptr_eq(hit_custom_node(&path).unwrap(), &node));
1042 }
1043
1044 #[test]
1045 fn update_hover_switches_target() {
1046 let measurer: Arc<dyn TextMeasurer> = Arc::new(FallbackTextMeasurer);
1047 let a: Arc<dyn CustomNode> = Arc::new(ButtonComponent::new("A", Arc::clone(&measurer)));
1048 let b: Arc<dyn CustomNode> = Arc::new(ButtonComponent::new("B", measurer));
1049 let info_a = input_info(Arc::clone(&a));
1050 let info_b = input_info(Arc::clone(&b));
1051 let layout = LayoutNode::new(ui_layout::Style::default());
1052 let path_a = vec![HitItem {
1053 layout: &layout,
1054 info: &info_a,
1055 }];
1056 let path_b = vec![HitItem {
1057 layout: &layout,
1058 info: &info_b,
1059 }];
1060
1061 assert!(update_hover(&path_a, None));
1062 assert!(a.is_hovered());
1063 assert!(!b.is_hovered());
1064
1065 assert!(update_hover(&path_b, Some(&a)));
1066 assert!(!a.is_hovered());
1067 assert!(b.is_hovered());
1068
1069 assert!(!update_hover(&path_b, Some(&b)));
1070 }
1071
1072 #[test]
1073 fn any_custom_node_needs_repaint_tracks_dirty_nodes() {
1074 let measurer: Arc<dyn TextMeasurer> = Arc::new(FallbackTextMeasurer);
1075 let a: Arc<dyn CustomNode> = Arc::new(ButtonComponent::new("A", Arc::clone(&measurer)));
1076 let info_a = input_info(Arc::clone(&a));
1077
1078 assert!(any_custom_node_needs_repaint(&info_a));
1080 assert!(!any_custom_node_needs_repaint(&info_a));
1081
1082 a.on_pointer_event(PointerEvent::Down { x: 0.0, y: 0.0 });
1084 assert!(any_custom_node_needs_repaint(&info_a));
1085 assert!(!any_custom_node_needs_repaint(&info_a));
1086 }
1087
1088 fn box_model(
1089 x: f32,
1090 y: f32,
1091 width: f32,
1092 height: f32,
1093 children_width: f32,
1094 children_height: f32,
1095 ) -> ui_layout::BoxModel {
1096 let rect = ui_layout::Rect {
1097 x,
1098 y,
1099 width,
1100 height,
1101 };
1102 ui_layout::BoxModel {
1103 sticky_edges: None,
1104 border_box: rect,
1105 padding_box: rect,
1106 content_box: rect,
1107 children_box: ui_layout::Rect {
1108 x,
1109 y,
1110 width: children_width,
1111 height: children_height,
1112 },
1113 }
1114 }
1115
1116 fn scrollable_info() -> InfoNode {
1117 InfoNode {
1118 kind: NodeKind::Container {
1119 scroll_x: false,
1120 scroll_y: true,
1121 scroll_offset_x: 0.0,
1122 scroll_offset_y: 0.0,
1123 style: ContainerStyle::default(),
1124 role: crate::engine::layouter::types::ContainerRole::Normal,
1125 },
1126 children: Vec::new(),
1127 dom_id: None,
1128 }
1129 }
1130
1131 fn container_info(scroll_y: bool, dom_id: Option<u32>) -> InfoNode {
1132 InfoNode {
1133 kind: NodeKind::Container {
1134 scroll_x: false,
1135 scroll_y,
1136 scroll_offset_x: 0.0,
1137 scroll_offset_y: 0.0,
1138 style: ContainerStyle::default(),
1139 role: crate::engine::layouter::types::ContainerRole::Normal,
1140 },
1141 children: Vec::new(),
1142 dom_id,
1143 }
1144 }
1145
1146 fn inline_node(children: Vec<LayoutChild>) -> LayoutNode {
1150 let mut node = LayoutNode::with_children(ui_layout::Style::default(), children);
1151 node.layout_box = ui_layout::LayoutBox::InlineBox(ui_layout::InlineBox {
1152 box_model: box_model(10.0, 10.0, 200.0, 20.0, 200.0, 20.0),
1153 line_spans: vec![ui_layout::LineSpan {
1154 x_range: 0.0..200.0,
1155 line_pos: (10.0, 10.0),
1156 line_index: 0,
1157 }],
1158 });
1159 node
1160 }
1161
1162 fn scroll_offset_y_of(info: &InfoNode) -> f32 {
1163 let NodeKind::Container {
1164 scroll_offset_y, ..
1165 } = &info.kind
1166 else {
1167 panic!("expected container");
1168 };
1169 *scroll_offset_y
1170 }
1171
1172 fn set_vertical_scroll(info: &mut InfoNode, offset: f32) {
1173 let NodeKind::Container {
1174 scroll_offset_y, ..
1175 } = &mut info.kind
1176 else {
1177 panic!("expected container");
1178 };
1179 *scroll_offset_y = offset;
1180 }
1181
1182 fn fixed_child_layout(children_height: f32) -> LayoutNode {
1183 let mut style = ui_layout::Style::default();
1184 style.position.kind = Position::Fixed;
1185 let mut child = LayoutNode::new(style);
1186 child.layout_box = ui_layout::LayoutBox::BlockBox(box_model(
1187 10.0,
1188 10.0,
1189 30.0,
1190 20.0,
1191 30.0,
1192 children_height,
1193 ));
1194 child
1195 }
1196
1197 #[test]
1198 fn hit_test_fixed_child_ignores_ancestor_scroll() {
1199 let mut layout =
1200 LayoutNode::with_children(ui_layout::Style::default(), [fixed_child_layout(20.0)]);
1201 layout.layout_box =
1202 ui_layout::LayoutBox::BlockBox(box_model(0.0, 0.0, 200.0, 100.0, 200.0, 300.0));
1203
1204 let mut info = scrollable_info();
1205 set_vertical_scroll(&mut info, 50.0);
1206 let mut fixed_info = scrollable_info();
1207 fixed_info.dom_id = Some(42);
1208 info.children.push(fixed_info);
1209
1210 let path = hit_test(&layout, &info, 15.0, 15.0);
1211 assert_eq!(hit_dom_id(&path), Some(42));
1212 }
1213
1214 #[test]
1215 fn scroll_at_fixed_child_ignores_ancestor_scroll() {
1216 let mut layout =
1217 LayoutNode::with_children(ui_layout::Style::default(), [fixed_child_layout(80.0)]);
1218 layout.layout_box =
1219 ui_layout::LayoutBox::BlockBox(box_model(0.0, 0.0, 200.0, 100.0, 200.0, 300.0));
1220
1221 let mut info = scrollable_info();
1222 set_vertical_scroll(&mut info, 50.0);
1223 info.children.push(scrollable_info());
1224
1225 assert!(
1226 scroll_at(
1227 &layout,
1228 &mut info,
1229 (VIEWPORT_WIDTH, VIEWPORT_HEIGHT),
1230 15.0,
1231 15.0,
1232 0.0,
1233 10.0
1234 )
1235 .is_some()
1236 );
1237 let NodeKind::Container {
1238 scroll_offset_y: child_scroll,
1239 ..
1240 } = &info.children[0].kind
1241 else {
1242 panic!("expected fixed child container");
1243 };
1244 assert_eq!(*child_scroll, 10.0);
1245 let NodeKind::Container {
1246 scroll_offset_y: parent_scroll,
1247 ..
1248 } = &info.kind
1249 else {
1250 panic!("expected parent container");
1251 };
1252 assert_eq!(*parent_scroll, 50.0);
1253 }
1254
1255 #[test]
1256 fn scroll_at_scrolls_under_point_clamped_to_range() {
1257 let mut layout = LayoutNode::new(ui_layout::Style::default());
1258 layout.layout_box =
1259 ui_layout::LayoutBox::BlockBox(box_model(0.0, 0.0, 200.0, 100.0, 200.0, 300.0));
1260 let mut info = scrollable_info();
1261
1262 assert!(
1264 scroll_at(
1265 &layout,
1266 &mut info,
1267 (VIEWPORT_WIDTH, VIEWPORT_HEIGHT),
1268 50.0,
1269 50.0,
1270 0.0,
1271 100.0
1272 )
1273 .is_some()
1274 );
1275 let NodeKind::Container {
1276 scroll_offset_y, ..
1277 } = &info.kind
1278 else {
1279 panic!("expected container");
1280 };
1281 assert_eq!(*scroll_offset_y, 100.0);
1282
1283 assert!(
1285 scroll_at(
1286 &layout,
1287 &mut info,
1288 (VIEWPORT_WIDTH, VIEWPORT_HEIGHT),
1289 50.0,
1290 50.0,
1291 0.0,
1292 300.0
1293 )
1294 .is_some()
1295 );
1296 let NodeKind::Container {
1297 scroll_offset_y, ..
1298 } = &info.kind
1299 else {
1300 panic!("expected container");
1301 };
1302 assert_eq!(*scroll_offset_y, 200.0);
1303
1304 assert!(
1306 scroll_at(
1307 &layout,
1308 &mut info,
1309 (VIEWPORT_WIDTH, VIEWPORT_HEIGHT),
1310 50.0,
1311 50.0,
1312 0.0,
1313 -500.0
1314 )
1315 .is_some()
1316 );
1317 let NodeKind::Container {
1318 scroll_offset_y, ..
1319 } = &info.kind
1320 else {
1321 panic!("expected container");
1322 };
1323 assert_eq!(*scroll_offset_y, 0.0);
1324 }
1325
1326 #[test]
1327 fn scroll_at_ignores_cursor_outside_box() {
1328 let mut layout = LayoutNode::new(ui_layout::Style::default());
1329 layout.layout_box =
1330 ui_layout::LayoutBox::BlockBox(box_model(0.0, 0.0, 200.0, 100.0, 200.0, 300.0));
1331 let mut info = scrollable_info();
1332
1333 assert!(
1334 scroll_at(
1335 &layout,
1336 &mut info,
1337 (VIEWPORT_WIDTH, VIEWPORT_HEIGHT),
1338 250.0,
1339 50.0,
1340 0.0,
1341 -100.0
1342 )
1343 .is_none()
1344 );
1345 }
1346
1347 #[test]
1348 fn scroll_at_ignores_non_scrollable_containers() {
1349 let mut layout = LayoutNode::new(ui_layout::Style::default());
1350 layout.layout_box =
1351 ui_layout::LayoutBox::BlockBox(box_model(0.0, 0.0, 200.0, 100.0, 200.0, 300.0));
1352 let mut info = InfoNode {
1353 kind: NodeKind::Container {
1354 scroll_x: false,
1355 scroll_y: false,
1356 scroll_offset_x: 0.0,
1357 scroll_offset_y: 0.0,
1358 style: ContainerStyle::default(),
1359 role: crate::engine::layouter::types::ContainerRole::Normal,
1360 },
1361 children: Vec::new(),
1362 dom_id: None,
1363 };
1364
1365 assert!(
1366 scroll_at(
1367 &layout,
1368 &mut info,
1369 (VIEWPORT_WIDTH, VIEWPORT_HEIGHT),
1370 50.0,
1371 50.0,
1372 0.0,
1373 -100.0
1374 )
1375 .is_none()
1376 );
1377 let NodeKind::Container {
1378 scroll_offset_y, ..
1379 } = &info.kind
1380 else {
1381 panic!("expected container");
1382 };
1383 assert_eq!(*scroll_offset_y, 0.0);
1384 }
1385
1386 #[test]
1387 fn scroll_at_prefers_innermost_scrollable_container() {
1388 let outer_children_box = box_model(0.0, 0.0, 400.0, 300.0, 400.0, 900.0);
1392 let inner_children_box = box_model(10.0, 10.0, 100.0, 80.0, 100.0, 240.0);
1393
1394 let mut outer_layout = LayoutNode::with_children(
1395 ui_layout::Style::default(),
1396 [LayoutNode::new(ui_layout::Style::default())],
1397 );
1398 outer_layout.layout_box = ui_layout::LayoutBox::BlockBox(outer_children_box);
1399
1400 let mut inner_layout = LayoutNode::new(ui_layout::Style::default());
1401 inner_layout.layout_box = ui_layout::LayoutBox::BlockBox(inner_children_box);
1402
1403 outer_layout.children[0] = ui_layout::LayoutChild::Node(Box::new(inner_layout));
1404
1405 let mut outer_info = scrollable_info();
1406 let inner_info = scrollable_info();
1407 outer_info.children.push(inner_info);
1408
1409 assert!(
1411 scroll_at(
1412 &outer_layout,
1413 &mut outer_info,
1414 (VIEWPORT_WIDTH, VIEWPORT_HEIGHT),
1415 50.0,
1416 50.0,
1417 0.0,
1418 30.0
1419 )
1420 .is_some()
1421 );
1422 let NodeKind::Container {
1423 scroll_offset_y, ..
1424 } = &outer_info.children[0].kind
1425 else {
1426 panic!("expected inner container");
1427 };
1428 assert_eq!(*scroll_offset_y, 30.0);
1429 let NodeKind::Container {
1430 scroll_offset_y: outer_off,
1431 ..
1432 } = &outer_info.kind
1433 else {
1434 panic!("expected outer container");
1435 };
1436 assert_eq!(*outer_off, 0.0);
1437 }
1438
1439 #[test]
1440 fn scroll_at_reports_the_scrolled_containers_dom_id() {
1441 let outer_children_box = box_model(0.0, 0.0, 400.0, 300.0, 400.0, 900.0);
1444 let inner_children_box = box_model(10.0, 10.0, 100.0, 80.0, 100.0, 240.0);
1445
1446 let mut outer_layout = LayoutNode::with_children(
1447 ui_layout::Style::default(),
1448 [LayoutNode::new(ui_layout::Style::default())],
1449 );
1450 outer_layout.layout_box = ui_layout::LayoutBox::BlockBox(outer_children_box);
1451 let mut inner_layout = LayoutNode::new(ui_layout::Style::default());
1452 inner_layout.layout_box = ui_layout::LayoutBox::BlockBox(inner_children_box);
1453 outer_layout.children[0] = ui_layout::LayoutChild::Node(Box::new(inner_layout));
1454
1455 let mut outer_info = container_info(true, Some(7));
1456 let inner_info = container_info(true, Some(9));
1457 outer_info.children.push(inner_info);
1458
1459 let scrolled = scroll_at(
1460 &outer_layout,
1461 &mut outer_info,
1462 (VIEWPORT_WIDTH, VIEWPORT_HEIGHT),
1463 50.0,
1464 50.0,
1465 0.0,
1466 30.0,
1467 );
1468 assert_eq!(scrolled, Some(9));
1469 }
1470
1471 #[test]
1472 fn scroll_at_reports_marker_when_scrolled_container_has_no_dom_id() {
1473 let mut layout = LayoutNode::new(ui_layout::Style::default());
1474 layout.layout_box =
1475 ui_layout::LayoutBox::BlockBox(box_model(0.0, 0.0, 200.0, 100.0, 200.0, 300.0));
1476 let mut info = container_info(true, None);
1477 let scrolled = scroll_at(
1478 &layout,
1479 &mut info,
1480 (VIEWPORT_WIDTH, VIEWPORT_HEIGHT),
1481 50.0,
1482 50.0,
1483 0.0,
1484 10.0,
1485 );
1486 assert_eq!(scrolled, Some(NO_SCROLL_DOM_ID));
1487 }
1488
1489 #[derive(Debug)]
1491 struct RecordingNode {
1492 pointer_events: Mutex<Vec<PointerEvent>>,
1493 popup_events: Mutex<Vec<PointerEvent>>,
1494 popup_rect: Rect,
1495 popup_open: AtomicBool,
1496 dismissed: AtomicBool,
1497 }
1498
1499 impl RecordingNode {
1500 fn new(popup_rect: Rect) -> Self {
1501 RecordingNode {
1502 pointer_events: Mutex::new(Vec::new()),
1503 popup_events: Mutex::new(Vec::new()),
1504 popup_rect,
1505 popup_open: AtomicBool::new(true),
1506 dismissed: AtomicBool::new(false),
1507 }
1508 }
1509
1510 fn events(&self) -> Vec<PointerEvent> {
1511 self.pointer_events.lock().unwrap().clone()
1512 }
1513
1514 fn popup_events(&self) -> Vec<PointerEvent> {
1515 self.popup_events.lock().unwrap().clone()
1516 }
1517 }
1518
1519 impl CustomNode for RecordingNode {
1520 fn draw_sized(
1521 &self,
1522 _cmd_buf: &mut Vec<DrawCommand>,
1523 _text_style: &TextStyle,
1524 _text_flow_style: &TextFlowStyle,
1525 _style: &Style,
1526 _size: ContentSize,
1527 ) {
1528 }
1529
1530 fn intrinsic_size(&self) -> ContentSize {
1531 ContentSize {
1532 width: 120.0,
1533 height: 28.0,
1534 }
1535 }
1536
1537 fn on_pointer_event(&self, event: PointerEvent) -> bool {
1538 self.pointer_events.lock().unwrap().push(event);
1539 true
1540 }
1541
1542 fn popup(
1543 &self,
1544 _text_style: &TextStyle,
1545 _text_flow_style: &TextFlowStyle,
1546 ) -> Option<Popup> {
1547 self.popup_open.load(Ordering::Relaxed).then(|| Popup {
1548 rect: self.popup_rect,
1549 commands: Vec::new(),
1550 })
1551 }
1552
1553 fn on_popup_pointer_event(&self, event: PointerEvent) -> bool {
1554 self.popup_events.lock().unwrap().push(event);
1555 true
1556 }
1557
1558 fn dismiss_popup(&self) {
1559 self.dismissed.store(true, Ordering::Relaxed);
1560 }
1561 }
1562
1563 fn make_tree(a_scroll_y: f32) -> (LayoutNode, InfoNode, Arc<RecordingNode>) {
1566 let node: Arc<RecordingNode> = Arc::new(RecordingNode::new(Rect {
1567 x: 0.0,
1568 y: 28.0,
1569 width: 120.0,
1570 height: 84.0,
1571 }));
1572 let custom_info = input_info(Arc::clone(&node) as Arc<dyn CustomNode>);
1573
1574 let mut a_layout = LayoutNode::new(ui_layout::Style::default());
1575 a_layout.layout_box =
1576 ui_layout::LayoutBox::BlockBox(box_model(10.0, 20.0, 160.0, 100.0, 160.0, 300.0));
1577 let mut custom_layout = LayoutNode::new(ui_layout::Style::default());
1578 custom_layout.layout_box =
1579 ui_layout::LayoutBox::BlockBox(box_model(5.0, 0.0, 120.0, 28.0, 120.0, 28.0));
1580 a_layout.children = vec![LayoutChild::Node(Box::new(custom_layout))];
1581
1582 let mut root_layout = LayoutNode::with_children(ui_layout::Style::default(), [a_layout]);
1583 root_layout.layout_box =
1584 ui_layout::LayoutBox::BlockBox(box_model(0.0, 0.0, 200.0, 200.0, 200.0, 200.0));
1585
1586 let a_info = InfoNode {
1587 kind: NodeKind::Container {
1588 scroll_x: false,
1589 scroll_y: true,
1590 scroll_offset_x: 0.0,
1591 scroll_offset_y: a_scroll_y,
1592 style: ContainerStyle::default(),
1593 role: ContainerRole::Normal,
1594 },
1595 children: vec![custom_info],
1596 dom_id: None,
1597 };
1598 let root_info = InfoNode {
1599 kind: NodeKind::Container {
1600 scroll_x: false,
1601 scroll_y: false,
1602 scroll_offset_x: 0.0,
1603 scroll_offset_y: 0.0,
1604 style: ContainerStyle::default(),
1605 role: ContainerRole::Normal,
1606 },
1607 children: vec![a_info],
1608 dom_id: None,
1609 };
1610
1611 (root_layout, root_info, node)
1612 }
1613
1614 fn build_path<'a>(root_layout: &'a LayoutNode, root_info: &'a InfoNode) -> HitPath<'a> {
1616 let a_layout = match &root_layout.children[0] {
1617 LayoutChild::Node(node) => node,
1618 _ => unreachable!("expected container child"),
1619 };
1620 let custom_layout = match &a_layout.children[0] {
1621 LayoutChild::Node(node) => node,
1622 _ => unreachable!("expected custom child"),
1623 };
1624 let a_info = &root_info.children[0];
1625 let custom_info = &a_info.children[0];
1626 vec![
1627 HitItem {
1628 layout: custom_layout,
1629 info: custom_info,
1630 },
1631 HitItem {
1632 layout: a_layout,
1633 info: a_info,
1634 },
1635 HitItem {
1636 layout: root_layout,
1637 info: root_info,
1638 },
1639 ]
1640 }
1641
1642 #[test]
1643 fn dispatch_pointer_uses_node_local_coords() {
1644 let (root_layout, root_info, node) = make_tree(0.0);
1645 node.popup_open.store(false, Ordering::Relaxed);
1646 let path = build_path(&root_layout, &root_info);
1647
1648 dispatch_pointer(&path, PointerEvent::Down { x: 20.0, y: 25.0 });
1650 assert_eq!(node.events(), vec![PointerEvent::Down { x: 5.0, y: 5.0 }]);
1651 }
1652
1653 #[test]
1654 fn dispatch_pointer_folds_ancestor_scroll_into_local_coords() {
1655 let (root_layout, root_info, node) = make_tree(50.0);
1656 node.popup_open.store(false, Ordering::Relaxed);
1657 let path = build_path(&root_layout, &root_info);
1658
1659 dispatch_pointer(&path, PointerEvent::Move { x: 20.0, y: 25.0 });
1661 assert_eq!(node.events(), vec![PointerEvent::Move { x: 5.0, y: 55.0 }]);
1662 }
1663
1664 #[test]
1665 fn popup_events_use_popup_local_coords() {
1666 let (root_layout, root_info, node) = make_tree(0.0);
1667 let path = build_path(&root_layout, &root_info);
1668
1669 dispatch_pointer(&path, PointerEvent::Down { x: 20.0, y: 60.0 });
1672 assert_eq!(
1673 node.popup_events(),
1674 vec![PointerEvent::Down { x: 5.0, y: 12.0 }]
1675 );
1676 assert!(node.events().is_empty());
1677 assert!(!node.dismissed.load(Ordering::Relaxed));
1678 }
1679
1680 #[test]
1681 fn down_outside_popup_routes_to_node() {
1682 let (root_layout, root_info, node) = make_tree(0.0);
1683 let path = build_path(&root_layout, &root_info);
1684
1685 dispatch_pointer(&path, PointerEvent::Down { x: 20.0, y: 30.0 });
1688 assert!(!node.dismissed.load(Ordering::Relaxed));
1689 assert_eq!(node.events(), vec![PointerEvent::Down { x: 5.0, y: 10.0 }]);
1690 assert!(node.popup_events().is_empty());
1691 }
1692
1693 #[test]
1694 fn move_outside_popup_routes_to_node() {
1695 let (root_layout, root_info, node) = make_tree(0.0);
1696 let path = build_path(&root_layout, &root_info);
1697
1698 dispatch_pointer(&path, PointerEvent::Move { x: 20.0, y: 30.0 });
1701 assert!(!node.dismissed.load(Ordering::Relaxed));
1702 assert_eq!(node.events(), vec![PointerEvent::Move { x: 5.0, y: 10.0 }]);
1703 assert!(node.popup_events().is_empty());
1704 }
1705
1706 fn popup_hit_assert(
1707 root_layout: &LayoutNode,
1708 root_info: &InfoNode,
1709 node: &Arc<RecordingNode>,
1710 x: f32,
1711 y: f32,
1712 ) {
1713 let path = hit_test(root_layout, root_info, x, y);
1714 let hit = hit_custom_node(&path).unwrap();
1715 assert!(std::ptr::eq(hit.as_ref(), node.as_ref() as &dyn CustomNode));
1716 }
1717
1718 #[test]
1719 fn hit_test_finds_open_popup() {
1720 let (root_layout, root_info, node) = make_tree(0.0);
1721 popup_hit_assert(&root_layout, &root_info, &node, 20.0, 60.0);
1724 }
1725
1726 #[test]
1727 fn hit_test_ignores_closed_popup() {
1728 let (root_layout, root_info, node) = make_tree(0.0);
1729 node.popup_open.store(false, Ordering::Relaxed);
1730 let path = hit_test(&root_layout, &root_info, 20.0, 60.0);
1733 assert_eq!(path.len(), 2);
1734 assert!(hit_custom_node(&path).is_none());
1735 }
1736
1737 #[test]
1738 fn hit_test_finds_popup_escaping_ancestor_box() {
1739 let (root_layout, root_info, node) = make_tree(0.0);
1740 popup_hit_assert(&root_layout, &root_info, &node, 20.0, 125.0);
1744 }
1745
1746 #[test]
1747 fn hit_test_folds_scroll_into_popup_hit() {
1748 let (root_layout, root_info, node) = make_tree(50.0);
1749 popup_hit_assert(&root_layout, &root_info, &node, 20.0, 25.0);
1752 }
1753
1754 #[test]
1755 fn dismiss_open_popups_closes_popup_on_outside_press() {
1756 let (root_layout, root_info, node) = make_tree(0.0);
1757 let path = hit_test(&root_layout, &root_info, 10.0, 25.0);
1759 assert!(dismiss_open_popups(&root_info, &path, 50.0, 50.0));
1760 assert!(node.dismissed.load(Ordering::Relaxed));
1761 }
1762
1763 #[test]
1764 fn dismiss_open_popups_keeps_popup_under_press() {
1765 let (root_layout, root_info, node) = make_tree(0.0);
1766 let path = hit_test(&root_layout, &root_info, 20.0, 60.0);
1768 assert!(!dismiss_open_popups(&root_info, &path, 50.0, 50.0));
1769 assert!(!node.dismissed.load(Ordering::Relaxed));
1770 }
1771
1772 #[test]
1773 fn dismiss_open_popups_keeps_popup_when_press_on_owner_box() {
1774 let (root_layout, root_info, node) = make_tree(0.0);
1775 let path = hit_test(&root_layout, &root_info, 20.0, 30.0);
1778 assert!(!dismiss_open_popups(&root_info, &path, 50.0, 50.0));
1779 assert!(!node.dismissed.load(Ordering::Relaxed));
1780 }
1781
1782 #[test]
1783 fn dismiss_open_popups_ignores_closed_popup() {
1784 let (root_layout, root_info, node) = make_tree(0.0);
1785 node.popup_open.store(false, Ordering::Relaxed);
1786 let path = hit_test(&root_layout, &root_info, 10.0, 25.0);
1787 assert!(!dismiss_open_popups(&root_info, &path, 50.0, 50.0));
1788 assert!(!node.dismissed.load(Ordering::Relaxed));
1789 }
1790
1791 fn make_inline_tree() -> (LayoutNode, InfoNode) {
1795 let mut c = LayoutNode::new(ui_layout::Style::default());
1796 c.layout_box =
1797 ui_layout::LayoutBox::BlockBox(box_model(30.0, 10.0, 50.0, 20.0, 50.0, 20.0));
1798 let mut b =
1799 LayoutNode::with_children(ui_layout::Style::default(), [inline_node(vec![c.into()])]);
1800 b.layout_box =
1801 ui_layout::LayoutBox::BlockBox(box_model(0.0, 0.0, 300.0, 100.0, 300.0, 100.0));
1802
1803 let c_info = container_info(false, Some(7));
1804 let mut i_info = container_info(false, None);
1805 i_info.children.push(c_info);
1806 let mut b_info = container_info(false, None);
1807 b_info.children.push(i_info);
1808
1809 (b, b_info)
1810 }
1811
1812 #[test]
1813 fn hit_test_inside_inline_keeps_parent_coords() {
1814 let (b, b_info) = make_inline_tree();
1815 let path = hit_test(&b, &b_info, 40.0, 15.0);
1819 assert_eq!(hit_dom_id(&path), Some(7));
1820 assert_eq!(path.len(), 3);
1821 }
1822
1823 #[test]
1824 fn hit_test_inside_inline_but_outside_child_hits_inline() {
1825 let (b, b_info) = make_inline_tree();
1826 let path = hit_test(&b, &b_info, 12.0, 12.0);
1829 assert_eq!(path.len(), 2);
1830 assert_eq!(hit_dom_id(&path), None);
1831 }
1832
1833 #[test]
1834 fn hit_test_outside_inline_line_boxes_falls_back_to_parent() {
1835 let (b, b_info) = make_inline_tree();
1836 let path = hit_test(&b, &b_info, 40.0, 50.0);
1838 assert_eq!(path.len(), 1);
1839 assert_eq!(hit_dom_id(&path), None);
1840 }
1841
1842 #[test]
1843 fn dispatch_pointer_to_inline_custom_keeps_parent_content_coords() {
1844 let node: Arc<RecordingNode> = Arc::new(RecordingNode::new(Rect {
1845 x: 0.0,
1846 y: 28.0,
1847 width: 120.0,
1848 height: 84.0,
1849 }));
1850 let custom_info = input_info(Arc::clone(&node) as Arc<dyn CustomNode>);
1851
1852 let mut inline_layout = LayoutNode::new(ui_layout::Style::default());
1853 inline_layout.layout_box = ui_layout::LayoutBox::InlineBox(ui_layout::InlineBox {
1854 box_model: box_model(10.0, 20.0, 160.0, 100.0, 160.0, 100.0),
1855 line_spans: vec![ui_layout::LineSpan {
1856 x_range: 0.0..160.0,
1857 line_pos: (10.0, 20.0),
1858 line_index: 0,
1859 }],
1860 });
1861 let mut block_layout = LayoutNode::new(ui_layout::Style::default());
1862 block_layout.layout_box =
1863 ui_layout::LayoutBox::BlockBox(box_model(0.0, 0.0, 200.0, 200.0, 200.0, 200.0));
1864
1865 let inline_info = container_info(false, None);
1866 let root_info = container_info(false, None);
1867
1868 node.popup_open.store(false, Ordering::Relaxed);
1869 let path = vec![
1870 HitItem {
1871 layout: &inline_layout,
1872 info: &custom_info,
1873 },
1874 HitItem {
1875 layout: &inline_layout,
1876 info: &inline_info,
1877 },
1878 HitItem {
1879 layout: &block_layout,
1880 info: &root_info,
1881 },
1882 ];
1883
1884 dispatch_pointer(&path, PointerEvent::Down { x: 30.0, y: 35.0 });
1888 assert_eq!(node.events(), vec![PointerEvent::Down { x: 30.0, y: 35.0 }]);
1889 }
1890
1891 #[test]
1892 fn scroll_at_inside_inline_scrolls_child_in_parent_coords() {
1893 let mut c = LayoutNode::new(ui_layout::Style::default());
1894 c.layout_box =
1895 ui_layout::LayoutBox::BlockBox(box_model(30.0, 10.0, 50.0, 80.0, 50.0, 240.0));
1896 let mut b =
1897 LayoutNode::with_children(ui_layout::Style::default(), [inline_node(vec![c.into()])]);
1898 b.layout_box =
1899 ui_layout::LayoutBox::BlockBox(box_model(0.0, 0.0, 200.0, 100.0, 200.0, 300.0));
1900
1901 let c_info = container_info(true, None);
1902 let mut i_info = container_info(false, None);
1903 i_info.children.push(c_info);
1904 let mut b_info = scrollable_info();
1905 b_info.children.push(i_info);
1906
1907 assert!(
1908 scroll_at(
1909 &b,
1910 &mut b_info,
1911 (VIEWPORT_WIDTH, VIEWPORT_HEIGHT),
1912 40.0,
1913 15.0,
1914 0.0,
1915 10.0
1916 )
1917 .is_some()
1918 );
1919 assert_eq!(scroll_offset_y_of(&b_info.children[0].children[0]), 10.0);
1921 assert_eq!(scroll_offset_y_of(&b_info), 0.0);
1922 }
1923
1924 fn sticky_box_model(
1925 x: f32,
1926 y: f32,
1927 width: f32,
1928 height: f32,
1929 children_width: f32,
1930 children_height: f32,
1931 edges: ui_layout::EdgeOption,
1932 ) -> ui_layout::BoxModel {
1933 let mut bm = box_model(x, y, width, height, children_width, children_height);
1934 bm.sticky_edges = Some(edges);
1935 bm
1936 }
1937
1938 #[test]
1939 fn hit_test_sticky_element_uses_visual_position() {
1940 let mut container_layout = LayoutNode::with_children(
1945 ui_layout::Style::default(),
1946 [LayoutNode::new({
1947 let mut s = ui_layout::Style::default();
1948 s.position.kind = Position::Sticky;
1949 s
1950 })],
1951 );
1952 container_layout.layout_box =
1953 ui_layout::LayoutBox::BlockBox(box_model(0.0, 0.0, 200.0, 150.0, 200.0, 400.0));
1954 let mut child_layout = LayoutNode::new({
1955 let mut s = ui_layout::Style::default();
1956 s.position.kind = Position::Sticky;
1957 s
1958 });
1959 child_layout.layout_box = ui_layout::LayoutBox::BlockBox(sticky_box_model(
1960 10.0,
1961 80.0,
1962 40.0,
1963 30.0,
1964 40.0,
1965 30.0,
1966 ui_layout::EdgeOption {
1967 top: Some(0.0),
1968 right: None,
1969 bottom: None,
1970 left: None,
1971 },
1972 ));
1973 container_layout.children[0] = ui_layout::LayoutChild::Node(Box::new(child_layout));
1974
1975 let mut root_layout =
1976 LayoutNode::with_children(ui_layout::Style::default(), [container_layout]);
1977 root_layout.layout_box =
1978 ui_layout::LayoutBox::BlockBox(box_model(0.0, 0.0, 200.0, 150.0, 200.0, 150.0));
1979
1980 let mut scroll_container_info = scrollable_info();
1981 set_vertical_scroll(&mut scroll_container_info, 100.0);
1982 let mut child_info = container_info_child(Some(77));
1983 if let NodeKind::Container { scroll_y, .. } = &mut child_info.kind {
1984 *scroll_y = false;
1985 }
1986 scroll_container_info.children = vec![child_info];
1987 let mut root_info = container_info(false, None);
1988 root_info.children = vec![scroll_container_info];
1989
1990 let natural = hit_test(&root_layout, &root_info, 20.0, 90.0);
1992 assert_ne!(hit_dom_id(&natural), Some(77));
1993 let visual = hit_test(&root_layout, &root_info, 20.0, 15.0);
1995 assert_eq!(hit_dom_id(&visual), Some(77));
1996 }
1997
1998 fn container_info_child(dom_id: Option<u32>) -> InfoNode {
1999 InfoNode {
2000 kind: NodeKind::Container {
2001 scroll_x: false,
2002 scroll_y: false,
2003 scroll_offset_x: 0.0,
2004 scroll_offset_y: 0.0,
2005 style: ContainerStyle::default(),
2006 role: ContainerRole::Normal,
2007 },
2008 children: Vec::new(),
2009 dom_id,
2010 }
2011 }
2012
2013 #[test]
2014 fn scroll_at_sticky_element_uses_visual_position() {
2015 let mut container_layout = LayoutNode::with_children(
2016 ui_layout::Style::default(),
2017 [LayoutNode::new({
2018 let mut s = ui_layout::Style::default();
2019 s.position.kind = Position::Sticky;
2020 s
2021 })],
2022 );
2023 container_layout.layout_box =
2024 ui_layout::LayoutBox::BlockBox(box_model(0.0, 0.0, 200.0, 150.0, 200.0, 400.0));
2025 let mut child_layout = LayoutNode::new({
2026 let mut s = ui_layout::Style::default();
2027 s.position.kind = Position::Sticky;
2028 s
2029 });
2030 child_layout.layout_box = ui_layout::LayoutBox::BlockBox(sticky_box_model(
2031 10.0,
2032 80.0,
2033 40.0,
2034 30.0,
2035 40.0,
2036 120.0,
2037 ui_layout::EdgeOption {
2038 top: Some(0.0),
2039 right: None,
2040 bottom: None,
2041 left: None,
2042 },
2043 ));
2044 container_layout.children[0] = ui_layout::LayoutChild::Node(Box::new(child_layout));
2045
2046 let mut root_layout =
2047 LayoutNode::with_children(ui_layout::Style::default(), [container_layout]);
2048 root_layout.layout_box =
2049 ui_layout::LayoutBox::BlockBox(box_model(0.0, 0.0, 200.0, 150.0, 200.0, 150.0));
2050
2051 let mut scroll_container_info = scrollable_info();
2052 set_vertical_scroll(&mut scroll_container_info, 100.0);
2053 let child_info = container_info(true, Some(77));
2054 scroll_container_info.children = vec![child_info];
2055 let mut root_info = container_info(false, None);
2056 root_info.children = vec![scroll_container_info];
2057
2058 let scrolled = scroll_at(
2060 &root_layout,
2061 &mut root_info,
2062 (VIEWPORT_WIDTH, VIEWPORT_HEIGHT),
2063 20.0,
2064 15.0,
2065 0.0,
2066 10.0,
2067 );
2068 assert_eq!(scrolled, Some(77));
2069 assert_eq!(scroll_offset_y_of(&root_info.children[0].children[0]), 10.0);
2070 assert_eq!(scroll_offset_y_of(&root_info.children[0]), 100.0);
2071 }
2072
2073 fn transformed_container_info(
2074 transform: crate::engine::renderer_model::AffineTransform,
2075 ) -> InfoNode {
2076 InfoNode {
2077 kind: NodeKind::Container {
2078 scroll_x: false,
2079 scroll_y: true,
2080 scroll_offset_x: 0.0,
2081 scroll_offset_y: 0.0,
2082 style: ContainerStyle {
2083 transform: Some(transform),
2084 ..ContainerStyle::default()
2085 },
2086 role: ContainerRole::Normal,
2087 },
2088 children: Vec::new(),
2089 dom_id: None,
2090 }
2091 }
2092
2093 #[test]
2094 fn hit_test_and_scroll_translated_element_use_visual_position() {
2095 let mut root_layout = LayoutNode::with_children(
2097 ui_layout::Style::default(),
2098 [LayoutNode::new(ui_layout::Style::default())],
2099 );
2100 root_layout.layout_box =
2101 ui_layout::LayoutBox::BlockBox(box_model(0.0, 0.0, 300.0, 200.0, 300.0, 200.0));
2102 let mut child_layout = LayoutNode::new(ui_layout::Style::default());
2103 child_layout.layout_box =
2104 ui_layout::LayoutBox::BlockBox(box_model(50.0, 50.0, 40.0, 30.0, 40.0, 120.0));
2105 root_layout.children[0] = ui_layout::LayoutChild::Node(Box::new(child_layout));
2106
2107 let mut root_info = container_info(false, None);
2108 let mut child_info = transformed_container_info(
2109 crate::engine::renderer_model::AffineTransform::translate(60.0, 0.0),
2110 );
2111 child_info.dom_id = Some(88);
2112 root_info.children.push(child_info);
2113
2114 assert_ne!(
2116 hit_dom_id(&hit_test(&root_layout, &root_info, 60.0, 60.0)),
2117 Some(88)
2118 );
2119 assert_eq!(
2121 hit_dom_id(&hit_test(&root_layout, &root_info, 120.0, 60.0)),
2122 Some(88)
2123 );
2124
2125 let scrolled = scroll_at(
2126 &root_layout,
2127 &mut root_info,
2128 (VIEWPORT_WIDTH, VIEWPORT_HEIGHT),
2129 120.0,
2130 60.0,
2131 0.0,
2132 10.0,
2133 );
2134 assert_eq!(scrolled, Some(88));
2135 assert_eq!(scroll_offset_y_of(&root_info.children[0]), 10.0);
2136 }
2137
2138 #[test]
2139 fn dispatch_pointer_to_translated_custom_uses_visual_inverse() {
2140 let node: Arc<RecordingNode> = Arc::new(RecordingNode::new(Rect {
2141 x: 0.0,
2142 y: 0.0,
2143 width: 0.0,
2144 height: 0.0,
2145 }));
2146 node.popup_open.store(false, Ordering::Relaxed);
2147 let mut custom_info = input_info(Arc::clone(&node) as Arc<dyn CustomNode>);
2148 if let NodeKind::Custom { style, .. } = &mut custom_info.kind {
2149 style.transform = Some(crate::engine::renderer_model::AffineTransform::translate(
2150 60.0, 0.0,
2151 ));
2152 }
2153
2154 let mut root_layout = LayoutNode::with_children(
2155 ui_layout::Style::default(),
2156 [LayoutNode::new(ui_layout::Style::default())],
2157 );
2158 root_layout.layout_box =
2159 ui_layout::LayoutBox::BlockBox(box_model(0.0, 0.0, 300.0, 200.0, 300.0, 200.0));
2160 let mut child_layout = LayoutNode::new(ui_layout::Style::default());
2161 child_layout.layout_box =
2162 ui_layout::LayoutBox::BlockBox(box_model(50.0, 50.0, 40.0, 30.0, 40.0, 30.0));
2163 root_layout.children[0] = ui_layout::LayoutChild::Node(Box::new(child_layout));
2164 let mut root_info = container_info(false, None);
2165 root_info.children.push(custom_info);
2166
2167 let path = hit_test(&root_layout, &root_info, 120.0, 60.0);
2168 assert!(hit_custom_node(&path).is_some());
2169 dispatch_pointer(&path, PointerEvent::Down { x: 120.0, y: 60.0 });
2170 assert_eq!(node.events(), vec![PointerEvent::Down { x: 10.0, y: 10.0 }]);
2172 }
2173
2174 #[test]
2175 fn hit_test_scaled_element_uses_visual_inverse() {
2176 let mut root_layout = LayoutNode::with_children(
2178 ui_layout::Style::default(),
2179 [LayoutNode::new(ui_layout::Style::default())],
2180 );
2181 root_layout.layout_box =
2182 ui_layout::LayoutBox::BlockBox(box_model(0.0, 0.0, 300.0, 300.0, 300.0, 300.0));
2183 let mut child_layout = LayoutNode::new(ui_layout::Style::default());
2184 child_layout.layout_box =
2185 ui_layout::LayoutBox::BlockBox(box_model(100.0, 100.0, 40.0, 40.0, 40.0, 40.0));
2186 root_layout.children[0] = ui_layout::LayoutChild::Node(Box::new(child_layout));
2187
2188 let mut root_info = container_info(false, None);
2189 let mut child_info = transformed_container_info(
2190 crate::engine::renderer_model::AffineTransform::scale(2.0, 2.0),
2191 );
2192 child_info.dom_id = Some(99);
2193 if let NodeKind::Container { scroll_y, .. } = &mut child_info.kind {
2195 *scroll_y = false;
2196 }
2197 root_info.children.push(child_info);
2198
2199 assert_eq!(
2201 hit_dom_id(&hit_test(&root_layout, &root_info, 90.0, 120.0)),
2202 Some(99)
2203 );
2204 assert_ne!(
2206 hit_dom_id(&hit_test(&root_layout, &root_info, 170.0, 120.0)),
2207 Some(99)
2208 );
2209 }
2210
2211 #[test]
2212 fn popup_hit_and_dispatch_on_translated_custom_use_visual_coords() {
2213 let node: Arc<RecordingNode> = Arc::new(RecordingNode::new(Rect {
2216 x: 0.0,
2217 y: 30.0,
2218 width: 40.0,
2219 height: 20.0,
2220 }));
2221 let mut custom_info = input_info(Arc::clone(&node) as Arc<dyn CustomNode>);
2222 if let NodeKind::Custom { style, .. } = &mut custom_info.kind {
2223 style.transform = Some(crate::engine::renderer_model::AffineTransform::translate(
2224 60.0, 0.0,
2225 ));
2226 }
2227
2228 let mut root_layout = LayoutNode::with_children(
2229 ui_layout::Style::default(),
2230 [LayoutNode::new(ui_layout::Style::default())],
2231 );
2232 root_layout.layout_box =
2233 ui_layout::LayoutBox::BlockBox(box_model(0.0, 0.0, 300.0, 200.0, 300.0, 200.0));
2234 let mut child_layout = LayoutNode::new(ui_layout::Style::default());
2235 child_layout.layout_box =
2236 ui_layout::LayoutBox::BlockBox(box_model(50.0, 50.0, 40.0, 30.0, 40.0, 30.0));
2237 root_layout.children[0] = ui_layout::LayoutChild::Node(Box::new(child_layout));
2238 let mut root_info = container_info(false, None);
2239 root_info.children.push(custom_info);
2240
2241 let path = hit_test(&root_layout, &root_info, 120.0, 90.0);
2244 assert!(hit_custom_node(&path).is_some());
2245 dispatch_pointer(&path, PointerEvent::Down { x: 120.0, y: 90.0 });
2246 assert_eq!(
2247 node.popup_events(),
2248 vec![PointerEvent::Down { x: 10.0, y: 10.0 }]
2249 );
2250 assert!(node.events().is_empty());
2251 }
2252
2253 #[test]
2254 fn popup_hit_and_dispatch_on_inline_custom_node() {
2255 let node: Arc<RecordingNode> = Arc::new(RecordingNode::new(Rect {
2256 x: 0.0,
2257 y: 30.0,
2258 width: 100.0,
2259 height: 60.0,
2260 }));
2261 let custom_info = input_info(Arc::clone(&node) as Arc<dyn CustomNode>);
2262
2263 let mut root_layout = LayoutNode::with_children(
2264 ui_layout::Style::default(),
2265 [LayoutNode::new(ui_layout::Style::default())],
2266 );
2267 root_layout.layout_box =
2268 ui_layout::LayoutBox::BlockBox(box_model(0.0, 0.0, 400.0, 400.0, 400.0, 400.0));
2269
2270 let mut inline_custom_layout = LayoutNode::new(ui_layout::Style::default());
2271 inline_custom_layout.layout_box = ui_layout::LayoutBox::InlineBox(ui_layout::InlineBox {
2272 box_model: box_model(40.0, 50.0, 100.0, 30.0, 100.0, 30.0),
2273 line_spans: vec![ui_layout::LineSpan {
2274 x_range: 40.0..140.0,
2275 line_pos: (40.0, 50.0),
2276 line_index: 0,
2277 }],
2278 });
2279 root_layout.children[0] = ui_layout::LayoutChild::Node(Box::new(inline_custom_layout));
2280
2281 let mut root_info = container_info(false, None);
2282 root_info.children.push(custom_info);
2283
2284 let path = hit_test(&root_layout, &root_info, 60.0, 95.0);
2288 assert!(hit_custom_node(&path).is_some());
2289 assert!(!dismiss_open_popups(&root_info, &path, 60.0, 95.0));
2290 dispatch_pointer(&path, PointerEvent::Down { x: 60.0, y: 95.0 });
2291 assert_eq!(
2292 node.popup_events(),
2293 vec![PointerEvent::Down { x: 20.0, y: 15.0 }]
2294 );
2295 assert!(node.events().is_empty());
2296 }
2297
2298 #[test]
2299 fn popup_escaping_ancestor_box_receives_dispatch() {
2300 let (root_layout, root_info, node) = make_tree(0.0);
2301 let path = hit_test(&root_layout, &root_info, 20.0, 125.0);
2304 assert!(hit_custom_node(&path).is_some());
2305 assert!(!dismiss_open_popups(&root_info, &path, 20.0, 125.0));
2306
2307 dispatch_pointer(&path, PointerEvent::Down { x: 20.0, y: 125.0 });
2310 assert_eq!(
2311 node.popup_events(),
2312 vec![PointerEvent::Down { x: 5.0, y: 77.0 }]
2313 );
2314 assert!(node.events().is_empty());
2315 }
2316}