Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LibWeb: Improve handling of stacking contexts with pointer-events and continuation chains #3367

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
9 changes: 4 additions & 5 deletions Libraries/LibWeb/Painting/StackingContext.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -443,13 +443,12 @@ TraversalDecision StackingContext::hit_test(CSSPixelPoint position, HitTestType

CSSPixelPoint enclosing_scroll_offset = paintable_box().cumulative_offset_of_enclosing_scroll_frame();

auto position_adjusted_by_scroll_offset = transformed_position;
position_adjusted_by_scroll_offset.translate_by(-enclosing_scroll_offset);
auto position_adjusted_by_scroll_offset = transformed_position.translated(-enclosing_scroll_offset);

// 1. the background and borders of the element forming the stacking context.
if (paintable_box().absolute_border_box_rect().contains(position_adjusted_by_scroll_offset.x(), position_adjusted_by_scroll_offset.y())) {
auto hit_test_result = HitTestResult { .paintable = const_cast<PaintableBox&>(paintable_box()) };
if (callback(hit_test_result) == TraversalDecision::Break)
if (paintable_box().visible_for_hit_testing()
&& paintable_box().absolute_border_box_rect().contains(position_adjusted_by_scroll_offset)) {
if (callback({ const_cast<PaintableBox&>(paintable_box()) }) == TraversalDecision::Break)
return TraversalDecision::Break;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<HTML>
<#document>
3 changes: 3 additions & 0 deletions Tests/LibWeb/Text/expected/hit_testing/pointer-events.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,6 @@
<I id="d2">
<B id="d1">
---
<DIV id="e2">
<BODY>
---
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>
5 changes: 5 additions & 0 deletions Tests/LibWeb/Text/input/hit_testing/pointer-events.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@

<!-- 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>

<!-- div creates its own stacking context, #e2 must be hit instead of crashing -->
<div id="e1" style="position: fixed; width: 100px; height: 100px; pointer-events: none; background-color: red"></div>
<div id="e2" style="width: 100px; height: 100px; background-color: green"></div>
</body>
<script>
test(() => {
Expand All @@ -33,5 +37,6 @@
printHit(b1.offsetLeft + 50, b1.offsetTop + 50);
printHit(c1.offsetLeft + 50, c1.offsetTop + 50);
printHit(d4.offsetLeft + 10, d4.offsetTop + 8);
printHit(e1.offsetLeft + 50, e1.offsetTop + 50);
});
</script>
Loading