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