1use std::sync::Arc;
4
5use crate::engine::layouter::types::TextFlowStyle;
6
7use super::layouter::types::{InfoNode, NodeKind, TextStyle};
8use super::ui::PointerEvent;
9use super::ui::custom_node::CustomNode;
10use super::ui::input_text_types::InputTextEvent;
11use ui_layout::{LayoutNode, Position};
12#[derive(Clone, Debug)]
14pub struct HitItem<'a> {
15 pub layout: &'a LayoutNode,
16 pub info: &'a InfoNode,
17}
18
19pub type HitPath<'a> = Vec<HitItem<'a>>;
21
22pub fn hit_custom_node<'a>(path: &'a HitPath<'a>) -> Option<&'a Arc<dyn CustomNode>> {
27 path.iter().find_map(|hit| match &hit.info.kind {
28 NodeKind::Custom { node, .. } => Some(node),
29 _ => None,
30 })
31}
32
33pub fn hit_dom_id(path: &HitPath<'_>) -> Option<u32> {
38 path.iter().find_map(|hit| hit.info.dom_id)
39}
40
41fn local_pointer_coords(path: &HitPath<'_>, target: usize, x: f32, y: f32) -> (f32, f32) {
54 let mut lx = x;
55 let mut ly = y;
56 for (offset, hit) in path.iter().rev().take(path.len() - target).enumerate() {
57 let index = path.len() - 1 - offset;
58 let shares_parent_layout =
59 index + 1 < path.len() && std::ptr::eq(hit.layout, path[index + 1].layout);
60 let is_inline_container =
65 matches!(hit.layout.layout_box, ui_layout::LayoutBox::InlineBox(_))
66 && matches!(&hit.info.kind, NodeKind::Container { .. });
67 if shares_parent_layout || is_inline_container {
68 continue;
69 }
70 let (cx, cy) = hit
71 .layout
72 .layout_box
73 .iter()
74 .next()
75 .map_or((0.0, 0.0), |b| (b.content_box.x, b.content_box.y));
76 let (sx, sy) = hit.info.kind.scroll_offsets();
77 lx += sx - cx;
78 ly += sy - cy;
79 }
80 (lx, ly)
81}
82
83pub fn dispatch_pointer(path: &HitPath<'_>, event: PointerEvent) -> bool {
88 for (target, hit) in path.iter().enumerate() {
89 if let NodeKind::Custom {
90 node,
91 text_style,
92 text_flow_style,
93 ..
94 } = &hit.info.kind
95 {
96 let (px, py) = match event {
97 PointerEvent::Move { x, y } => (x, y),
98 PointerEvent::Down { x, y } => (x, y),
99 PointerEvent::Up { x, y } => (x, y),
100 PointerEvent::Leave => (0.0, 0.0), };
102 let (lx, ly) = local_pointer_coords(path, target, px, py);
103 let local_event = match event {
104 PointerEvent::Move { .. } => PointerEvent::Move { x: lx, y: ly },
105 PointerEvent::Down { .. } => PointerEvent::Down { x: lx, y: ly },
106 PointerEvent::Up { .. } => PointerEvent::Up { x: lx, y: ly },
107 PointerEvent::Leave => PointerEvent::Leave,
108 };
109
110 if let Some(popup) = node.popup(text_style, text_flow_style) {
113 let in_popup = lx >= popup.rect.x
114 && ly >= popup.rect.y
115 && lx <= popup.rect.x + popup.rect.width
116 && ly <= popup.rect.y + popup.rect.height;
117 let popup_event = match local_event {
120 PointerEvent::Move { x, y } => PointerEvent::Move {
121 x: x - popup.rect.x,
122 y: y - popup.rect.y,
123 },
124 PointerEvent::Down { x, y } => PointerEvent::Down {
125 x: x - popup.rect.x,
126 y: y - popup.rect.y,
127 },
128 PointerEvent::Up { x, y } => PointerEvent::Up {
129 x: x - popup.rect.x,
130 y: y - popup.rect.y,
131 },
132 PointerEvent::Leave => PointerEvent::Leave,
133 };
134
135 if in_popup {
136 return node.on_popup_pointer_event(popup_event);
137 }
138 }
139 return node.on_pointer_event(local_event);
140 }
141 }
142 false
143}
144
145pub fn dismiss_open_popups(info: &InfoNode, path: &HitPath<'_>) -> bool {
152 let mut dismissed = false;
153 dismiss_open_popups_inner(info, path, &mut dismissed);
154 dismissed
155}
156
157fn dismiss_open_popups_inner(info: &InfoNode, path: &HitPath<'_>, dismissed: &mut bool) {
158 if let NodeKind::Custom { node, .. } = &info.kind
159 && node.popup(&TextStyle::default(), &TextFlowStyle::default()).is_some()
160 && !path.iter().any(|hit| {
161 matches!(&hit.info.kind, NodeKind::Custom { node: owner, .. } if Arc::ptr_eq(node, owner))
162 })
163 {
164 node.dismiss_popup();
165 *dismissed = true;
166 }
167 for child in &info.children {
168 dismiss_open_popups_inner(child, path, dismissed);
169 }
170}
171
172pub fn update_hover(path: &HitPath<'_>, previous: Option<&Arc<dyn CustomNode>>) -> bool {
177 let current = hit_custom_node(path);
178 match (previous, current) {
179 (Some(prev), Some(curr)) if Arc::ptr_eq(prev, curr) => false,
180 (Some(prev), _) => {
181 prev.set_hovered(false);
182 if let Some(curr) = current {
183 curr.set_hovered(true);
184 }
185 true
186 }
187 (None, Some(curr)) => {
188 curr.set_hovered(true);
189 true
190 }
191 (None, None) => false,
192 }
193}
194
195pub fn hit_test<'a>(layout: &'a LayoutNode, info: &'a InfoNode, x: f32, y: f32) -> HitPath<'a> {
196 if let Some(path) = hit_test_popup(layout, info, x, y) {
200 return path;
201 }
202 hit_test_inner(layout, info, x, y, (0.0, 0.0))
203}
204
205fn hit_test_inner<'a>(
206 layout: &'a LayoutNode,
207 info: &'a InfoNode,
208 mut x: f32,
209 mut y: f32,
210 accumulated_scroll: (f32, f32),
211) -> HitPath<'a> {
212 if layout.layout_box.is_empty() {
214 return Vec::new();
215 }
216
217 let is_inline = matches!(layout.layout_box, ui_layout::LayoutBox::InlineBox(_));
218
219 let is_fixed = layout.style.position.kind == Position::Fixed;
220 if is_fixed {
221 x -= accumulated_scroll.0;
222 y -= accumulated_scroll.1;
223 }
224
225 let own_scroll = info.kind.scroll_offsets();
226 let child_scroll = if is_fixed {
227 own_scroll
228 } else {
229 (
230 accumulated_scroll.0 + own_scroll.0,
231 accumulated_scroll.1 + own_scroll.1,
232 )
233 };
234
235 for box_model in layout
236 .layout_box
237 .iter()
238 .collect::<Vec<_>>()
239 .into_iter()
240 .rev()
241 {
242 let rect = box_model.padding_box;
244
245 if x < rect.x || y < rect.y || x > rect.x + rect.width || y > rect.y + rect.height {
247 continue;
248 }
249
250 let is_inline_container = is_inline && matches!(&info.kind, NodeKind::Container { .. });
256 let (local_x, local_y) = if is_inline_container {
257 (x, y)
258 } else {
259 (
260 x - box_model.content_box.x + own_scroll.0,
261 y - box_model.content_box.y + own_scroll.1,
262 )
263 };
264
265 for (child_layout, child_info) in layout.children.iter().zip(&info.children).rev() {
267 if let Some(child_node) = child_layout.node() {
268 let mut path =
269 hit_test_inner(child_node, child_info, local_x, local_y, child_scroll);
270 if !path.is_empty() {
271 path.push(HitItem { layout, info });
273 return path;
274 }
275 } else if let Some(result) = child_layout.custom_result()
276 && result.spans.iter().any(|span| {
277 local_x >= span.line_pos.0
281 && local_x <= span.line_pos.0 + span.width()
282 && local_y >= span.line_pos.1
283 && local_y <= span.line_pos.1 + result.box_model.content_box.height
284 })
285 {
286 return vec![
287 HitItem {
288 layout,
289 info: child_info,
290 },
291 HitItem { layout, info },
292 ];
293 }
294 }
295
296 return vec![HitItem { layout, info }];
298 }
299
300 Vec::new()
302}
303
304fn hit_test_popup<'a>(
313 layout: &'a LayoutNode,
314 info: &'a InfoNode,
315 x: f32,
316 y: f32,
317) -> Option<HitPath<'a>> {
318 let mut best: Option<HitPath<'a>> = None;
319 let mut prefix: HitPath<'a> = Vec::new();
320 hit_test_popup_inner(layout, info, x, y, (0.0, 0.0), &mut prefix, &mut best);
321 best
322}
323
324fn hit_test_popup_inner<'a>(
327 layout: &'a LayoutNode,
328 info: &'a InfoNode,
329 mut x: f32,
330 mut y: f32,
331 accumulated_scroll: (f32, f32),
332 prefix: &mut HitPath<'a>,
333 best: &mut Option<HitPath<'a>>,
334) {
335 if layout.layout_box.is_empty() {
336 return;
337 }
338
339 let is_inline = matches!(layout.layout_box, ui_layout::LayoutBox::InlineBox(_));
340
341 let is_fixed = layout.style.position.kind == Position::Fixed;
342 if is_fixed {
343 x -= accumulated_scroll.0;
344 y -= accumulated_scroll.1;
345 }
346
347 let own_scroll = info.kind.scroll_offsets();
348 let child_scroll = if is_fixed {
349 own_scroll
350 } else {
351 (
352 accumulated_scroll.0 + own_scroll.0,
353 accumulated_scroll.1 + own_scroll.1,
354 )
355 };
356
357 let is_inline_container = is_inline && matches!(&info.kind, NodeKind::Container { .. });
362 let (local_x, local_y) = if is_inline_container {
363 (x, y)
364 } else {
365 layout.layout_box.iter().next().map_or((x, y), |b| {
366 (
367 x - b.content_box.x + own_scroll.0,
368 y - b.content_box.y + own_scroll.1,
369 )
370 })
371 };
372
373 if let NodeKind::Custom {
374 node,
375 text_style,
376 text_flow_style,
377 ..
378 } = &info.kind
379 && let Some(popup) = node.popup(text_style, text_flow_style)
380 && local_x >= popup.rect.x
381 && local_y >= popup.rect.y
382 && local_x <= popup.rect.x + popup.rect.width
383 && local_y <= popup.rect.y + popup.rect.height
384 {
385 let mut path = prefix.clone();
386 path.push(HitItem { layout, info });
387 path.reverse();
388 *best = Some(path);
389 }
390
391 prefix.push(HitItem { layout, info });
392 for (child_layout, child_info) in layout.children.iter().zip(&info.children) {
393 if let Some(child_node) = child_layout.node() {
394 hit_test_popup_inner(
395 child_node,
396 child_info,
397 local_x,
398 local_y,
399 child_scroll,
400 prefix,
401 best,
402 );
403 }
404 }
405 prefix.pop();
406}
407
408pub const NO_SCROLL_DOM_ID: u32 = u32::MAX;
412
413pub fn scroll_at(
428 layout: &LayoutNode,
429 info: &mut InfoNode,
430 viewport: (f32, f32),
431 x: f32,
432 y: f32,
433 dx: f32,
434 dy: f32,
435) -> Option<u32> {
436 scroll_at_inner(layout, info, viewport, x, y, dx, dy, (0.0, 0.0))
437}
438
439#[allow(clippy::too_many_arguments)]
440fn scroll_at_inner(
441 layout: &LayoutNode,
442 info: &mut InfoNode,
443 viewport: (f32, f32),
444 mut x: f32,
445 mut y: f32,
446 dx: f32,
447 dy: f32,
448 accumulated_scroll: (f32, f32),
449) -> Option<u32> {
450 if layout.layout_box.is_empty() {
451 return None;
452 }
453
454 let is_inline = matches!(layout.layout_box, ui_layout::LayoutBox::InlineBox(_));
455
456 let is_fixed = layout.style.position.kind == Position::Fixed;
457 if is_fixed {
458 x -= accumulated_scroll.0;
459 y -= accumulated_scroll.1;
460 }
461
462 let own_scroll = info.kind.scroll_offsets();
463 let child_scroll = if is_fixed {
464 own_scroll
465 } else {
466 (
467 accumulated_scroll.0 + own_scroll.0,
468 accumulated_scroll.1 + own_scroll.1,
469 )
470 };
471
472 for box_model in layout
473 .layout_box
474 .iter()
475 .collect::<Vec<_>>()
476 .into_iter()
477 .rev()
478 {
479 let rect = box_model.padding_box;
480 if x < rect.x || y < rect.y || x > rect.x + rect.width || y > rect.y + rect.height {
481 continue;
482 }
483
484 let is_inline_container = is_inline && matches!(&info.kind, NodeKind::Container { .. });
489 let (local_x, local_y) = if is_inline_container {
490 (x, y)
491 } else {
492 (
493 x - box_model.content_box.x + own_scroll.0,
494 y - box_model.content_box.y + own_scroll.1,
495 )
496 };
497
498 for (child_layout, child_info) in layout.children.iter().zip(&mut info.children).rev() {
499 if let Some(child_node) = child_layout.node()
500 && let Some(scrolled_id) = scroll_at_inner(
501 child_node,
502 child_info,
503 viewport,
504 local_x,
505 local_y,
506 dx,
507 dy,
508 child_scroll,
509 )
510 {
511 return Some(scrolled_id);
512 }
513 }
514
515 let scrolled = match &mut info.kind {
516 NodeKind::Container {
517 scroll_x,
518 scroll_y,
519 scroll_offset_x,
520 scroll_offset_y,
521 ..
522 }
523 | NodeKind::Custom {
524 scroll_x,
525 scroll_y,
526 scroll_offset_x,
527 scroll_offset_y,
528 ..
529 } => {
530 let mut changed = false;
531
532 let (vw, vh) = viewport;
533
534 if *scroll_y {
535 let max_scroll = (box_model.children_box.height
536 - box_model.content_box.height.min(vh))
537 .max(0.0);
538 let next = (*scroll_offset_y + dy).clamp(0.0, max_scroll);
539 if (next - *scroll_offset_y).abs() > f32::EPSILON {
540 changed = true;
541 }
542 *scroll_offset_y = next;
543 }
544 if *scroll_x {
545 let max_scroll = (box_model.children_box.width
546 - box_model.content_box.width.min(vw))
547 .max(0.0);
548 let next = (*scroll_offset_x + dx).clamp(0.0, max_scroll);
549 if (next - *scroll_offset_x).abs() > f32::EPSILON {
550 changed = true;
551 }
552 *scroll_offset_x = next;
553 }
554 changed
555 }
556 _ => false,
557 };
558 if scrolled {
559 return Some(info.dom_id.unwrap_or(NO_SCROLL_DOM_ID));
560 }
561 }
562
563 None
564}
565
566pub fn focus_text_input(info: &InfoNode, target: Option<&Arc<dyn CustomNode>>) -> bool {
570 let mut focused = false;
571 if let NodeKind::Custom { node, .. } = &info.kind
572 && node.accepts_text_input()
573 {
574 let is_target = target.is_some_and(|target| Arc::ptr_eq(node, target));
575 node.set_focused(is_target);
576 focused |= is_target;
577 }
578 for child in &info.children {
579 focused |= focus_text_input(child, target);
580 }
581 focused
582}
583
584pub fn dispatch_text_input(info: &InfoNode, event: InputTextEvent) -> bool {
586 if let NodeKind::Custom { node, .. } = &info.kind
587 && node.accepts_text_input()
588 && node.is_focused()
589 {
590 return node.handle_text_input(event);
591 }
592 for child in &info.children {
593 if dispatch_text_input(child, event.clone()) {
594 return true;
595 }
596 }
597 false
598}
599
600pub fn focused_text_input_is_composing(info: &InfoNode) -> bool {
602 if let NodeKind::Custom { node, .. } = &info.kind
603 && node.accepts_text_input()
604 && node.is_focused()
605 {
606 return node.is_composing();
607 }
608 info.children.iter().any(focused_text_input_is_composing)
609}
610
611pub fn any_custom_node_needs_repaint(info: &InfoNode) -> bool {
616 if let NodeKind::Custom { node, .. } = &info.kind
617 && node.needs_repaint()
618 {
619 return true;
620 }
621 info.children.iter().any(any_custom_node_needs_repaint)
622}
623
624#[cfg(test)]
625mod tests {
626 use super::*;
627 use crate::engine::bridge::text::{FallbackTextMeasurer, TextMeasurer};
628 use crate::engine::layouter::types::{Color, ContainerRole, ContainerStyle, TextStyle};
629 use crate::engine::renderer_model::{DrawCommand, Rect};
630 use crate::engine::ui::button::ButtonComponent;
631 use crate::engine::ui::input_text::InputTextComponent;
632 use crate::engine::ui::input_text_types::InputTextEvent;
633 use crate::engine::ui::{ContentSize, Popup};
634 use std::sync::Arc;
635 use std::sync::Mutex;
636 use std::sync::atomic::{AtomicBool, Ordering};
637 use ui_layout::{LayoutChild, Style};
638
639 const VIEWPORT_WIDTH: f32 = 800.0;
640 const VIEWPORT_HEIGHT: f32 = 600.0;
641
642 fn input_info(node: Arc<dyn CustomNode>) -> InfoNode {
643 InfoNode {
644 kind: NodeKind::Custom {
645 node,
646 scroll_x: false,
647 scroll_y: false,
648 scroll_offset_x: 0.0,
649 scroll_offset_y: 0.0,
650 style: ContainerStyle::default(),
651 layout_style: ui_layout::Style::default(),
652 text_style: TextStyle::default(),
653 text_flow_style: TextFlowStyle::default(),
654 },
655 children: Vec::new(),
656 dom_id: None,
657 }
658 }
659
660 #[test]
661 fn focus_and_dispatch_target_one_input() {
662 let measurer: Arc<dyn TextMeasurer> = Arc::new(FallbackTextMeasurer);
663 let first: Arc<dyn CustomNode> =
664 Arc::new(InputTextComponent::new("", "", Arc::clone(&measurer)));
665 let second: Arc<dyn CustomNode> = Arc::new(InputTextComponent::new("", "", measurer));
666 let root = InfoNode {
667 kind: NodeKind::LineBreak,
668 children: vec![
669 input_info(Arc::clone(&first)),
670 input_info(Arc::clone(&second)),
671 ],
672 dom_id: None,
673 };
674
675 assert!(focus_text_input(&root, Some(&second)));
676 assert!(!first.is_focused());
677 assert!(second.is_focused());
678 assert!(dispatch_text_input(
679 &root,
680 InputTextEvent::Commit("日本".into())
681 ));
682 }
683
684 #[test]
685 fn hit_custom_node_finds_innermost_custom() {
686 let node: Arc<dyn CustomNode> = Arc::new(InputTextComponent::new(
687 "",
688 "",
689 Arc::new(FallbackTextMeasurer),
690 ));
691 let info = input_info(Arc::clone(&node));
692 let layout = LayoutNode::new(ui_layout::Style::default());
693 let path = vec![
694 HitItem {
695 layout: &layout,
696 info: &info,
697 },
698 HitItem {
699 layout: &layout,
700 info: &info,
701 },
702 ];
703 assert!(Arc::ptr_eq(hit_custom_node(&path).unwrap(), &node));
704 }
705
706 #[test]
707 fn update_hover_switches_target() {
708 let measurer: Arc<dyn TextMeasurer> = Arc::new(FallbackTextMeasurer);
709 let a: Arc<dyn CustomNode> = Arc::new(ButtonComponent::new(
710 "A",
711 Color(0, 0, 0, 255),
712 Color(255, 255, 255, 255),
713 Arc::clone(&measurer),
714 ));
715 let b: Arc<dyn CustomNode> = Arc::new(ButtonComponent::new(
716 "B",
717 Color(0, 0, 0, 255),
718 Color(255, 255, 255, 255),
719 measurer,
720 ));
721 let info_a = input_info(Arc::clone(&a));
722 let info_b = input_info(Arc::clone(&b));
723 let layout = LayoutNode::new(ui_layout::Style::default());
724 let path_a = vec![HitItem {
725 layout: &layout,
726 info: &info_a,
727 }];
728 let path_b = vec![HitItem {
729 layout: &layout,
730 info: &info_b,
731 }];
732
733 assert!(update_hover(&path_a, None));
734 assert!(a.is_hovered());
735 assert!(!b.is_hovered());
736
737 assert!(update_hover(&path_b, Some(&a)));
738 assert!(!a.is_hovered());
739 assert!(b.is_hovered());
740
741 assert!(!update_hover(&path_b, Some(&b)));
742 }
743
744 #[test]
745 fn any_custom_node_needs_repaint_tracks_dirty_nodes() {
746 let measurer: Arc<dyn TextMeasurer> = Arc::new(FallbackTextMeasurer);
747 let a: Arc<dyn CustomNode> = Arc::new(ButtonComponent::new(
748 "A",
749 Color(0, 0, 0, 255),
750 Color(255, 255, 255, 255),
751 Arc::clone(&measurer),
752 ));
753 let info_a = input_info(Arc::clone(&a));
754
755 assert!(any_custom_node_needs_repaint(&info_a));
757 assert!(!any_custom_node_needs_repaint(&info_a));
758
759 a.on_pointer_event(PointerEvent::Down { x: 0.0, y: 0.0 });
761 assert!(any_custom_node_needs_repaint(&info_a));
762 assert!(!any_custom_node_needs_repaint(&info_a));
763 }
764
765 fn box_model(
766 x: f32,
767 y: f32,
768 width: f32,
769 height: f32,
770 children_width: f32,
771 children_height: f32,
772 ) -> ui_layout::BoxModel {
773 let rect = ui_layout::Rect {
774 x,
775 y,
776 width,
777 height,
778 };
779 ui_layout::BoxModel {
780 sticky_edges: None,
781 border_box: rect,
782 padding_box: rect,
783 content_box: rect,
784 children_box: ui_layout::Rect {
785 x,
786 y,
787 width: children_width,
788 height: children_height,
789 },
790 }
791 }
792
793 fn scrollable_info() -> InfoNode {
794 InfoNode {
795 kind: NodeKind::Container {
796 scroll_x: false,
797 scroll_y: true,
798 scroll_offset_x: 0.0,
799 scroll_offset_y: 0.0,
800 style: ContainerStyle::default(),
801 role: crate::engine::layouter::types::ContainerRole::Normal,
802 },
803 children: Vec::new(),
804 dom_id: None,
805 }
806 }
807
808 fn container_info(scroll_y: bool, dom_id: Option<u32>) -> InfoNode {
809 InfoNode {
810 kind: NodeKind::Container {
811 scroll_x: false,
812 scroll_y,
813 scroll_offset_x: 0.0,
814 scroll_offset_y: 0.0,
815 style: ContainerStyle::default(),
816 role: crate::engine::layouter::types::ContainerRole::Normal,
817 },
818 children: Vec::new(),
819 dom_id,
820 }
821 }
822
823 fn inline_node(children: Vec<LayoutChild>) -> LayoutNode {
827 let mut node = LayoutNode::with_children(ui_layout::Style::default(), children);
828 node.layout_box = ui_layout::LayoutBox::InlineBox(ui_layout::InlineBox {
829 box_model: box_model(10.0, 10.0, 200.0, 20.0, 200.0, 20.0),
830 line_spans: vec![ui_layout::LineSpan {
831 x_range: 0.0..200.0,
832 line_pos: (10.0, 10.0),
833 line_index: 0,
834 }],
835 });
836 node
837 }
838
839 fn scroll_offset_y_of(info: &InfoNode) -> f32 {
840 let NodeKind::Container {
841 scroll_offset_y, ..
842 } = &info.kind
843 else {
844 panic!("expected container");
845 };
846 *scroll_offset_y
847 }
848
849 fn set_vertical_scroll(info: &mut InfoNode, offset: f32) {
850 let NodeKind::Container {
851 scroll_offset_y, ..
852 } = &mut info.kind
853 else {
854 panic!("expected container");
855 };
856 *scroll_offset_y = offset;
857 }
858
859 fn fixed_child_layout(children_height: f32) -> LayoutNode {
860 let mut style = ui_layout::Style::default();
861 style.position.kind = Position::Fixed;
862 let mut child = LayoutNode::new(style);
863 child.layout_box = ui_layout::LayoutBox::BlockBox(box_model(
864 10.0,
865 10.0,
866 30.0,
867 20.0,
868 30.0,
869 children_height,
870 ));
871 child
872 }
873
874 #[test]
875 fn hit_test_fixed_child_ignores_ancestor_scroll() {
876 let mut layout =
877 LayoutNode::with_children(ui_layout::Style::default(), [fixed_child_layout(20.0)]);
878 layout.layout_box =
879 ui_layout::LayoutBox::BlockBox(box_model(0.0, 0.0, 200.0, 100.0, 200.0, 300.0));
880
881 let mut info = scrollable_info();
882 set_vertical_scroll(&mut info, 50.0);
883 let mut fixed_info = scrollable_info();
884 fixed_info.dom_id = Some(42);
885 info.children.push(fixed_info);
886
887 let path = hit_test(&layout, &info, 15.0, 15.0);
888 assert_eq!(hit_dom_id(&path), Some(42));
889 }
890
891 #[test]
892 fn scroll_at_fixed_child_ignores_ancestor_scroll() {
893 let mut layout =
894 LayoutNode::with_children(ui_layout::Style::default(), [fixed_child_layout(80.0)]);
895 layout.layout_box =
896 ui_layout::LayoutBox::BlockBox(box_model(0.0, 0.0, 200.0, 100.0, 200.0, 300.0));
897
898 let mut info = scrollable_info();
899 set_vertical_scroll(&mut info, 50.0);
900 info.children.push(scrollable_info());
901
902 assert!(
903 scroll_at(
904 &layout,
905 &mut info,
906 (VIEWPORT_WIDTH, VIEWPORT_HEIGHT),
907 15.0,
908 15.0,
909 0.0,
910 10.0
911 )
912 .is_some()
913 );
914 let NodeKind::Container {
915 scroll_offset_y: child_scroll,
916 ..
917 } = &info.children[0].kind
918 else {
919 panic!("expected fixed child container");
920 };
921 assert_eq!(*child_scroll, 10.0);
922 let NodeKind::Container {
923 scroll_offset_y: parent_scroll,
924 ..
925 } = &info.kind
926 else {
927 panic!("expected parent container");
928 };
929 assert_eq!(*parent_scroll, 50.0);
930 }
931
932 #[test]
933 fn scroll_at_scrolls_under_point_clamped_to_range() {
934 let mut layout = LayoutNode::new(ui_layout::Style::default());
935 layout.layout_box =
936 ui_layout::LayoutBox::BlockBox(box_model(0.0, 0.0, 200.0, 100.0, 200.0, 300.0));
937 let mut info = scrollable_info();
938
939 assert!(
941 scroll_at(
942 &layout,
943 &mut info,
944 (VIEWPORT_WIDTH, VIEWPORT_HEIGHT),
945 50.0,
946 50.0,
947 0.0,
948 100.0
949 )
950 .is_some()
951 );
952 let NodeKind::Container {
953 scroll_offset_y, ..
954 } = &info.kind
955 else {
956 panic!("expected container");
957 };
958 assert_eq!(*scroll_offset_y, 100.0);
959
960 assert!(
962 scroll_at(
963 &layout,
964 &mut info,
965 (VIEWPORT_WIDTH, VIEWPORT_HEIGHT),
966 50.0,
967 50.0,
968 0.0,
969 300.0
970 )
971 .is_some()
972 );
973 let NodeKind::Container {
974 scroll_offset_y, ..
975 } = &info.kind
976 else {
977 panic!("expected container");
978 };
979 assert_eq!(*scroll_offset_y, 200.0);
980
981 assert!(
983 scroll_at(
984 &layout,
985 &mut info,
986 (VIEWPORT_WIDTH, VIEWPORT_HEIGHT),
987 50.0,
988 50.0,
989 0.0,
990 -500.0
991 )
992 .is_some()
993 );
994 let NodeKind::Container {
995 scroll_offset_y, ..
996 } = &info.kind
997 else {
998 panic!("expected container");
999 };
1000 assert_eq!(*scroll_offset_y, 0.0);
1001 }
1002
1003 #[test]
1004 fn scroll_at_ignores_cursor_outside_box() {
1005 let mut layout = LayoutNode::new(ui_layout::Style::default());
1006 layout.layout_box =
1007 ui_layout::LayoutBox::BlockBox(box_model(0.0, 0.0, 200.0, 100.0, 200.0, 300.0));
1008 let mut info = scrollable_info();
1009
1010 assert!(
1011 scroll_at(
1012 &layout,
1013 &mut info,
1014 (VIEWPORT_WIDTH, VIEWPORT_HEIGHT),
1015 250.0,
1016 50.0,
1017 0.0,
1018 -100.0
1019 )
1020 .is_none()
1021 );
1022 }
1023
1024 #[test]
1025 fn scroll_at_ignores_non_scrollable_containers() {
1026 let mut layout = LayoutNode::new(ui_layout::Style::default());
1027 layout.layout_box =
1028 ui_layout::LayoutBox::BlockBox(box_model(0.0, 0.0, 200.0, 100.0, 200.0, 300.0));
1029 let mut info = InfoNode {
1030 kind: NodeKind::Container {
1031 scroll_x: false,
1032 scroll_y: false,
1033 scroll_offset_x: 0.0,
1034 scroll_offset_y: 0.0,
1035 style: ContainerStyle::default(),
1036 role: crate::engine::layouter::types::ContainerRole::Normal,
1037 },
1038 children: Vec::new(),
1039 dom_id: None,
1040 };
1041
1042 assert!(
1043 scroll_at(
1044 &layout,
1045 &mut info,
1046 (VIEWPORT_WIDTH, VIEWPORT_HEIGHT),
1047 50.0,
1048 50.0,
1049 0.0,
1050 -100.0
1051 )
1052 .is_none()
1053 );
1054 let NodeKind::Container {
1055 scroll_offset_y, ..
1056 } = &info.kind
1057 else {
1058 panic!("expected container");
1059 };
1060 assert_eq!(*scroll_offset_y, 0.0);
1061 }
1062
1063 #[test]
1064 fn scroll_at_prefers_innermost_scrollable_container() {
1065 let outer_children_box = box_model(0.0, 0.0, 400.0, 300.0, 400.0, 900.0);
1069 let inner_children_box = box_model(10.0, 10.0, 100.0, 80.0, 100.0, 240.0);
1070
1071 let mut outer_layout = LayoutNode::with_children(
1072 ui_layout::Style::default(),
1073 [LayoutNode::new(ui_layout::Style::default())],
1074 );
1075 outer_layout.layout_box = ui_layout::LayoutBox::BlockBox(outer_children_box);
1076
1077 let mut inner_layout = LayoutNode::new(ui_layout::Style::default());
1078 inner_layout.layout_box = ui_layout::LayoutBox::BlockBox(inner_children_box);
1079
1080 outer_layout.children[0] = ui_layout::LayoutChild::Node(Box::new(inner_layout));
1081
1082 let mut outer_info = scrollable_info();
1083 let inner_info = scrollable_info();
1084 outer_info.children.push(inner_info);
1085
1086 assert!(
1088 scroll_at(
1089 &outer_layout,
1090 &mut outer_info,
1091 (VIEWPORT_WIDTH, VIEWPORT_HEIGHT),
1092 50.0,
1093 50.0,
1094 0.0,
1095 30.0
1096 )
1097 .is_some()
1098 );
1099 let NodeKind::Container {
1100 scroll_offset_y, ..
1101 } = &outer_info.children[0].kind
1102 else {
1103 panic!("expected inner container");
1104 };
1105 assert_eq!(*scroll_offset_y, 30.0);
1106 let NodeKind::Container {
1107 scroll_offset_y: outer_off,
1108 ..
1109 } = &outer_info.kind
1110 else {
1111 panic!("expected outer container");
1112 };
1113 assert_eq!(*outer_off, 0.0);
1114 }
1115
1116 #[test]
1117 fn scroll_at_reports_the_scrolled_containers_dom_id() {
1118 let outer_children_box = box_model(0.0, 0.0, 400.0, 300.0, 400.0, 900.0);
1121 let inner_children_box = box_model(10.0, 10.0, 100.0, 80.0, 100.0, 240.0);
1122
1123 let mut outer_layout = LayoutNode::with_children(
1124 ui_layout::Style::default(),
1125 [LayoutNode::new(ui_layout::Style::default())],
1126 );
1127 outer_layout.layout_box = ui_layout::LayoutBox::BlockBox(outer_children_box);
1128 let mut inner_layout = LayoutNode::new(ui_layout::Style::default());
1129 inner_layout.layout_box = ui_layout::LayoutBox::BlockBox(inner_children_box);
1130 outer_layout.children[0] = ui_layout::LayoutChild::Node(Box::new(inner_layout));
1131
1132 let mut outer_info = container_info(true, Some(7));
1133 let inner_info = container_info(true, Some(9));
1134 outer_info.children.push(inner_info);
1135
1136 let scrolled = scroll_at(
1137 &outer_layout,
1138 &mut outer_info,
1139 (VIEWPORT_WIDTH, VIEWPORT_HEIGHT),
1140 50.0,
1141 50.0,
1142 0.0,
1143 30.0,
1144 );
1145 assert_eq!(scrolled, Some(9));
1146 }
1147
1148 #[test]
1149 fn scroll_at_reports_marker_when_scrolled_container_has_no_dom_id() {
1150 let mut layout = LayoutNode::new(ui_layout::Style::default());
1151 layout.layout_box =
1152 ui_layout::LayoutBox::BlockBox(box_model(0.0, 0.0, 200.0, 100.0, 200.0, 300.0));
1153 let mut info = container_info(true, None);
1154 let scrolled = scroll_at(
1155 &layout,
1156 &mut info,
1157 (VIEWPORT_WIDTH, VIEWPORT_HEIGHT),
1158 50.0,
1159 50.0,
1160 0.0,
1161 10.0,
1162 );
1163 assert_eq!(scrolled, Some(NO_SCROLL_DOM_ID));
1164 }
1165
1166 #[derive(Debug)]
1168 struct RecordingNode {
1169 pointer_events: Mutex<Vec<PointerEvent>>,
1170 popup_events: Mutex<Vec<PointerEvent>>,
1171 popup_rect: Rect,
1172 popup_open: AtomicBool,
1173 dismissed: AtomicBool,
1174 }
1175
1176 impl RecordingNode {
1177 fn new(popup_rect: Rect) -> Self {
1178 RecordingNode {
1179 pointer_events: Mutex::new(Vec::new()),
1180 popup_events: Mutex::new(Vec::new()),
1181 popup_rect,
1182 popup_open: AtomicBool::new(true),
1183 dismissed: AtomicBool::new(false),
1184 }
1185 }
1186
1187 fn events(&self) -> Vec<PointerEvent> {
1188 self.pointer_events.lock().unwrap().clone()
1189 }
1190
1191 fn popup_events(&self) -> Vec<PointerEvent> {
1192 self.popup_events.lock().unwrap().clone()
1193 }
1194 }
1195
1196 impl CustomNode for RecordingNode {
1197 fn draw_sized(
1198 &self,
1199 _cmd_buf: &mut Vec<DrawCommand>,
1200 _text_style: &TextStyle,
1201 _text_flow_style: &TextFlowStyle,
1202 _style: &Style,
1203 _size: ContentSize,
1204 ) {
1205 }
1206
1207 fn intrinsic_size(&self) -> ContentSize {
1208 ContentSize {
1209 width: 120.0,
1210 height: 28.0,
1211 }
1212 }
1213
1214 fn on_pointer_event(&self, event: PointerEvent) -> bool {
1215 self.pointer_events.lock().unwrap().push(event);
1216 true
1217 }
1218
1219 fn popup(
1220 &self,
1221 _text_style: &TextStyle,
1222 _text_flow_style: &TextFlowStyle,
1223 ) -> Option<Popup> {
1224 self.popup_open.load(Ordering::Relaxed).then(|| Popup {
1225 rect: self.popup_rect,
1226 commands: Vec::new(),
1227 })
1228 }
1229
1230 fn on_popup_pointer_event(&self, event: PointerEvent) -> bool {
1231 self.popup_events.lock().unwrap().push(event);
1232 true
1233 }
1234
1235 fn dismiss_popup(&self) {
1236 self.dismissed.store(true, Ordering::Relaxed);
1237 }
1238 }
1239
1240 fn make_tree(a_scroll_y: f32) -> (LayoutNode, InfoNode, Arc<RecordingNode>) {
1243 let node: Arc<RecordingNode> = Arc::new(RecordingNode::new(Rect {
1244 x: 0.0,
1245 y: 28.0,
1246 width: 120.0,
1247 height: 84.0,
1248 }));
1249 let custom_info = input_info(Arc::clone(&node) as Arc<dyn CustomNode>);
1250
1251 let mut a_layout = LayoutNode::new(ui_layout::Style::default());
1252 a_layout.layout_box =
1253 ui_layout::LayoutBox::BlockBox(box_model(10.0, 20.0, 160.0, 100.0, 160.0, 300.0));
1254 let mut custom_layout = LayoutNode::new(ui_layout::Style::default());
1255 custom_layout.layout_box =
1256 ui_layout::LayoutBox::BlockBox(box_model(5.0, 0.0, 120.0, 28.0, 120.0, 28.0));
1257 a_layout.children = vec![LayoutChild::Node(Box::new(custom_layout))];
1258
1259 let mut root_layout = LayoutNode::with_children(ui_layout::Style::default(), [a_layout]);
1260 root_layout.layout_box =
1261 ui_layout::LayoutBox::BlockBox(box_model(0.0, 0.0, 200.0, 200.0, 200.0, 200.0));
1262
1263 let a_info = InfoNode {
1264 kind: NodeKind::Container {
1265 scroll_x: false,
1266 scroll_y: true,
1267 scroll_offset_x: 0.0,
1268 scroll_offset_y: a_scroll_y,
1269 style: ContainerStyle::default(),
1270 role: ContainerRole::Normal,
1271 },
1272 children: vec![custom_info],
1273 dom_id: None,
1274 };
1275 let root_info = InfoNode {
1276 kind: NodeKind::Container {
1277 scroll_x: false,
1278 scroll_y: false,
1279 scroll_offset_x: 0.0,
1280 scroll_offset_y: 0.0,
1281 style: ContainerStyle::default(),
1282 role: ContainerRole::Normal,
1283 },
1284 children: vec![a_info],
1285 dom_id: None,
1286 };
1287
1288 (root_layout, root_info, node)
1289 }
1290
1291 fn build_path<'a>(root_layout: &'a LayoutNode, root_info: &'a InfoNode) -> HitPath<'a> {
1293 let a_layout = match &root_layout.children[0] {
1294 LayoutChild::Node(node) => node,
1295 _ => unreachable!("expected container child"),
1296 };
1297 let custom_layout = match &a_layout.children[0] {
1298 LayoutChild::Node(node) => node,
1299 _ => unreachable!("expected custom child"),
1300 };
1301 let a_info = &root_info.children[0];
1302 let custom_info = &a_info.children[0];
1303 vec![
1304 HitItem {
1305 layout: custom_layout,
1306 info: custom_info,
1307 },
1308 HitItem {
1309 layout: a_layout,
1310 info: a_info,
1311 },
1312 HitItem {
1313 layout: root_layout,
1314 info: root_info,
1315 },
1316 ]
1317 }
1318
1319 #[test]
1320 fn dispatch_pointer_uses_node_local_coords() {
1321 let (root_layout, root_info, node) = make_tree(0.0);
1322 node.popup_open.store(false, Ordering::Relaxed);
1323 let path = build_path(&root_layout, &root_info);
1324
1325 dispatch_pointer(&path, PointerEvent::Down { x: 20.0, y: 25.0 });
1327 assert_eq!(node.events(), vec![PointerEvent::Down { x: 5.0, y: 5.0 }]);
1328 }
1329
1330 #[test]
1331 fn dispatch_pointer_folds_ancestor_scroll_into_local_coords() {
1332 let (root_layout, root_info, node) = make_tree(50.0);
1333 node.popup_open.store(false, Ordering::Relaxed);
1334 let path = build_path(&root_layout, &root_info);
1335
1336 dispatch_pointer(&path, PointerEvent::Move { x: 20.0, y: 25.0 });
1338 assert_eq!(node.events(), vec![PointerEvent::Move { x: 5.0, y: 55.0 }]);
1339 }
1340
1341 #[test]
1342 fn popup_events_use_popup_local_coords() {
1343 let (root_layout, root_info, node) = make_tree(0.0);
1344 let path = build_path(&root_layout, &root_info);
1345
1346 dispatch_pointer(&path, PointerEvent::Down { x: 20.0, y: 60.0 });
1349 assert_eq!(
1350 node.popup_events(),
1351 vec![PointerEvent::Down { x: 5.0, y: 12.0 }]
1352 );
1353 assert!(node.events().is_empty());
1354 assert!(!node.dismissed.load(Ordering::Relaxed));
1355 }
1356
1357 #[test]
1358 fn down_outside_popup_routes_to_node() {
1359 let (root_layout, root_info, node) = make_tree(0.0);
1360 let path = build_path(&root_layout, &root_info);
1361
1362 dispatch_pointer(&path, PointerEvent::Down { x: 20.0, y: 30.0 });
1365 assert!(!node.dismissed.load(Ordering::Relaxed));
1366 assert_eq!(node.events(), vec![PointerEvent::Down { x: 5.0, y: 10.0 }]);
1367 assert!(node.popup_events().is_empty());
1368 }
1369
1370 #[test]
1371 fn move_outside_popup_routes_to_node() {
1372 let (root_layout, root_info, node) = make_tree(0.0);
1373 let path = build_path(&root_layout, &root_info);
1374
1375 dispatch_pointer(&path, PointerEvent::Move { x: 20.0, y: 30.0 });
1378 assert!(!node.dismissed.load(Ordering::Relaxed));
1379 assert_eq!(node.events(), vec![PointerEvent::Move { x: 5.0, y: 10.0 }]);
1380 assert!(node.popup_events().is_empty());
1381 }
1382
1383 fn popup_hit_assert(
1384 root_layout: &LayoutNode,
1385 root_info: &InfoNode,
1386 node: &Arc<RecordingNode>,
1387 x: f32,
1388 y: f32,
1389 ) {
1390 let path = hit_test(root_layout, root_info, x, y);
1391 let hit = hit_custom_node(&path).unwrap();
1392 let expected: Arc<dyn CustomNode> = node.clone();
1393 assert!(Arc::ptr_eq(hit, &expected));
1394 }
1395
1396 #[test]
1397 fn hit_test_finds_open_popup() {
1398 let (root_layout, root_info, node) = make_tree(0.0);
1399 popup_hit_assert(&root_layout, &root_info, &node, 20.0, 60.0);
1402 }
1403
1404 #[test]
1405 fn hit_test_ignores_closed_popup() {
1406 let (root_layout, root_info, node) = make_tree(0.0);
1407 node.popup_open.store(false, Ordering::Relaxed);
1408 let path = hit_test(&root_layout, &root_info, 20.0, 60.0);
1411 assert_eq!(path.len(), 2);
1412 assert!(hit_custom_node(&path).is_none());
1413 }
1414
1415 #[test]
1416 fn hit_test_finds_popup_escaping_ancestor_box() {
1417 let (root_layout, root_info, node) = make_tree(0.0);
1418 popup_hit_assert(&root_layout, &root_info, &node, 20.0, 125.0);
1422 }
1423
1424 #[test]
1425 fn hit_test_folds_scroll_into_popup_hit() {
1426 let (root_layout, root_info, node) = make_tree(50.0);
1427 popup_hit_assert(&root_layout, &root_info, &node, 20.0, 25.0);
1430 }
1431
1432 #[test]
1433 fn dismiss_open_popups_closes_popup_on_outside_press() {
1434 let (root_layout, root_info, node) = make_tree(0.0);
1435 let path = hit_test(&root_layout, &root_info, 10.0, 25.0);
1437 assert!(dismiss_open_popups(&root_info, &path));
1438 assert!(node.dismissed.load(Ordering::Relaxed));
1439 }
1440
1441 #[test]
1442 fn dismiss_open_popups_keeps_popup_under_press() {
1443 let (root_layout, root_info, node) = make_tree(0.0);
1444 let path = hit_test(&root_layout, &root_info, 20.0, 60.0);
1446 assert!(!dismiss_open_popups(&root_info, &path));
1447 assert!(!node.dismissed.load(Ordering::Relaxed));
1448 }
1449
1450 #[test]
1451 fn dismiss_open_popups_keeps_popup_when_press_on_owner_box() {
1452 let (root_layout, root_info, node) = make_tree(0.0);
1453 let path = hit_test(&root_layout, &root_info, 20.0, 30.0);
1456 assert!(!dismiss_open_popups(&root_info, &path));
1457 assert!(!node.dismissed.load(Ordering::Relaxed));
1458 }
1459
1460 #[test]
1461 fn dismiss_open_popups_ignores_closed_popup() {
1462 let (root_layout, root_info, node) = make_tree(0.0);
1463 node.popup_open.store(false, Ordering::Relaxed);
1464 let path = hit_test(&root_layout, &root_info, 10.0, 25.0);
1465 assert!(!dismiss_open_popups(&root_info, &path));
1466 assert!(!node.dismissed.load(Ordering::Relaxed));
1467 }
1468
1469 fn make_inline_tree() -> (LayoutNode, InfoNode) {
1473 let mut c = LayoutNode::new(ui_layout::Style::default());
1474 c.layout_box =
1475 ui_layout::LayoutBox::BlockBox(box_model(30.0, 10.0, 50.0, 20.0, 50.0, 20.0));
1476 let mut b =
1477 LayoutNode::with_children(ui_layout::Style::default(), [inline_node(vec![c.into()])]);
1478 b.layout_box =
1479 ui_layout::LayoutBox::BlockBox(box_model(0.0, 0.0, 300.0, 100.0, 300.0, 100.0));
1480
1481 let c_info = container_info(false, Some(7));
1482 let mut i_info = container_info(false, None);
1483 i_info.children.push(c_info);
1484 let mut b_info = container_info(false, None);
1485 b_info.children.push(i_info);
1486
1487 (b, b_info)
1488 }
1489
1490 #[test]
1491 fn hit_test_inside_inline_keeps_parent_coords() {
1492 let (b, b_info) = make_inline_tree();
1493 let path = hit_test(&b, &b_info, 40.0, 15.0);
1497 assert_eq!(hit_dom_id(&path), Some(7));
1498 assert_eq!(path.len(), 3);
1499 }
1500
1501 #[test]
1502 fn hit_test_inside_inline_but_outside_child_hits_inline() {
1503 let (b, b_info) = make_inline_tree();
1504 let path = hit_test(&b, &b_info, 12.0, 12.0);
1507 assert_eq!(path.len(), 2);
1508 assert_eq!(hit_dom_id(&path), None);
1509 }
1510
1511 #[test]
1512 fn hit_test_outside_inline_line_boxes_falls_back_to_parent() {
1513 let (b, b_info) = make_inline_tree();
1514 let path = hit_test(&b, &b_info, 40.0, 50.0);
1516 assert_eq!(path.len(), 1);
1517 assert_eq!(hit_dom_id(&path), None);
1518 }
1519
1520 #[test]
1521 fn dispatch_pointer_to_inline_custom_keeps_parent_content_coords() {
1522 let node: Arc<RecordingNode> = Arc::new(RecordingNode::new(Rect {
1523 x: 0.0,
1524 y: 28.0,
1525 width: 120.0,
1526 height: 84.0,
1527 }));
1528 let custom_info = input_info(Arc::clone(&node) as Arc<dyn CustomNode>);
1529
1530 let mut inline_layout = LayoutNode::new(ui_layout::Style::default());
1531 inline_layout.layout_box = ui_layout::LayoutBox::InlineBox(ui_layout::InlineBox {
1532 box_model: box_model(10.0, 20.0, 160.0, 100.0, 160.0, 100.0),
1533 line_spans: vec![ui_layout::LineSpan {
1534 x_range: 0.0..160.0,
1535 line_pos: (10.0, 20.0),
1536 line_index: 0,
1537 }],
1538 });
1539 let mut block_layout = LayoutNode::new(ui_layout::Style::default());
1540 block_layout.layout_box =
1541 ui_layout::LayoutBox::BlockBox(box_model(0.0, 0.0, 200.0, 200.0, 200.0, 200.0));
1542
1543 let inline_info = container_info(false, None);
1544 let root_info = container_info(false, None);
1545
1546 node.popup_open.store(false, Ordering::Relaxed);
1547 let path = vec![
1548 HitItem {
1549 layout: &inline_layout,
1550 info: &custom_info,
1551 },
1552 HitItem {
1553 layout: &inline_layout,
1554 info: &inline_info,
1555 },
1556 HitItem {
1557 layout: &block_layout,
1558 info: &root_info,
1559 },
1560 ];
1561
1562 dispatch_pointer(&path, PointerEvent::Down { x: 30.0, y: 35.0 });
1566 assert_eq!(node.events(), vec![PointerEvent::Down { x: 30.0, y: 35.0 }]);
1567 }
1568
1569 #[test]
1570 fn scroll_at_inside_inline_scrolls_child_in_parent_coords() {
1571 let mut c = LayoutNode::new(ui_layout::Style::default());
1572 c.layout_box =
1573 ui_layout::LayoutBox::BlockBox(box_model(30.0, 10.0, 50.0, 80.0, 50.0, 240.0));
1574 let mut b =
1575 LayoutNode::with_children(ui_layout::Style::default(), [inline_node(vec![c.into()])]);
1576 b.layout_box =
1577 ui_layout::LayoutBox::BlockBox(box_model(0.0, 0.0, 200.0, 100.0, 200.0, 300.0));
1578
1579 let c_info = container_info(true, None);
1580 let mut i_info = container_info(false, None);
1581 i_info.children.push(c_info);
1582 let mut b_info = scrollable_info();
1583 b_info.children.push(i_info);
1584
1585 assert!(
1586 scroll_at(
1587 &b,
1588 &mut b_info,
1589 (VIEWPORT_WIDTH, VIEWPORT_HEIGHT),
1590 40.0,
1591 15.0,
1592 0.0,
1593 10.0
1594 )
1595 .is_some()
1596 );
1597 assert_eq!(scroll_offset_y_of(&b_info.children[0].children[0]), 10.0);
1599 assert_eq!(scroll_offset_y_of(&b_info), 0.0);
1600 }
1601}