1use std::sync::Arc;
10
11use kozan_primitives::geometry::{Offset, Point};
12
13use crate::scroll::ScrollOffsets;
14
15use super::fragment::{Fragment, FragmentKind, OverflowClip};
16
17#[derive(Debug, Clone)]
19pub struct HitTestResult {
20 pub node_index: Option<u32>,
21 pub local_point: Point,
22}
23
24impl HitTestResult {
25 pub const NONE: Self = Self {
26 node_index: None,
27 local_point: Point::ZERO,
28 };
29}
30
31pub struct HitTester<'a> {
36 scroll_offsets: &'a ScrollOffsets,
37}
38
39impl<'a> HitTester<'a> {
40 pub fn new(scroll_offsets: &'a ScrollOffsets) -> Self {
41 Self { scroll_offsets }
42 }
43
44 pub fn test(&self, fragment: &Fragment, point: Point) -> HitTestResult {
46 self.test_fragment(fragment, point, Point::ZERO)
47 }
48
49 fn test_fragment(&self, fragment: &Fragment, point: Point, origin: Point) -> HitTestResult {
50 let local = point - origin;
51 let in_bounds = local.dx >= 0.0
52 && local.dy >= 0.0
53 && local.dx < fragment.size.width
54 && local.dy < fragment.size.height;
55
56 if let FragmentKind::Box(ref box_data) = fragment.kind {
57 let clips = box_data.overflow_x != OverflowClip::Visible
58 || box_data.overflow_y != OverflowClip::Visible;
59 if clips && !in_bounds {
60 return HitTestResult::NONE;
61 }
62
63 let is_user_scrollable = box_data.overflow_x.is_user_scrollable()
66 || box_data.overflow_y.is_user_scrollable();
67 let scroll_offset = if is_user_scrollable {
68 fragment
69 .dom_node
70 .map(|id| self.scroll_offsets.offset(id))
71 .unwrap_or(Offset::ZERO)
72 } else {
73 Offset::ZERO
74 };
75
76 for child in box_data.children.iter().rev() {
78 let child_origin =
79 origin + Offset::new(child.offset.x, child.offset.y) - scroll_offset;
80 let result = self.test_fragment(&child.fragment, point, child_origin);
81 if result.node_index.is_some() {
82 return result;
83 }
84 }
85 }
86
87 if let FragmentKind::Line(ref line_data) = fragment.kind {
88 for child in line_data.children.iter().rev() {
89 let child_origin = origin + Offset::new(child.offset.x, child.offset.y);
90 let result = self.test_fragment(&child.fragment, point, child_origin);
91 if result.node_index.is_some() {
92 return result;
93 }
94 }
95 }
96
97 if in_bounds {
98 if let Some(node_index) = fragment.dom_node {
99 return HitTestResult {
100 node_index: Some(node_index),
101 local_point: Point::new(local.dx, local.dy),
102 };
103 }
104 }
105
106 HitTestResult::NONE
107 }
108}
109
110pub struct HitTestCache {
115 last_fragment_ptr: usize,
116 last_point: Point,
117 last_result: HitTestResult,
118}
119
120const TOLERANCE: f32 = 0.5;
121
122impl Default for HitTestCache {
123 fn default() -> Self {
124 Self {
125 last_fragment_ptr: 0,
126 last_point: Point::ZERO,
127 last_result: HitTestResult::NONE,
128 }
129 }
130}
131
132impl HitTestCache {
133 #[must_use]
134 pub fn new() -> Self {
135 Self::default()
136 }
137
138 pub fn test(
140 &mut self,
141 tester: &HitTester,
142 fragment: &Arc<Fragment>,
143 point: Point,
144 ) -> &HitTestResult {
145 let ptr = Arc::as_ptr(fragment) as usize;
146 let same_fragment = self.last_fragment_ptr == ptr;
147 let close_enough = (point.x - self.last_point.x).abs() < TOLERANCE
148 && (point.y - self.last_point.y).abs() < TOLERANCE;
149
150 if same_fragment && close_enough {
151 return &self.last_result;
152 }
153
154 self.last_result = tester.test(fragment, point);
155 self.last_fragment_ptr = ptr;
156 self.last_point = point;
157 &self.last_result
158 }
159
160 pub fn invalidate(&mut self) {
162 self.last_fragment_ptr = 0;
163 }
164}
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169 use crate::layout::fragment::*;
170 use kozan_primitives::geometry::Size;
171
172 fn make_box(size: Size, dom_node: Option<u32>, children: Vec<ChildFragment>) -> Arc<Fragment> {
173 Fragment::new_box_styled_with_node(size, dom_node, children)
174 }
175
176 impl Fragment {
177 fn new_box_styled_with_node(
178 size: Size,
179 dom_node: Option<u32>,
180 children: Vec<ChildFragment>,
181 ) -> Arc<Self> {
182 Arc::new(Self {
183 size,
184 kind: FragmentKind::Box(BoxFragmentData {
185 children,
186 ..Default::default()
187 }),
188 style: None,
189 dom_node,
190 })
191 }
192 }
193
194 fn no_scroll() -> ScrollOffsets {
195 ScrollOffsets::new()
196 }
197
198 fn tester(offsets: &ScrollOffsets) -> HitTester<'_> {
199 HitTester::new(offsets)
200 }
201
202 #[test]
203 fn hit_empty_fragment() {
204 let root = make_box(Size::new(800.0, 600.0), Some(0), vec![]);
205 let result = tester(&no_scroll()).test(&root, Point::new(400.0, 300.0));
206 assert_eq!(result.node_index, Some(0));
207 }
208
209 #[test]
210 fn miss_outside_root() {
211 let root = make_box(Size::new(800.0, 600.0), Some(0), vec![]);
212 let result = tester(&no_scroll()).test(&root, Point::new(900.0, 300.0));
213 assert_eq!(result.node_index, None);
214 }
215
216 #[test]
217 fn miss_negative_coords() {
218 let root = make_box(Size::new(800.0, 600.0), Some(0), vec![]);
219 let result = tester(&no_scroll()).test(&root, Point::new(-10.0, 300.0));
220 assert_eq!(result.node_index, None);
221 }
222
223 #[test]
224 fn nested_child_hit() {
225 let child = make_box(Size::new(100.0, 50.0), Some(1), vec![]);
226 let root = make_box(
227 Size::new(800.0, 600.0),
228 Some(0),
229 vec![ChildFragment {
230 offset: Point::new(50.0, 50.0),
231 fragment: child,
232 }],
233 );
234 let result = tester(&no_scroll()).test(&root, Point::new(75.0, 60.0));
235 assert_eq!(result.node_index, Some(1));
236 assert!((result.local_point.x - 25.0).abs() < 0.001);
237 }
238
239 #[test]
240 fn last_child_wins_overlap() {
241 let a = make_box(Size::new(100.0, 100.0), Some(1), vec![]);
242 let b = make_box(Size::new(100.0, 100.0), Some(2), vec![]);
243 let root = make_box(
244 Size::new(800.0, 600.0),
245 Some(0),
246 vec![
247 ChildFragment {
248 offset: Point::new(10.0, 10.0),
249 fragment: a,
250 },
251 ChildFragment {
252 offset: Point::new(10.0, 10.0),
253 fragment: b,
254 },
255 ],
256 );
257 let result = tester(&no_scroll()).test(&root, Point::new(50.0, 50.0));
258 assert_eq!(result.node_index, Some(2));
259 }
260
261 #[test]
262 fn deeply_nested() {
263 let gc = make_box(Size::new(20.0, 20.0), Some(3), vec![]);
264 let child = make_box(
265 Size::new(100.0, 100.0),
266 Some(2),
267 vec![ChildFragment {
268 offset: Point::new(10.0, 10.0),
269 fragment: gc,
270 }],
271 );
272 let root = make_box(
273 Size::new(800.0, 600.0),
274 Some(1),
275 vec![ChildFragment {
276 offset: Point::new(50.0, 50.0),
277 fragment: child,
278 }],
279 );
280 let result = tester(&no_scroll()).test(&root, Point::new(65.0, 65.0));
281 assert_eq!(result.node_index, Some(3));
282 }
283
284 #[test]
285 fn anonymous_box_passthrough() {
286 let anon = Arc::new(Fragment {
287 size: Size::new(100.0, 100.0),
288 kind: FragmentKind::Box(BoxFragmentData::default()),
289 style: None,
290 dom_node: None,
291 });
292 let root = make_box(
293 Size::new(800.0, 600.0),
294 Some(0),
295 vec![ChildFragment {
296 offset: Point::ZERO,
297 fragment: anon,
298 }],
299 );
300 let result = tester(&no_scroll()).test(&root, Point::new(50.0, 50.0));
301 assert_eq!(result.node_index, Some(0));
302 }
303
304 #[test]
305 fn overflow_hidden_clips() {
306 let child = Arc::new(Fragment {
307 size: Size::new(200.0, 200.0),
308 kind: FragmentKind::Box(BoxFragmentData {
309 overflow_x: OverflowClip::Hidden,
310 overflow_y: OverflowClip::Hidden,
311 ..Default::default()
312 }),
313 style: None,
314 dom_node: Some(1),
315 });
316 let root = make_box(
317 Size::new(800.0, 600.0),
318 Some(0),
319 vec![ChildFragment {
320 offset: Point::ZERO,
321 fragment: child,
322 }],
323 );
324 let result = tester(&no_scroll()).test(&root, Point::new(50.0, 50.0));
325 assert_eq!(result.node_index, Some(1));
326 }
327
328 #[test]
329 fn boundary_inclusive_exclusive() {
330 let root = make_box(Size::new(100.0, 100.0), Some(0), vec![]);
331 let offsets = no_scroll();
332 let t = tester(&offsets);
333
334 assert_eq!(t.test(&root, Point::new(0.0, 0.0)).node_index, Some(0));
335 assert_eq!(t.test(&root, Point::new(100.0, 100.0)).node_index, None);
336 assert_eq!(t.test(&root, Point::new(99.9, 99.9)).node_index, Some(0));
337 }
338
339 #[test]
340 fn scroll_offset_adjusts_child_hit() {
341 let child = make_box(Size::new(200.0, 800.0), Some(2), vec![]);
342 let root = Arc::new(Fragment {
343 size: Size::new(200.0, 200.0),
344 kind: FragmentKind::Box(BoxFragmentData {
345 children: vec![ChildFragment {
346 offset: Point::ZERO,
347 fragment: child,
348 }],
349 overflow_y: OverflowClip::Scroll,
350 ..Default::default()
351 }),
352 style: None,
353 dom_node: Some(1),
354 });
355
356 let mut offsets = ScrollOffsets::new();
357 offsets.set_offset(1, Offset::new(0.0, 300.0));
358
359 let result = HitTester::new(&offsets).test(&root, Point::new(100.0, 50.0));
362 assert_eq!(result.node_index, Some(2));
363 }
364}