1use std::sync::Arc;
6
7use app_units::Au;
8use embedder_traits::Cursor;
9use euclid::{Box2D, Vector2D};
10use kurbo::{Ellipse, Shape};
11use layout_api::ElementsFromPointResult;
12use rustc_hash::FxHashMap;
13use servo_base::id::ScrollTreeNodeId;
14use servo_geometry::FastLayoutTransform;
15use style::computed_values::backface_visibility::T as BackfaceVisibility;
16use style::computed_values::pointer_events::T as PointerEvents;
17use style::computed_values::visibility::T as Visibility;
18use style::properties::ComputedValues;
19use style::values::computed::ui::CursorKind;
20use webrender_api::BorderRadius;
21use webrender_api::units::{LayoutPoint, LayoutRect, LayoutSize, RectExt};
22
23use crate::display_list::clip::{Clip, ClipId};
24use crate::display_list::paint_traversal::{PaintTraversal, PaintTraversalHandler};
25use crate::display_list::{StackingContext, StackingContextTree, ToWebRender, TraversalState};
26use crate::fragment_tree::{BoxFragmentWithStyle, Fragment, FragmentFlags, TextFragment};
27use crate::geom::PhysicalRect;
28
29pub(crate) struct HitTest<'a> {
30 point_to_test: LayoutPoint,
32 projected_point_to_test: Option<(ScrollTreeNodeId, LayoutPoint, FastLayoutTransform)>,
35 stacking_context_tree: &'a StackingContextTree,
37 results: Vec<ElementsFromPointResult>,
39 clip_hit_test_results: FxHashMap<ClipId, bool>,
41 collected_reference_frame_clips: Vec<ClipId>,
45}
46
47impl<'a> HitTest<'a> {
48 pub(crate) fn run(
49 stacking_context_tree: &'a StackingContextTree,
50 point_to_test: LayoutPoint,
51 ) -> Vec<ElementsFromPointResult> {
52 let mut hit_test = Self {
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 collected_reference_frame_clips: Default::default(),
59 };
60
61 PaintTraversal::traverse(&stacking_context_tree.root_stacking_context, &mut hit_test);
62
63 hit_test.results.reverse();
70
71 hit_test.results
72 }
73
74 fn hit_test_clip_id(&mut self, clip_id: ClipId) -> bool {
77 for index in 0..self.collected_reference_frame_clips.len() {
79 if !self.hit_test_individual_clip_id(self.collected_reference_frame_clips[index]) {
80 return false;
81 }
82 }
83 self.hit_test_individual_clip_id(clip_id)
84 }
85
86 fn hit_test_individual_clip_id(&mut self, clip_id: ClipId) -> bool {
87 if clip_id == ClipId::INVALID {
88 return true;
89 }
90
91 if let Some(result) = self.clip_hit_test_results.get(&clip_id) {
92 return *result;
93 }
94
95 let clip = self.stacking_context_tree.clip_store.get(clip_id);
96 let result = self
97 .location_in_spatial_node(clip.parent_scroll_node_id)
98 .is_some_and(|(point, _)| {
99 clip.contains(point) && self.hit_test_individual_clip_id(clip.parent_clip_id)
100 });
101 self.clip_hit_test_results.insert(clip_id, result);
102 result
103 }
104
105 fn location_in_spatial_node(
109 &mut self,
110 scroll_tree_node_id: ScrollTreeNodeId,
111 ) -> Option<(LayoutPoint, FastLayoutTransform)> {
112 match self.projected_point_to_test {
113 Some((cached_scroll_tree_node_id, projected_point, transform))
114 if cached_scroll_tree_node_id == scroll_tree_node_id =>
115 {
116 return Some((projected_point, transform));
117 },
118 _ => {},
119 }
120
121 let transform = self
122 .stacking_context_tree
123 .paint_info
124 .scroll_tree
125 .cumulative_root_to_node_transform(scroll_tree_node_id)?;
126
127 let projected_point = transform.project_point2d(self.point_to_test)?;
128
129 self.projected_point_to_test = Some((scroll_tree_node_id, projected_point, transform));
130 Some((projected_point, transform))
131 }
132}
133
134impl PaintTraversalHandler for HitTest<'_> {
135 type StackingContextState = bool;
137
138 fn visit_stacking_context(
139 &mut self,
140 stacking_context: &StackingContext,
141 ) -> Self::StackingContextState {
142 if let Some(reference_frame_info) = stacking_context.reference_frame_info.as_ref() &&
143 reference_frame_info.captured_clip_id != ClipId::INVALID
144 {
145 self.collected_reference_frame_clips
146 .push(reference_frame_info.captured_clip_id);
147 return true;
148 }
149 false
150 }
151
152 fn leave_stacking_context(
153 &mut self,
154 _: &TraversalState,
155 pushed_reference_frame_clip: Self::StackingContextState,
156 ) {
157 if pushed_reference_frame_clip {
158 self.collected_reference_frame_clips.pop();
159 }
160 }
161
162 fn visit_box(&mut self, state: &TraversalState, fragment: &BoxFragmentWithStyle<'_>) {
163 Fragment::Box(fragment.box_fragment.clone()).hit_test(state, self);
164 }
165
166 fn visit_text(
167 &mut self,
168 state: &TraversalState,
169 _: PhysicalRect<Au>,
170 fragment: &Arc<TextFragment>,
171 ) {
172 Fragment::Text(fragment.clone()).hit_test(state, self);
173 }
174}
175
176impl Clip {
177 fn contains(&self, point: LayoutPoint) -> bool {
178 rounded_rect_contains_point(self.rect, &self.radii, point)
179 }
180}
181
182impl Fragment {
183 pub(crate) fn hit_test(&self, state: &TraversalState, hit_test: &mut HitTest) -> bool {
184 let Some(tag) = self.tag() else {
185 return false;
186 };
187 if !hit_test.hit_test_clip_id(state.clip_id) {
188 return false;
189 }
190
191 let mut hit_test_fragment_inner =
192 |style: &ComputedValues,
193 fragment_rect: PhysicalRect<Au>,
194 border_radius: BorderRadius,
195 fragment_flags: FragmentFlags,
196 auto_cursor: Cursor| {
197 let is_root_element = fragment_flags.contains(FragmentFlags::IS_ROOT_ELEMENT);
198
199 if !is_root_element {
200 if style.get_inherited_ui().pointer_events == PointerEvents::None {
201 return false;
202 }
203 if style.get_inherited_box().visibility != Visibility::Visible {
204 return false;
205 }
206 }
207
208 let (point_in_spatial_node, transform) =
209 match hit_test.location_in_spatial_node(state.spatial_id) {
210 Some(point) => point,
211 None => return false,
212 };
213
214 if !is_root_element &&
215 style.get_box().backface_visibility == BackfaceVisibility::Hidden &&
216 transform.is_backface_visible()
217 {
218 return false;
219 }
220
221 let fragment_rect = fragment_rect.translate(state.origin.to_vector());
222 if is_root_element {
223 let viewport_size = hit_test
224 .stacking_context_tree
225 .paint_info
226 .viewport_details
227 .size;
228 let viewport_rect = LayoutRect::from_origin_and_size(
229 Default::default(),
230 viewport_size.cast_unit(),
231 );
232 if !viewport_rect.contains(hit_test.point_to_test) {
233 return false;
234 }
235 } else if !rounded_rect_contains_point(
236 fragment_rect.to_webrender(),
237 &border_radius,
238 point_in_spatial_node,
239 ) {
240 return false;
241 }
242
243 let point_in_target = point_in_spatial_node.cast_unit() -
244 Vector2D::new(
245 fragment_rect.origin.x.to_f32_px(),
246 fragment_rect.origin.y.to_f32_px(),
247 );
248
249 hit_test.results.push(ElementsFromPointResult {
250 node: tag.node,
251 point_in_target,
252 cursor: cursor(style.get_inherited_ui().cursor.keyword, auto_cursor),
253 });
254
255 false
261 };
262
263 match self {
264 Fragment::LayoutRoot(layout_root_fragment) => {
265 layout_root_fragment.inner().hit_test(state, hit_test)
266 },
267 Fragment::Box(box_fragment) | Fragment::Float(box_fragment) => hit_test_fragment_inner(
268 &box_fragment.style(),
269 box_fragment.border_rect(),
270 box_fragment.border_radius(),
271 box_fragment.base.flags,
272 Cursor::Default,
273 ),
274 Fragment::Text(text) => hit_test_fragment_inner(
275 &text.base.style(),
276 text.base.rect(),
277 BorderRadius::zero(),
278 FragmentFlags::empty(),
279 Cursor::Text,
280 ),
281 _ => false,
282 }
283 }
284}
285
286fn rounded_rect_contains_point(
287 rect: LayoutRect,
288 border_radius: &BorderRadius,
289 point: LayoutPoint,
290) -> bool {
291 if !rect.contains(point) {
292 return false;
293 }
294
295 if border_radius.is_zero() {
296 return true;
297 }
298
299 let check_corner = |corner: LayoutPoint, radius: &LayoutSize, is_right, is_bottom| {
300 let mut origin = corner;
301 if is_right {
302 origin.x -= radius.width;
303 }
304 if is_bottom {
305 origin.y -= radius.height;
306 }
307 if !Box2D::from_origin_and_size(origin, *radius).contains(point) {
308 return true;
309 }
310 let center = (
311 if is_right {
312 corner.x - radius.width
313 } else {
314 corner.x + radius.width
315 },
316 if is_bottom {
317 corner.y - radius.height
318 } else {
319 corner.y + radius.height
320 },
321 );
322 let radius = (radius.width as f64, radius.height as f64);
323 Ellipse::new(center, radius, 0.0).contains((point.x, point.y).into())
324 };
325
326 check_corner(rect.top_left(), &border_radius.top_left, false, false) &&
327 check_corner(rect.top_right(), &border_radius.top_right, true, false) &&
328 check_corner(rect.bottom_right(), &border_radius.bottom_right, true, true) &&
329 check_corner(rect.bottom_left(), &border_radius.bottom_left, false, true)
330}
331
332fn cursor(kind: CursorKind, auto_cursor: Cursor) -> Cursor {
333 match kind {
334 CursorKind::Auto => auto_cursor,
335 CursorKind::None => Cursor::None,
336 CursorKind::Default => Cursor::Default,
337 CursorKind::Pointer => Cursor::Pointer,
338 CursorKind::ContextMenu => Cursor::ContextMenu,
339 CursorKind::Help => Cursor::Help,
340 CursorKind::Progress => Cursor::Progress,
341 CursorKind::Wait => Cursor::Wait,
342 CursorKind::Cell => Cursor::Cell,
343 CursorKind::Crosshair => Cursor::Crosshair,
344 CursorKind::Text => Cursor::Text,
345 CursorKind::VerticalText => Cursor::VerticalText,
346 CursorKind::Alias => Cursor::Alias,
347 CursorKind::Copy => Cursor::Copy,
348 CursorKind::Move => Cursor::Move,
349 CursorKind::NoDrop => Cursor::NoDrop,
350 CursorKind::NotAllowed => Cursor::NotAllowed,
351 CursorKind::Grab => Cursor::Grab,
352 CursorKind::Grabbing => Cursor::Grabbing,
353 CursorKind::EResize => Cursor::EResize,
354 CursorKind::NResize => Cursor::NResize,
355 CursorKind::NeResize => Cursor::NeResize,
356 CursorKind::NwResize => Cursor::NwResize,
357 CursorKind::SResize => Cursor::SResize,
358 CursorKind::SeResize => Cursor::SeResize,
359 CursorKind::SwResize => Cursor::SwResize,
360 CursorKind::WResize => Cursor::WResize,
361 CursorKind::EwResize => Cursor::EwResize,
362 CursorKind::NsResize => Cursor::NsResize,
363 CursorKind::NeswResize => Cursor::NeswResize,
364 CursorKind::NwseResize => Cursor::NwseResize,
365 CursorKind::ColResize => Cursor::ColResize,
366 CursorKind::RowResize => Cursor::RowResize,
367 CursorKind::AllScroll => Cursor::AllScroll,
368 CursorKind::ZoomIn => Cursor::ZoomIn,
369 CursorKind::ZoomOut => Cursor::ZoomOut,
370 }
371}