From e3864f9a9e2ce1baba225761493c7503e43f0212 Mon Sep 17 00:00:00 2001 From: Jelle Raaijmakers Date: Fri, 4 Jul 2025 12:11:41 +0200 Subject: [PATCH] LibWeb: Do not hit test anonymous containers' box for inline content A PaintableWithLines will first try to see if there are any fragments that have a hit. If not, it falls back to hit testing against its border box rect. However, inline content is often hoisted out of its parent into an anonymous container to maintain the invariant that all layout nodes either have inline or block level children. If that's the case, we should not check the border box rect of the anonymous container, because we might trigger a hit too early if the node has previous siblings in its original parent node that overlap with their bounds. By ignoring anonymous nodes, we leave the border box hit testing to the nearest non-anonymous ancestor, which correctly applies the hit testing order to its children. Note that the border box rect checks whether the _untransformed_ point is inside of it, which mirrors the behavior of PaintableBox::hit_test(). --- Libraries/LibWeb/Painting/PaintableBox.cpp | 9 ++++--- ...ine-content-inside-anonymous-container.txt | 12 +++++++++ ...ne-content-inside-anonymous-container.html | 26 +++++++++++++++++++ 3 files changed, 43 insertions(+), 4 deletions(-) create mode 100644 Tests/LibWeb/Text/expected/hit_testing/inline-content-inside-anonymous-container.txt create mode 100644 Tests/LibWeb/Text/input/hit_testing/inline-content-inside-anonymous-container.html diff --git a/Libraries/LibWeb/Painting/PaintableBox.cpp b/Libraries/LibWeb/Painting/PaintableBox.cpp index 4c9cc64fa64..d97937d1730 100644 --- a/Libraries/LibWeb/Painting/PaintableBox.cpp +++ b/Libraries/LibWeb/Painting/PaintableBox.cpp @@ -1185,8 +1185,8 @@ TraversalDecision PaintableWithLines::hit_test(CSSPixelPoint position, HitTestTy if (m_fragments.is_empty() && !has_children() && type == HitTestType::TextCursor - && layout_node_with_style_and_box_metrics().dom_node() - && layout_node_with_style_and_box_metrics().dom_node()->is_editable()) { + && layout_node().dom_node() + && layout_node().dom_node()->is_editable()) { HitTestResult const hit_test_result { .paintable = const_cast(*this), .index_in_node = 0, @@ -1197,7 +1197,7 @@ TraversalDecision PaintableWithLines::hit_test(CSSPixelPoint position, HitTestTy return TraversalDecision::Break; } - if (!layout_node_with_style_and_box_metrics().children_are_inline() || m_fragments.is_empty()) + if (!layout_node().children_are_inline() || m_fragments.is_empty()) return PaintableBox::hit_test(position, type, callback); // NOTE: This CSSPixels -> Float -> CSSPixels conversion is because we can't AffineTransform::map() a CSSPixelPoint. @@ -1278,7 +1278,8 @@ TraversalDecision PaintableWithLines::hit_test(CSSPixelPoint position, HitTestTy } } - if (!stacking_context() && is_visible() && absolute_border_box_rect().contains(transformed_position_adjusted_by_scroll_offset)) { + if (!stacking_context() && is_visible() && !layout_node().is_anonymous() + && absolute_border_box_rect().contains(position_adjusted_by_scroll_offset)) { if (callback(HitTestResult { const_cast(*this) }) == TraversalDecision::Break) return TraversalDecision::Break; } diff --git a/Tests/LibWeb/Text/expected/hit_testing/inline-content-inside-anonymous-container.txt b/Tests/LibWeb/Text/expected/hit_testing/inline-content-inside-anonymous-container.txt new file mode 100644 index 00000000000..bfed174f779 --- /dev/null +++ b/Tests/LibWeb/Text/expected/hit_testing/inline-content-inside-anonymous-container.txt @@ -0,0 +1,12 @@ +<#text> +index: 4 + +--- +<#text> +index: 20 + +--- +