Skip to content

Commit

Permalink
LibWeb: Handle continuation chain during hit-testing instead of after
Browse files Browse the repository at this point in the history
My previous attempt at resolving the continuation chain tried to resolve
`pointer-events: none` by repeatedly falling back to the parent
paintable until one was found that _would_ want to handle pointer
events. But since we were no longer performing hit-tests on those
paintables, false positives could pop up. This could happen for
out-of-flow block elements that did not overlap with their parent rects,
for example.

This approach works much better since it only handles the continuation
case that's relevant (the "middle" anonymous box) and it does so during
hit-testing instead of after, allowing all the other relevant logic to
come into play.
  • Loading branch information
gmta committed Jan 25, 2025
1 parent c145d40 commit d56f7d2
Show file tree
Hide file tree
Showing 4 changed files with 45 additions and 22 deletions.
49 changes: 27 additions & 22 deletions Libraries/LibWeb/Painting/PaintableBox.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -965,12 +965,36 @@ TraversalDecision PaintableBox::hit_test(CSSPixelPoint position, HitTestType typ
return TraversalDecision::Break;
}

if (!visible_for_hit_testing())
return TraversalDecision::Continue;

if (!absolute_border_box_rect().contains(position_adjusted_by_scroll_offset))
return TraversalDecision::Continue;

if (hit_test_continuation(callback) == TraversalDecision::Break)
return TraversalDecision::Break;

return callback(HitTestResult { const_cast<PaintableBox&>(*this) });
}

TraversalDecision PaintableBox::hit_test_continuation(Function<TraversalDecision(HitTestResult)> const& callback) const
{
// If we're hit testing the "middle" part of a continuation chain, we are dealing with an anonymous box that is
// linked to a parent inline node. Since our block element children did not match the hit test, but we did, we
// should walk the continuation chain up to the inline parent and return a hit on that instead.
auto continuation_node = layout_node_with_style_and_box_metrics().continuation_of_node();
if (!continuation_node || !layout_node().is_anonymous())
return TraversalDecision::Continue;

while (continuation_node->continuation_of_node())
continuation_node = continuation_node->continuation_of_node();
auto& paintable = *continuation_node->first_paintable();
if (!paintable.visible_for_hit_testing())
return TraversalDecision::Continue;

return callback(HitTestResult { paintable });
}

Optional<HitTestResult> PaintableBox::hit_test(CSSPixelPoint position, HitTestType type) const
{
Optional<HitTestResult> result;
Expand All @@ -985,28 +1009,6 @@ Optional<HitTestResult> PaintableBox::hit_test(CSSPixelPoint position, HitTestTy
return TraversalDecision::Break;
return TraversalDecision::Continue;
});

// If our hit-testing has resulted in a hit on a paintable, we know that it is the most specific hit. If that
// paintable turns out to be invisible for hit-testing, we need to traverse up the paintable tree to find the next
// paintable that is visible for hit-testing. This implements the behavior expected for pointer-events.
while (result.has_value() && !result->paintable->visible_for_hit_testing()) {
result->index_in_node = result->paintable->dom_node() ? result->paintable->dom_node()->index() : 0;
result->paintable = result->paintable->parent();

// If the new parent is an anonymous box part of a continuation, we need to follow the chain to the inline node
// that spawned the anonymous "middle" part of the continuation, since that inline node is the actual parent.
if (is<PaintableBox>(*result->paintable)) {
auto const& box_layout_node = static_cast<PaintableBox&>(*result->paintable).layout_node_with_style_and_box_metrics();
if (box_layout_node.is_anonymous() && box_layout_node.continuation_of_node()) {
auto const* original_inline_node = &box_layout_node;
while (original_inline_node->continuation_of_node())
original_inline_node = original_inline_node->continuation_of_node();

result->paintable = const_cast<Paintable*>(original_inline_node->first_paintable());
}
}
}

return result;
}

Expand Down Expand Up @@ -1048,6 +1050,9 @@ TraversalDecision PaintableWithLines::hit_test(CSSPixelPoint position, HitTestTy
return TraversalDecision::Break;
}

if (!visible_for_hit_testing())
return TraversalDecision::Continue;

for (auto const& fragment : fragments()) {
if (fragment.paintable().has_stacking_context())
continue;
Expand Down
1 change: 1 addition & 0 deletions Libraries/LibWeb/Painting/PaintableBox.h
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ class PaintableBox : public Paintable

[[nodiscard]] virtual TraversalDecision hit_test(CSSPixelPoint position, HitTestType type, Function<TraversalDecision(HitTestResult)> const& callback) const override;
Optional<HitTestResult> hit_test(CSSPixelPoint, HitTestType) const;
[[nodiscard]] TraversalDecision hit_test_continuation(Function<TraversalDecision(HitTestResult)> const& callback) const;

virtual bool handle_mousewheel(Badge<EventHandler>, CSSPixelPoint, unsigned buttons, unsigned modifiers, int wheel_delta_x, int wheel_delta_y) override;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<HTML>
<#document>
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<script src="../include.js"></script>
<body>
<div>
<!-- this div should not be hit, nor should its parent -->
<div id="a1" style="position: fixed; width: 100px; height: 100px; pointer-events: none"></div>
foobar
</div>
</body>
<script>
test(() => {
const hit = internals.hitTest(a1.offsetLeft + 50, a1.offsetTop + 80);
printElement(hit.node);
printElement(hit.node.parentNode);
});
</script>

0 comments on commit d56f7d2

Please sign in to comment.