1use std::sync::Arc;
6
7use app_units::Au;
8use embedder_traits::Cursor;
9use euclid::{Box2D, Point2D, Vector2D};
10use kurbo::{Ellipse, Shape};
11use layout_api::{HitTestFlags, HitTestResult, HitTestResultItem};
12use rustc_hash::FxHashMap;
13use servo_base::id::ScrollTreeNodeId;
14use servo_base::text::{Utf32CodeUnits, Utf32CodeUnitsOrNodeOffset};
15use servo_geometry::FastLayoutTransform;
16use style::computed_values::backface_visibility::T as BackfaceVisibility;
17use style::computed_values::pointer_events::T as PointerEvents;
18use style::computed_values::visibility::T as Visibility;
19use style::dom::OpaqueNode;
20use style::properties::ComputedValues;
21use style::values::computed::ui::CursorKind;
22use style_traits::CSSPixel;
23use webrender_api::BorderRadius;
24use webrender_api::units::{LayoutPoint, LayoutRect, LayoutSize, RectExt};
25
26use crate::display_list::clip::{Clip, ClipId};
27use crate::display_list::paint_traversal::{PaintTraversal, PaintTraversalHandler};
28use crate::display_list::{StackingContext, StackingContextTree, ToWebRender, TraversalState};
29use crate::fragment_tree::{BoxFragmentWithStyle, Fragment, FragmentFlags, TextFragment};
30use crate::geom::PhysicalRect;
31
32struct DomPositionCandidate {
33 fragment: Fragment,
34 node: OpaqueNode,
35 point_in_target: Point2D<f32, CSSPixel>,
36}
37
38pub(crate) struct HitTest<'a> {
39 flags: HitTestFlags,
41 point_to_test: LayoutPoint,
43 projected_point_to_test: Option<(ScrollTreeNodeId, LayoutPoint, FastLayoutTransform)>,
46 stacking_context_tree: &'a StackingContextTree,
48 items: Vec<HitTestResultItem>,
50 dom_position_candidate: Option<DomPositionCandidate>,
52 clip_hit_test_results: FxHashMap<ClipId, bool>,
54 collected_reference_frame_clips: Vec<ClipId>,
58}
59
60impl<'a> HitTest<'a> {
61 pub(crate) fn run(
62 flags: HitTestFlags,
63 stacking_context_tree: &'a StackingContextTree,
64 point_to_test: LayoutPoint,
65 ) -> HitTestResult {
66 let mut hit_test = Self {
67 flags,
68 point_to_test,
69 projected_point_to_test: None,
70 stacking_context_tree,
71 items: Vec::new(),
72 dom_position_candidate: None,
73 clip_hit_test_results: FxHashMap::default(),
74 collected_reference_frame_clips: Default::default(),
75 };
76
77 PaintTraversal::traverse(&stacking_context_tree.root_stacking_context, &mut hit_test);
78
79 hit_test.items.reverse();
86
87 let dom_position_for_selection = if flags.contains(HitTestFlags::IncludeDomPosition) {
88 hit_test
89 .dom_position()
90 .map(|(node, offset)| (node, Utf32CodeUnitsOrNodeOffset(offset.0)))
91 } else {
92 None
93 };
94
95 HitTestResult {
96 dom_position_for_selection,
97 items: hit_test.items,
98 }
99 }
100
101 fn dom_position(&self) -> Option<(OpaqueNode, Utf32CodeUnits)> {
102 let hit = self.dom_position_candidate.as_ref()?;
103 if let Fragment::Text(text_fragment) = &hit.fragment {
104 let character_offset =
105 text_fragment.character_offset(hit.point_in_target.map(Au::from_f32_px))?;
106 return Some((hit.node, character_offset));
107 }
108
109 let mut search = ClosestFragmentSearch::default();
110 if let Some(point_in_fragment) = self.stacking_context_tree.offset_in_fragment(
111 &hit.fragment,
112 self.point_to_test.map(Au::from_f32_px).cast_unit(),
113 ) {
114 search.collect_relevant_children(&hit.fragment, point_in_fragment);
115 }
116 search.into_dom_position()
117 }
118
119 fn hit_test_clip_id(&mut self, clip_id: ClipId) -> bool {
122 for index in 0..self.collected_reference_frame_clips.len() {
124 if !self.hit_test_individual_clip_id(self.collected_reference_frame_clips[index]) {
125 return false;
126 }
127 }
128 self.hit_test_individual_clip_id(clip_id)
129 }
130
131 fn hit_test_individual_clip_id(&mut self, clip_id: ClipId) -> bool {
132 if clip_id == ClipId::INVALID {
133 return true;
134 }
135
136 if let Some(result) = self.clip_hit_test_results.get(&clip_id) {
137 return *result;
138 }
139
140 let clip = self.stacking_context_tree.clip_store.get(clip_id);
141 let result = self
142 .location_in_spatial_node(clip.parent_scroll_node_id)
143 .is_some_and(|(point, _)| {
144 clip.contains(point) && self.hit_test_individual_clip_id(clip.parent_clip_id)
145 });
146 self.clip_hit_test_results.insert(clip_id, result);
147 result
148 }
149
150 fn location_in_spatial_node(
154 &mut self,
155 scroll_tree_node_id: ScrollTreeNodeId,
156 ) -> Option<(LayoutPoint, FastLayoutTransform)> {
157 match self.projected_point_to_test {
158 Some((cached_scroll_tree_node_id, projected_point, transform))
159 if cached_scroll_tree_node_id == scroll_tree_node_id =>
160 {
161 return Some((projected_point, transform));
162 },
163 _ => {},
164 }
165
166 let transform = self
167 .stacking_context_tree
168 .paint_info
169 .scroll_tree
170 .cumulative_root_to_node_transform(scroll_tree_node_id)?;
171
172 let projected_point = transform.project_point2d(self.point_to_test)?;
173
174 self.projected_point_to_test = Some((scroll_tree_node_id, projected_point, transform));
175 Some((projected_point, transform))
176 }
177}
178
179impl PaintTraversalHandler for HitTest<'_> {
180 type StackingContextState = bool;
182
183 fn visit_stacking_context(
184 &mut self,
185 stacking_context: &StackingContext,
186 ) -> Self::StackingContextState {
187 if let Some(reference_frame_info) = stacking_context.reference_frame_info.as_ref() &&
188 reference_frame_info.captured_clip_id != ClipId::INVALID
189 {
190 self.collected_reference_frame_clips
191 .push(reference_frame_info.captured_clip_id);
192 return true;
193 }
194 false
195 }
196
197 fn leave_stacking_context(
198 &mut self,
199 _: &TraversalState,
200 pushed_reference_frame_clip: Self::StackingContextState,
201 ) {
202 if pushed_reference_frame_clip {
203 self.collected_reference_frame_clips.pop();
204 }
205 }
206
207 fn visit_box(&mut self, state: &TraversalState, fragment: &BoxFragmentWithStyle<'_>) {
208 Fragment::Box(fragment.box_fragment.clone()).hit_test(state, self);
209 }
210
211 fn visit_text(
212 &mut self,
213 state: &TraversalState,
214 _: PhysicalRect<Au>,
215 fragment: &Arc<TextFragment>,
216 ) {
217 Fragment::Text(fragment.clone()).hit_test(state, self);
218 }
219}
220
221impl Clip {
222 fn contains(&self, point: LayoutPoint) -> bool {
223 rounded_rect_contains_point(self.rect, &self.radii, point)
224 }
225}
226
227impl Fragment {
228 pub(crate) fn hit_test(&self, state: &TraversalState, hit_test: &mut HitTest) -> bool {
229 let Some(tag) = self.tag() else {
230 return false;
231 };
232 if !hit_test.hit_test_clip_id(state.clip_id) {
233 return false;
234 }
235
236 let mut hit_test_fragment_inner =
237 |style: &ComputedValues,
238 fragment_rect: PhysicalRect<Au>,
239 border_radius: BorderRadius,
240 fragment_flags: FragmentFlags,
241 auto_cursor: Cursor| {
242 let is_root_element = fragment_flags.contains(FragmentFlags::IS_ROOT_ELEMENT);
243
244 if !is_root_element {
245 if style.get_inherited_ui().pointer_events == PointerEvents::None {
246 return false;
247 }
248 if style.get_inherited_box().visibility != Visibility::Visible {
249 return false;
250 }
251 }
252
253 let (point_in_spatial_node, transform) =
254 match hit_test.location_in_spatial_node(state.spatial_id) {
255 Some(point) => point,
256 None => return false,
257 };
258
259 if !is_root_element &&
260 style.get_box().backface_visibility == BackfaceVisibility::Hidden &&
261 transform.is_backface_visible()
262 {
263 return false;
264 }
265
266 let fragment_rect = fragment_rect.translate(state.origin.to_vector());
267 if is_root_element {
268 let viewport_size = hit_test
269 .stacking_context_tree
270 .paint_info
271 .viewport_details
272 .size;
273 let viewport_rect = LayoutRect::from_origin_and_size(
274 Default::default(),
275 viewport_size.cast_unit(),
276 );
277 if !viewport_rect.contains(hit_test.point_to_test) {
278 return false;
279 }
280 } else if !rounded_rect_contains_point(
281 fragment_rect.to_webrender(),
282 &border_radius,
283 point_in_spatial_node,
284 ) {
285 return false;
286 }
287
288 let point_in_target = point_in_spatial_node.cast_unit() -
289 Vector2D::new(
290 fragment_rect.origin.x.to_f32_px(),
291 fragment_rect.origin.y.to_f32_px(),
292 );
293
294 hit_test.items.push(HitTestResultItem {
295 node: tag.node,
296 point_in_target,
297 cursor: cursor(style.get_inherited_ui().cursor.keyword, auto_cursor),
298 });
299
300 if hit_test.flags.intersects(HitTestFlags::IncludeDomPosition) &&
303 tag.pseudo_element_chain.is_empty()
304 {
305 hit_test.dom_position_candidate = Some(DomPositionCandidate {
306 fragment: self.clone(),
307 node: tag.node,
308 point_in_target,
309 });
310 }
311
312 false
318 };
319
320 match self {
321 Fragment::LayoutRoot(layout_root_fragment) => {
322 layout_root_fragment.inner().hit_test(state, hit_test)
323 },
324 Fragment::Box(box_fragment) | Fragment::Float(box_fragment) => hit_test_fragment_inner(
325 &box_fragment.style(),
326 box_fragment.border_rect(),
327 box_fragment.border_radius(),
328 box_fragment.base.flags,
329 Cursor::Default,
330 ),
331 Fragment::Text(text) => hit_test_fragment_inner(
332 &text.style(),
333 text.base.rect(),
334 BorderRadius::zero(),
335 FragmentFlags::empty(),
336 Cursor::Text,
337 ),
338 _ => false,
339 }
340 }
341}
342
343fn rounded_rect_contains_point(
344 rect: LayoutRect,
345 border_radius: &BorderRadius,
346 point: LayoutPoint,
347) -> bool {
348 if !rect.contains(point) {
349 return false;
350 }
351
352 if border_radius.is_zero() {
353 return true;
354 }
355
356 let check_corner = |corner: LayoutPoint, radius: &LayoutSize, is_right, is_bottom| {
357 let mut origin = corner;
358 if is_right {
359 origin.x -= radius.width;
360 }
361 if is_bottom {
362 origin.y -= radius.height;
363 }
364 if !Box2D::from_origin_and_size(origin, *radius).contains(point) {
365 return true;
366 }
367 let center = (
368 if is_right {
369 corner.x - radius.width
370 } else {
371 corner.x + radius.width
372 },
373 if is_bottom {
374 corner.y - radius.height
375 } else {
376 corner.y + radius.height
377 },
378 );
379 let radius = (radius.width as f64, radius.height as f64);
380 Ellipse::new(center, radius, 0.0).contains((point.x, point.y).into())
381 };
382
383 check_corner(rect.top_left(), &border_radius.top_left, false, false) &&
384 check_corner(rect.top_right(), &border_radius.top_right, true, false) &&
385 check_corner(rect.bottom_right(), &border_radius.bottom_right, true, true) &&
386 check_corner(rect.bottom_left(), &border_radius.bottom_left, false, true)
387}
388
389fn cursor(kind: CursorKind, auto_cursor: Cursor) -> Cursor {
390 match kind {
391 CursorKind::Auto => auto_cursor,
392 CursorKind::None => Cursor::None,
393 CursorKind::Default => Cursor::Default,
394 CursorKind::Pointer => Cursor::Pointer,
395 CursorKind::ContextMenu => Cursor::ContextMenu,
396 CursorKind::Help => Cursor::Help,
397 CursorKind::Progress => Cursor::Progress,
398 CursorKind::Wait => Cursor::Wait,
399 CursorKind::Cell => Cursor::Cell,
400 CursorKind::Crosshair => Cursor::Crosshair,
401 CursorKind::Text => Cursor::Text,
402 CursorKind::VerticalText => Cursor::VerticalText,
403 CursorKind::Alias => Cursor::Alias,
404 CursorKind::Copy => Cursor::Copy,
405 CursorKind::Move => Cursor::Move,
406 CursorKind::NoDrop => Cursor::NoDrop,
407 CursorKind::NotAllowed => Cursor::NotAllowed,
408 CursorKind::Grab => Cursor::Grab,
409 CursorKind::Grabbing => Cursor::Grabbing,
410 CursorKind::EResize => Cursor::EResize,
411 CursorKind::NResize => Cursor::NResize,
412 CursorKind::NeResize => Cursor::NeResize,
413 CursorKind::NwResize => Cursor::NwResize,
414 CursorKind::SResize => Cursor::SResize,
415 CursorKind::SeResize => Cursor::SeResize,
416 CursorKind::SwResize => Cursor::SwResize,
417 CursorKind::WResize => Cursor::WResize,
418 CursorKind::EwResize => Cursor::EwResize,
419 CursorKind::NsResize => Cursor::NsResize,
420 CursorKind::NeswResize => Cursor::NeswResize,
421 CursorKind::NwseResize => Cursor::NwseResize,
422 CursorKind::ColResize => Cursor::ColResize,
423 CursorKind::RowResize => Cursor::RowResize,
424 CursorKind::AllScroll => Cursor::AllScroll,
425 CursorKind::ZoomIn => Cursor::ZoomIn,
426 CursorKind::ZoomOut => Cursor::ZoomOut,
427 }
428}
429
430pub(crate) struct ClosestFragment {
431 fragment: Arc<TextFragment>,
432 node: OpaqueNode,
433 point_in_fragment: Point2D<Au, CSSPixel>,
434 distance: Au,
435 point_in_vertical_bounds: bool,
436}
437
438impl ClosestFragment {
439 fn should_replace(&self, new_distance: Au, point_in_vertical_bounds: bool) -> bool {
440 if point_in_vertical_bounds && !self.point_in_vertical_bounds {
441 return true;
442 }
443 if self.point_in_vertical_bounds && !point_in_vertical_bounds {
444 return false;
445 }
446 new_distance <= self.distance
447 }
448
449 pub(crate) fn dom_position(&self) -> Option<(OpaqueNode, Utf32CodeUnits)> {
450 let character_offset = self.fragment.character_offset(self.point_in_fragment)?;
451 Some((self.node, character_offset))
452 }
453}
454
455#[derive(Default)]
456pub(crate) struct ClosestFragmentSearch {
457 closest: Option<ClosestFragment>,
458}
459
460impl ClosestFragmentSearch {
461 pub(crate) fn into_dom_position(self) -> Option<(OpaqueNode, Utf32CodeUnits)> {
462 self.closest?.dom_position()
463 }
464
465 fn maybe_update(&mut self, fragment: &Fragment, point_in_fragment: Point2D<Au, CSSPixel>) {
466 let Fragment::Text(text_fragment) = fragment else {
467 return;
468 };
469
470 let (distance, point_in_vertical_bounds) = {
471 (
472 text_fragment.distance_to_point_for_glyph_offset(point_in_fragment),
473 text_fragment.point_is_within_vertical_boundaries(point_in_fragment),
474 )
475 };
476
477 if let Some(tag) = text_fragment.base.tag.as_ref() &&
478 tag.pseudo_element_chain.is_empty() &&
479 self.closest.as_ref().is_none_or(|closest_fragment| {
480 closest_fragment.should_replace(distance, point_in_vertical_bounds)
481 })
482 {
483 self.closest = Some(ClosestFragment {
484 fragment: text_fragment.clone(),
485 node: tag.node,
486 point_in_fragment,
487 distance,
488 point_in_vertical_bounds,
489 });
490 }
491 }
492
493 pub(crate) fn collect_relevant_children(
494 &mut self,
495 fragment: &Fragment,
496 point_in_fragment: Point2D<Au, CSSPixel>,
497 ) {
498 self.maybe_update(fragment, point_in_fragment);
499 if let Some(children) = fragment.children() {
500 for child in children.iter() {
501 let offset = child
502 .base()
503 .map(|base| base.rect().origin)
504 .unwrap_or_default();
505 let point = point_in_fragment - offset.to_vector();
506 self.collect_relevant_children(child, point);
507 }
508 }
509 }
510}