Skip to content

Commit

Permalink
LibWeb: Reorder paintable hit-testing to account for pointer-events
Browse files Browse the repository at this point in the history
Instead of ignoring any paintable immediately when they're invisible to
hit-testing, consider every candidate and while the most specific
candidate is invisible to hit-testing, traverse up to its parent
paintable.

This more closely reflects the behavior expected when wrapping block
elements inside inline elements, where although the block element might
have `pointer-events: none`, it still becomes part of the hit-test body
of the inline parent.

This makes the following link work as expected:

  <a href="https://ladybird.org">
    <div style="pointer-events: none">Ladybird</div>
  </a>
  • Loading branch information
gmta committed Jan 23, 2025
1 parent e076cb9 commit b7a554d
Show file tree
Hide file tree
Showing 4 changed files with 76 additions and 10 deletions.
2 changes: 1 addition & 1 deletion Libraries/LibWeb/DOM/Document.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5315,7 +5315,7 @@ GC::RootVector<GC::Ref<Element>> Document::elements_from_point(double x, double
if (auto const* paintable_box = this->paintable_box(); paintable_box) {
(void)paintable_box->hit_test(position, Painting::HitTestType::Exact, [&](Painting::HitTestResult result) {
auto* dom_node = result.dom_node();
if (dom_node && dom_node->is_element())
if (dom_node && dom_node->is_element() && result.paintable->visible_for_hit_testing())
sequence.append(*static_cast<Element*>(dom_node));
return TraversalDecision::Continue;
});
Expand Down
35 changes: 26 additions & 9 deletions Libraries/LibWeb/Painting/PaintableBox.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -965,9 +965,6 @@ 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;

Expand All @@ -978,18 +975,38 @@ Optional<HitTestResult> PaintableBox::hit_test(CSSPixelPoint position, HitTestTy
{
Optional<HitTestResult> result;
(void)PaintableBox::hit_test(position, type, [&](HitTestResult candidate) {
if (candidate.paintable->visible_for_hit_testing()) {
if (!result.has_value()
|| candidate.vertical_distance.value_or(CSSPixels::max_integer_value) < result->vertical_distance.value_or(CSSPixels::max_integer_value)
|| candidate.horizontal_distance.value_or(CSSPixels::max_integer_value) < result->horizontal_distance.value_or(CSSPixels::max_integer_value)) {
result = move(candidate);
}
if (!result.has_value()
|| candidate.vertical_distance.value_or(CSSPixels::max_integer_value) < result->vertical_distance.value_or(CSSPixels::max_integer_value)
|| candidate.horizontal_distance.value_or(CSSPixels::max_integer_value) < result->horizontal_distance.value_or(CSSPixels::max_integer_value)) {
result = move(candidate);
}

if (result.has_value() && (type == HitTestType::Exact || (result->vertical_distance == 0 && result->horizontal_distance == 0)))
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
12 changes: 12 additions & 0 deletions Tests/LibWeb/Text/expected/hit_testing/pointer-events.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<DIV id="a1" >
<BODY >
---
<DIV id="b3" >
<DIV id="b2" >
---
<A id="c1" >
<BODY >
---
<I id="d2" >
<B id="d1" >
---
37 changes: 37 additions & 0 deletions Tests/LibWeb/Text/input/hit_testing/pointer-events.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<script src="../include.js"></script>
<body>
<!-- #a2 should be invisible to hit testing -->
<div id="a1">
<div id="a2" style="width: 100px; height: 100px; pointer-events: none"></div>
</div>

<!-- #b3 should be visible to hit testing -->
<div id="b1">
<div id="b2" style="width: 100px; height: 100px; pointer-events: none">
<div id="b3" style="width: 100px; height: 100px; pointer-events: auto"></div>
</div>
</div>

<!-- #c1 should be hit, even though it is an inline element surrounding a block element -->
<a id="c1">
<div id="c2" style="width: 100px; height: 100px; pointer-events: none"></div>
</a>

<!-- a pointer event on #d4 should hit #d2 instead -->
<b id="d1">foo<i id="d2"><div id="d3">bar</div>baz<u id="d4" style="pointer-events: none">lorem</u></i></b>
</body>
<script>
test(() => {
const printHit = (x, y) => {
const hit = internals.hitTest(x, y);
printElement(hit.node);
printElement(hit.node.parentNode);
println('---');
};

printHit(a1.offsetLeft + 50, a1.offsetTop + 50);
printHit(b1.offsetLeft + 50, b1.offsetTop + 50);
printHit(c1.offsetLeft + 50, c1.offsetTop + 50);
printHit(d4.offsetLeft + 10, d4.offsetTop + 8);
});
</script>

0 comments on commit b7a554d

Please sign in to comment.