1use std::collections::HashMap;
6
7use app_units::Au;
8use base::id::ScrollTreeNodeId;
9use embedder_traits::Cursor;
10use euclid::{Box2D, Vector2D};
11use kurbo::{Ellipse, Shape};
12use layout_api::{ElementsFromPointFlags, ElementsFromPointResult};
13use servo_geometry::FastLayoutTransform;
14use style::computed_values::backface_visibility::T as BackfaceVisibility;
15use style::computed_values::pointer_events::T as PointerEvents;
16use style::computed_values::visibility::T as Visibility;
17use style::properties::ComputedValues;
18use style::values::computed::ui::CursorKind;
19use webrender_api::BorderRadius;
20use webrender_api::units::{LayoutPoint, LayoutRect, LayoutSize, RectExt};
21
22use crate::display_list::clip::{Clip, ClipId};
23use crate::display_list::stacking_context::StackingContextSection;
24use crate::display_list::{
25 StackingContext, StackingContextContent, StackingContextTree, ToWebRender,
26};
27use crate::fragment_tree::{Fragment, FragmentFlags};
28use crate::geom::PhysicalRect;
29
30pub(crate) struct HitTest<'a> {
31 flags: ElementsFromPointFlags,
33 point_to_test: LayoutPoint,
35 projected_point_to_test: Option<(ScrollTreeNodeId, LayoutPoint, FastLayoutTransform)>,
38 stacking_context_tree: &'a StackingContextTree,
40 results: Vec<ElementsFromPointResult>,
42 clip_hit_test_results: HashMap<ClipId, bool>,
44}
45
46impl<'a> HitTest<'a> {
47 pub(crate) fn run(
48 stacking_context_tree: &'a StackingContextTree,
49 point_to_test: LayoutPoint,
50 flags: ElementsFromPointFlags,
51 ) -> Vec<ElementsFromPointResult> {
52 let mut hit_test = Self {
53 flags,
54 point_to_test,
55 projected_point_to_test: None,
56 stacking_context_tree,
57 results: Vec::new(),
58 clip_hit_test_results: HashMap::new(),
59 };
60 stacking_context_tree
61 .root_stacking_context
62 .hit_test(&mut hit_test);
63 hit_test.results
64 }
65
66 fn hit_test_clip_id(&mut self, clip_id: ClipId) -> bool {
69 if clip_id == ClipId::INVALID {
70 return true;
71 }
72
73 if let Some(result) = self.clip_hit_test_results.get(&clip_id) {
74 return *result;
75 }
76
77 let clip = self.stacking_context_tree.clip_store.get(clip_id);
78 let result = self
79 .location_in_spatial_node(clip.parent_scroll_node_id)
80 .is_some_and(|(point, _)| {
81 clip.contains(point) && self.hit_test_clip_id(clip.parent_clip_id)
82 });
83 self.clip_hit_test_results.insert(clip_id, result);
84 result
85 }
86
87 fn location_in_spatial_node(
91 &mut self,
92 scroll_tree_node_id: ScrollTreeNodeId,
93 ) -> Option<(LayoutPoint, FastLayoutTransform)> {
94 match self.projected_point_to_test {
95 Some((cached_scroll_tree_node_id, projected_point, transform))
96 if cached_scroll_tree_node_id == scroll_tree_node_id =>
97 {
98 return Some((projected_point, transform));
99 },
100 _ => {},
101 }
102
103 let transform = self
104 .stacking_context_tree
105 .compositor_info
106 .scroll_tree
107 .cumulative_root_to_node_transform(&scroll_tree_node_id)?;
108
109 let projected_point = transform.project_point2d(self.point_to_test)?;
110
111 self.projected_point_to_test = Some((scroll_tree_node_id, projected_point, transform));
112 Some((projected_point, transform))
113 }
114}
115
116impl Clip {
117 fn contains(&self, point: LayoutPoint) -> bool {
118 rounded_rect_contains_point(self.rect, &self.radii, point)
119 }
120}
121
122impl StackingContext {
123 fn hit_test(&self, hit_test: &mut HitTest) -> bool {
127 let mut contents = self.contents.iter().rev().peekable();
128
129 while contents
131 .peek()
132 .is_some_and(|child| child.section() == StackingContextSection::Outline)
133 {
134 let _ = contents.next().unwrap();
136 }
137
138 let mut real_stacking_contexts_and_positioned_stacking_containers = self
141 .real_stacking_contexts_and_positioned_stacking_containers
142 .iter()
143 .rev()
144 .peekable();
145 while real_stacking_contexts_and_positioned_stacking_containers
146 .peek()
147 .is_some_and(|child| child.z_index() >= 0)
148 {
149 let child = real_stacking_contexts_and_positioned_stacking_containers
150 .next()
151 .unwrap();
152 if child.hit_test(hit_test) {
153 return true;
154 }
155 }
156
157 while contents
159 .peek()
160 .is_some_and(|child| child.section() == StackingContextSection::Foreground)
161 {
162 let child = contents.next().unwrap();
163 if self.hit_test_content(child, hit_test) {
164 return true;
165 }
166 }
167
168 for child in self.float_stacking_containers.iter().rev() {
170 if child.hit_test(hit_test) {
171 return true;
172 }
173 }
174
175 while contents.peek().is_some_and(|child| {
177 child.section() == StackingContextSection::DescendantBackgroundsAndBorders
178 }) {
179 let child = contents.next().unwrap();
180 if self.hit_test_content(child, hit_test) {
181 return true;
182 }
183 }
184
185 for child in real_stacking_contexts_and_positioned_stacking_containers {
187 if child.hit_test(hit_test) {
188 return true;
189 }
190 }
191
192 while contents.peek().is_some_and(|child| {
194 child.section() == StackingContextSection::OwnBackgroundsAndBorders
195 }) {
196 let child = contents.next().unwrap();
197 if self.hit_test_content(child, hit_test) {
198 return true;
199 }
200 }
201 false
202 }
203
204 pub(crate) fn hit_test_content(
205 &self,
206 content: &StackingContextContent,
207 hit_test: &mut HitTest<'_>,
208 ) -> bool {
209 match content {
210 StackingContextContent::Fragment {
211 scroll_node_id,
212 clip_id,
213 containing_block,
214 fragment,
215 ..
216 } => {
217 hit_test.hit_test_clip_id(*clip_id) &&
218 fragment.hit_test(hit_test, *scroll_node_id, containing_block)
219 },
220 StackingContextContent::AtomicInlineStackingContainer { index } => {
221 self.atomic_inline_stacking_containers[*index].hit_test(hit_test)
222 },
223 }
224 }
225}
226
227impl Fragment {
228 pub(crate) fn hit_test(
229 &self,
230 hit_test: &mut HitTest,
231 spatial_node_id: ScrollTreeNodeId,
232 containing_block: &PhysicalRect<Au>,
233 ) -> bool {
234 let Some(tag) = self.tag() else {
235 return false;
236 };
237
238 let mut hit_test_fragment_inner =
239 |style: &ComputedValues,
240 fragment_rect: PhysicalRect<Au>,
241 border_radius: BorderRadius,
242 fragment_flags: FragmentFlags,
243 auto_cursor: Cursor| {
244 let is_root_element = fragment_flags.contains(FragmentFlags::IS_ROOT_ELEMENT);
245
246 if !is_root_element {
247 if style.get_inherited_ui().pointer_events == PointerEvents::None {
248 return false;
249 }
250 if style.get_inherited_box().visibility != Visibility::Visible {
251 return false;
252 }
253 }
254
255 let (point_in_spatial_node, transform) =
256 match hit_test.location_in_spatial_node(spatial_node_id) {
257 Some(point) => point,
258 None => return false,
259 };
260
261 if !is_root_element &&
262 style.get_box().backface_visibility == BackfaceVisibility::Hidden &&
263 transform.is_backface_visible()
264 {
265 return false;
266 }
267
268 let fragment_rect = fragment_rect.translate(containing_block.origin.to_vector());
269 if is_root_element {
270 let viewport_size = hit_test
271 .stacking_context_tree
272 .compositor_info
273 .viewport_details
274 .size;
275 let viewport_rect = LayoutRect::from_origin_and_size(
276 Default::default(),
277 viewport_size.cast_unit(),
278 );
279 if !viewport_rect.contains(hit_test.point_to_test) {
280 return false;
281 }
282 } else if !rounded_rect_contains_point(
283 fragment_rect.to_webrender(),
284 &border_radius,
285 point_in_spatial_node,
286 ) {
287 return false;
288 }
289
290 let point_in_target = point_in_spatial_node.cast_unit() -
291 Vector2D::new(
292 fragment_rect.origin.x.to_f32_px(),
293 fragment_rect.origin.y.to_f32_px(),
294 );
295
296 hit_test.results.push(ElementsFromPointResult {
297 node: tag.node,
298 point_in_target,
299 cursor: cursor(style.get_inherited_ui().cursor.keyword, auto_cursor),
300 });
301 !hit_test.flags.contains(ElementsFromPointFlags::FindAll)
302 };
303
304 match self {
305 Fragment::Box(box_fragment) | Fragment::Float(box_fragment) => {
306 let box_fragment = box_fragment.borrow();
307 hit_test_fragment_inner(
308 &box_fragment.style,
309 box_fragment.border_rect(),
310 box_fragment.border_radius(),
311 box_fragment.base.flags,
312 Cursor::Default,
313 )
314 },
315 Fragment::Text(text) => {
316 let text = &*text.borrow();
317 hit_test_fragment_inner(
318 &text.inline_styles.style.borrow(),
319 text.rect,
320 BorderRadius::zero(),
321 FragmentFlags::empty(),
322 Cursor::Text,
323 )
324 },
325 _ => false,
326 }
327 }
328}
329
330fn rounded_rect_contains_point(
331 rect: LayoutRect,
332 border_radius: &BorderRadius,
333 point: LayoutPoint,
334) -> bool {
335 if !rect.contains(point) {
336 return false;
337 }
338
339 if border_radius.is_zero() {
340 return true;
341 }
342
343 let check_corner = |corner: LayoutPoint, radius: &LayoutSize, is_right, is_bottom| {
344 let mut origin = corner;
345 if is_right {
346 origin.x -= radius.width;
347 }
348 if is_bottom {
349 origin.y -= radius.height;
350 }
351 if !Box2D::from_origin_and_size(origin, *radius).contains(point) {
352 return true;
353 }
354 let center = (
355 if is_right {
356 corner.x - radius.width
357 } else {
358 corner.x + radius.width
359 },
360 if is_bottom {
361 corner.y - radius.height
362 } else {
363 corner.y + radius.height
364 },
365 );
366 let radius = (radius.width as f64, radius.height as f64);
367 Ellipse::new(center, radius, 0.0).contains((point.x, point.y).into())
368 };
369
370 check_corner(rect.top_left(), &border_radius.top_left, false, false) &&
371 check_corner(rect.top_right(), &border_radius.top_right, true, false) &&
372 check_corner(rect.bottom_right(), &border_radius.bottom_right, true, true) &&
373 check_corner(rect.bottom_left(), &border_radius.bottom_left, false, true)
374}
375
376fn cursor(kind: CursorKind, auto_cursor: Cursor) -> Cursor {
377 match kind {
378 CursorKind::Auto => auto_cursor,
379 CursorKind::None => Cursor::None,
380 CursorKind::Default => Cursor::Default,
381 CursorKind::Pointer => Cursor::Pointer,
382 CursorKind::ContextMenu => Cursor::ContextMenu,
383 CursorKind::Help => Cursor::Help,
384 CursorKind::Progress => Cursor::Progress,
385 CursorKind::Wait => Cursor::Wait,
386 CursorKind::Cell => Cursor::Cell,
387 CursorKind::Crosshair => Cursor::Crosshair,
388 CursorKind::Text => Cursor::Text,
389 CursorKind::VerticalText => Cursor::VerticalText,
390 CursorKind::Alias => Cursor::Alias,
391 CursorKind::Copy => Cursor::Copy,
392 CursorKind::Move => Cursor::Move,
393 CursorKind::NoDrop => Cursor::NoDrop,
394 CursorKind::NotAllowed => Cursor::NotAllowed,
395 CursorKind::Grab => Cursor::Grab,
396 CursorKind::Grabbing => Cursor::Grabbing,
397 CursorKind::EResize => Cursor::EResize,
398 CursorKind::NResize => Cursor::NResize,
399 CursorKind::NeResize => Cursor::NeResize,
400 CursorKind::NwResize => Cursor::NwResize,
401 CursorKind::SResize => Cursor::SResize,
402 CursorKind::SeResize => Cursor::SeResize,
403 CursorKind::SwResize => Cursor::SwResize,
404 CursorKind::WResize => Cursor::WResize,
405 CursorKind::EwResize => Cursor::EwResize,
406 CursorKind::NsResize => Cursor::NsResize,
407 CursorKind::NeswResize => Cursor::NeswResize,
408 CursorKind::NwseResize => Cursor::NwseResize,
409 CursorKind::ColResize => Cursor::ColResize,
410 CursorKind::RowResize => Cursor::RowResize,
411 CursorKind::AllScroll => Cursor::AllScroll,
412 CursorKind::ZoomIn => Cursor::ZoomIn,
413 CursorKind::ZoomOut => Cursor::ZoomOut,
414 }
415}