From 15ed0ebdc6b659490e0695825baba0439d9fd7dc Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Fri, 4 Mar 2022 15:02:16 +0100 Subject: [PATCH] LibWeb: Implement hit testing a bit closer to spec We now perform hit testing in reverse paint order as described in CSS2's section about the z-index property. --- .../LibWeb/Layout/BlockContainer.cpp | 2 +- Userland/Libraries/LibWeb/Layout/Box.cpp | 2 +- Userland/Libraries/LibWeb/Layout/Node.cpp | 10 +-- .../LibWeb/Painting/StackingContext.cpp | 86 +++++++++++++++---- 4 files changed, 74 insertions(+), 26 deletions(-) diff --git a/Userland/Libraries/LibWeb/Layout/BlockContainer.cpp b/Userland/Libraries/LibWeb/Layout/BlockContainer.cpp index f58214fc5b..81948ada17 100644 --- a/Userland/Libraries/LibWeb/Layout/BlockContainer.cpp +++ b/Userland/Libraries/LibWeb/Layout/BlockContainer.cpp @@ -101,7 +101,7 @@ HitTestResult BlockContainer::hit_test(const Gfx::IntPoint& position, HitTestTyp if (type == HitTestType::TextCursor && last_good_candidate.layout_node) return last_good_candidate; - return { absolute_rect().contains(position.x(), position.y()) ? this : nullptr }; + return { absolute_border_box_rect().contains(position.x(), position.y()) ? this : nullptr }; } bool BlockContainer::is_scrollable() const diff --git a/Userland/Libraries/LibWeb/Layout/Box.cpp b/Userland/Libraries/LibWeb/Layout/Box.cpp index 5ca73e3439..73d0d3b940 100644 --- a/Userland/Libraries/LibWeb/Layout/Box.cpp +++ b/Userland/Libraries/LibWeb/Layout/Box.cpp @@ -178,7 +178,7 @@ HitTestResult Box::hit_test(const Gfx::IntPoint& position, HitTestType type) con // FIXME: It would be nice if we could confidently skip over hit testing // parts of the layout tree, but currently we can't just check // m_rect.contains() since inline text rects can't be trusted.. - HitTestResult result { absolute_rect().contains(position.x(), position.y()) ? this : nullptr }; + HitTestResult result { absolute_border_box_rect().contains(position.x(), position.y()) ? this : nullptr }; for_each_child_in_paint_order([&](auto& child) { auto child_result = child.hit_test(position, type); if (child_result.layout_node) diff --git a/Userland/Libraries/LibWeb/Layout/Node.cpp b/Userland/Libraries/LibWeb/Layout/Node.cpp index 7dd3a6b1fe..ed677e153e 100644 --- a/Userland/Libraries/LibWeb/Layout/Node.cpp +++ b/Userland/Libraries/LibWeb/Layout/Node.cpp @@ -80,15 +80,9 @@ bool Node::establishes_stacking_context() const return computed_values().opacity() < 1.0f; } -HitTestResult Node::hit_test(const Gfx::IntPoint& position, HitTestType type) const +HitTestResult Node::hit_test(Gfx::IntPoint const&, HitTestType) const { - HitTestResult result; - for_each_child_in_paint_order([&](auto& child) { - auto child_result = child.hit_test(position, type); - if (child_result.layout_node) - result = child_result; - }); - return result; + VERIFY_NOT_REACHED(); } HTML::BrowsingContext const& Node::browsing_context() const diff --git a/Userland/Libraries/LibWeb/Painting/StackingContext.cpp b/Userland/Libraries/LibWeb/Painting/StackingContext.cpp index 9bb7510069..8c667869e2 100644 --- a/Userland/Libraries/LibWeb/Painting/StackingContext.cpp +++ b/Userland/Libraries/LibWeb/Painting/StackingContext.cpp @@ -148,27 +148,81 @@ void StackingContext::paint(PaintContext& context) HitTestResult StackingContext::hit_test(const Gfx::IntPoint& position, HitTestType type) const { + // NOTE: Hit testing basically happens in reverse painting order. + // https://www.w3.org/TR/CSS22/visuren.html#z-index + + // 7. the child stacking contexts with positive stack levels (least positive first). + for (ssize_t i = m_children.size() - 1; i >= 0; --i) { + auto const& child = *m_children[i]; + if (child.m_box.computed_values().z_index().value_or(0) < 0) + break; + auto result = child.hit_test(position, type); + if (result.layout_node) + return result; + } + HitTestResult result; - if (!is(m_box)) { - result = m_box.hit_test(position, type); - } else { - // NOTE: InitialContainingBlock::hit_test() merely calls StackingContext::hit_test() - // so we call its base class instead. - result = verify_cast(m_box).BlockContainer::hit_test(position, type); + // 6. the child stacking contexts with stack level 0 and the positioned descendants with stack level 0. + m_box.for_each_in_subtree_of_type([&](Layout::Box const& box) { + if (box.is_positioned() && !box.stacking_context()) { + result = box.hit_test(position, type); + if (result.layout_node) + return IterationDecision::Break; + } + return IterationDecision::Continue; + }); + if (result.layout_node) + return result; + + // 5. the in-flow, inline-level, non-positioned descendants, including inline tables and inline blocks. + if (m_box.children_are_inline() && is(m_box)) { + auto result = m_box.hit_test(position, type); + if (result.layout_node) + return result; } - int z_index = m_box.computed_values().z_index().value_or(0); + // 4. the non-positioned floats. + m_box.for_each_in_subtree_of_type([&](Layout::Box const& box) { + if (box.is_floating()) { + result = box.hit_test(position, type); + if (result.layout_node) + return IterationDecision::Break; + } + return IterationDecision::Continue; + }); - for (auto* child : m_children) { - int child_z_index = child->m_box.computed_values().z_index().value_or(0); - if (result.layout_node && (child_z_index < z_index)) - continue; - - auto result_here = child->hit_test(position, type); - if (result_here.layout_node) - result = result_here; + // 3. the in-flow, non-inline-level, non-positioned descendants. + if (!m_box.children_are_inline()) { + m_box.for_each_in_subtree_of_type([&](Layout::Box const& box) { + if (!box.is_absolutely_positioned() && !box.is_floating()) { + result = box.hit_test(position, type); + if (result.layout_node) + return IterationDecision::Break; + } + return IterationDecision::Continue; + }); + if (result.layout_node) + return result; } - return result; + + // 2. the child stacking contexts with negative stack levels (most negative first). + for (ssize_t i = m_children.size() - 1; i >= 0; --i) { + auto const& child = *m_children[i]; + if (child.m_box.computed_values().z_index().value_or(0) < 0) + break; + auto result = child.hit_test(position, type); + if (result.layout_node) + return result; + } + + // 1. the background and borders of the element forming the stacking context. + if (m_box.absolute_border_box_rect().contains(position.to_type())) { + return HitTestResult { + .layout_node = m_box, + }; + } + + return {}; } void StackingContext::dump(int indent) const