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