Browse Source

LibWeb: Change StackingContext::hit_test() to accept callback

This change modifies hit_test() to no longer return the first paintable
encountered at a specified position. Instead, this function accepts a
callback that is invoked for each paintable located at a position, in
hit-testing order.

This modification will allow us to reuse this call for
`Document.elementsFromPoint()` in upcoming changes.
Aliaksandr Kalenik 1 year ago
parent
commit
9c99182b1e

+ 2 - 2
Tests/LibWeb/Text/expected/DOM/Elements-from-point.txt

@@ -5,8 +5,8 @@ Coordinates outside the viewport return empty array: true
 <HTML >
 <HTML >
 == FIXME: Elements at (550, 60) ==
 == FIXME: Elements at (550, 60) ==
 <DIV id="small-box" >
 <DIV id="small-box" >
-<DIV id="large-box" >
-<DIV id="large-box" >
+<PRE id="out" >
+<PRE id="out" >
 <DIV id="large-box" >
 <DIV id="large-box" >
 <DIV id="small-box" >
 <DIV id="small-box" >
 <PRE id="out" >
 <PRE id="out" >

+ 18 - 15
Userland/Libraries/LibWeb/Painting/InlinePaintable.cpp

@@ -199,39 +199,42 @@ void InlinePaintable::for_each_fragment(Callback callback) const
     }
     }
 }
 }
 
 
-Optional<HitTestResult> InlinePaintable::hit_test(CSSPixelPoint position, HitTestType type) const
+TraversalDecision InlinePaintable::hit_test(CSSPixelPoint position, HitTestType type, Function<TraversalDecision(HitTestResult)> const& callback) const
 {
 {
     if (m_clip_rect.has_value() && !m_clip_rect.value().contains(position))
     if (m_clip_rect.has_value() && !m_clip_rect.value().contains(position))
-        return {};
+        return TraversalDecision::Continue;
 
 
     auto position_adjusted_by_scroll_offset = position;
     auto position_adjusted_by_scroll_offset = position;
     if (enclosing_scroll_frame_offset().has_value())
     if (enclosing_scroll_frame_offset().has_value())
         position_adjusted_by_scroll_offset.translate_by(-enclosing_scroll_frame_offset().value());
         position_adjusted_by_scroll_offset.translate_by(-enclosing_scroll_frame_offset().value());
 
 
-    for (auto& fragment : m_fragments) {
+    for (auto const& fragment : m_fragments) {
         if (fragment.paintable().stacking_context())
         if (fragment.paintable().stacking_context())
             continue;
             continue;
         auto fragment_absolute_rect = fragment.absolute_rect();
         auto fragment_absolute_rect = fragment.absolute_rect();
         if (fragment_absolute_rect.contains(position_adjusted_by_scroll_offset)) {
         if (fragment_absolute_rect.contains(position_adjusted_by_scroll_offset)) {
-            if (auto result = fragment.paintable().hit_test(position, type); result.has_value())
-                return result;
-            return HitTestResult { const_cast<Paintable&>(fragment.paintable()),
-                fragment.text_index_at(position_adjusted_by_scroll_offset.x()) };
+            if (fragment.paintable().hit_test(position, type, callback) == TraversalDecision::Break)
+                return TraversalDecision::Break;
+            auto hit_test_result = HitTestResult { const_cast<Paintable&>(fragment.paintable()), fragment.text_index_at(position_adjusted_by_scroll_offset.x()) };
+            if (callback(hit_test_result) == TraversalDecision::Break)
+                return TraversalDecision::Break;
         }
         }
     }
     }
 
 
-    Optional<HitTestResult> hit_test_result;
+    bool should_exit = false;
     for_each_child([&](Paintable const& child) {
     for_each_child([&](Paintable const& child) {
+        if (should_exit)
+            return;
         if (child.stacking_context())
         if (child.stacking_context())
-            return IterationDecision::Continue;
-        if (auto result = child.hit_test(position, type); result.has_value()) {
-            hit_test_result = result;
-            return IterationDecision::Break;
-        }
-        return IterationDecision::Continue;
+            return;
+        if (child.hit_test(position, type, callback) == TraversalDecision::Break)
+            should_exit = true;
     });
     });
 
 
-    return hit_test_result;
+    if (should_exit)
+        return TraversalDecision::Break;
+
+    return TraversalDecision::Continue;
 }
 }
 
 
 CSSPixelRect InlinePaintable::bounding_rect() const
 CSSPixelRect InlinePaintable::bounding_rect() const

+ 1 - 1
Userland/Libraries/LibWeb/Painting/InlinePaintable.h

@@ -32,7 +32,7 @@ public:
 
 
     virtual bool is_inline_paintable() const override { return true; }
     virtual bool is_inline_paintable() const override { return true; }
 
 
-    virtual Optional<HitTestResult> hit_test(CSSPixelPoint, HitTestType) const override;
+    virtual TraversalDecision hit_test(CSSPixelPoint, HitTestType, Function<TraversalDecision(HitTestResult)> const& callback) const override;
 
 
     void set_box_shadow_data(Vector<ShadowData>&& box_shadow_data) { m_box_shadow_data = move(box_shadow_data); }
     void set_box_shadow_data(Vector<ShadowData>&& box_shadow_data) { m_box_shadow_data = move(box_shadow_data); }
     Vector<ShadowData> const& box_shadow_data() const { return m_box_shadow_data; }
     Vector<ShadowData> const& box_shadow_data() const { return m_box_shadow_data; }

+ 2 - 2
Userland/Libraries/LibWeb/Painting/Paintable.cpp

@@ -98,9 +98,9 @@ bool Paintable::handle_mousewheel(Badge<EventHandler>, CSSPixelPoint, unsigned,
     return false;
     return false;
 }
 }
 
 
-Optional<HitTestResult> Paintable::hit_test(CSSPixelPoint, HitTestType) const
+TraversalDecision Paintable::hit_test(CSSPixelPoint, HitTestType, Function<TraversalDecision(HitTestResult)> const&) const
 {
 {
-    return {};
+    return TraversalDecision::Continue;
 }
 }
 
 
 StackingContext* Paintable::enclosing_stacking_context()
 StackingContext* Paintable::enclosing_stacking_context()

+ 1 - 1
Userland/Libraries/LibWeb/Painting/Paintable.h

@@ -143,7 +143,7 @@ public:
     virtual void apply_clip_overflow_rect(PaintContext&, PaintPhase) const { }
     virtual void apply_clip_overflow_rect(PaintContext&, PaintPhase) const { }
     virtual void clear_clip_overflow_rect(PaintContext&, PaintPhase) const { }
     virtual void clear_clip_overflow_rect(PaintContext&, PaintPhase) const { }
 
 
-    virtual Optional<HitTestResult> hit_test(CSSPixelPoint, HitTestType) const;
+    [[nodiscard]] virtual TraversalDecision hit_test(CSSPixelPoint, HitTestType, Function<TraversalDecision(HitTestResult)> const& callback) const;
 
 
     virtual bool wants_mouse_events() const { return false; }
     virtual bool wants_mouse_events() const { return false; }
 
 

+ 48 - 32
Userland/Libraries/LibWeb/Painting/PaintableBox.cpp

@@ -719,17 +719,17 @@ Layout::BlockContainer& PaintableWithLines::layout_box()
     return static_cast<Layout::BlockContainer&>(PaintableBox::layout_box());
     return static_cast<Layout::BlockContainer&>(PaintableBox::layout_box());
 }
 }
 
 
-Optional<HitTestResult> PaintableBox::hit_test(CSSPixelPoint position, HitTestType type) const
+TraversalDecision PaintableBox::hit_test(CSSPixelPoint position, HitTestType type, Function<TraversalDecision(HitTestResult)> const& callback) const
 {
 {
     if (clip_rect().has_value() && !clip_rect()->contains(position))
     if (clip_rect().has_value() && !clip_rect()->contains(position))
-        return {};
+        return TraversalDecision::Continue;
 
 
     auto position_adjusted_by_scroll_offset = position;
     auto position_adjusted_by_scroll_offset = position;
     if (enclosing_scroll_frame_offset().has_value())
     if (enclosing_scroll_frame_offset().has_value())
         position_adjusted_by_scroll_offset.translate_by(-enclosing_scroll_frame_offset().value());
         position_adjusted_by_scroll_offset.translate_by(-enclosing_scroll_frame_offset().value());
 
 
     if (!is_visible())
     if (!is_visible())
-        return {};
+        return TraversalDecision::Continue;
 
 
     if (layout_box().is_viewport()) {
     if (layout_box().is_viewport()) {
         auto& viewport_paintable = const_cast<ViewportPaintable&>(static_cast<ViewportPaintable const&>(*this));
         auto& viewport_paintable = const_cast<ViewportPaintable&>(static_cast<ViewportPaintable const&>(*this));
@@ -737,46 +737,55 @@ Optional<HitTestResult> PaintableBox::hit_test(CSSPixelPoint position, HitTestTy
         viewport_paintable.document().update_paint_and_hit_testing_properties_if_needed();
         viewport_paintable.document().update_paint_and_hit_testing_properties_if_needed();
         viewport_paintable.refresh_scroll_state();
         viewport_paintable.refresh_scroll_state();
         viewport_paintable.refresh_clip_state();
         viewport_paintable.refresh_clip_state();
-        return stacking_context()->hit_test(position, type);
+        return stacking_context()->hit_test(position, type, callback);
     }
     }
 
 
     if (!absolute_border_box_rect().contains(position_adjusted_by_scroll_offset.x(), position_adjusted_by_scroll_offset.y()))
     if (!absolute_border_box_rect().contains(position_adjusted_by_scroll_offset.x(), position_adjusted_by_scroll_offset.y()))
-        return {};
+        return TraversalDecision::Continue;
 
 
-    for (auto* child = first_child(); child; child = child->next_sibling()) {
-        auto result = child->hit_test(position, type);
-        if (!result.has_value())
-            continue;
-        if (!result->paintable->visible_for_hit_testing())
+    for (auto const* child = last_child(); child; child = child->previous_sibling()) {
+        auto z_index = child->computed_values().z_index();
+        if (child->layout_node().is_positioned() && z_index.value_or(0) == 0)
             continue;
             continue;
-        return result;
+        if (child->hit_test(position, type, callback) == TraversalDecision::Break)
+            return TraversalDecision::Break;
     }
     }
 
 
     if (!visible_for_hit_testing())
     if (!visible_for_hit_testing())
-        return {};
+        return TraversalDecision::Continue;
+
+    return callback(HitTestResult { const_cast<PaintableBox&>(*this) });
+}
 
 
-    return HitTestResult { const_cast<PaintableBox&>(*this) };
+Optional<HitTestResult> PaintableBox::hit_test(CSSPixelPoint position, HitTestType type) const
+{
+    Optional<HitTestResult> result;
+    (void)PaintableBox::hit_test(position, type, [&](HitTestResult candidate) {
+        VERIFY(!result.has_value());
+        if (!candidate.paintable->visible_for_hit_testing())
+            return Painting::TraversalDecision::Continue;
+        result = move(candidate);
+        return Painting::TraversalDecision::Break;
+    });
+    return result;
 }
 }
 
 
-Optional<HitTestResult> PaintableWithLines::hit_test(CSSPixelPoint position, HitTestType type) const
+TraversalDecision PaintableWithLines::hit_test(CSSPixelPoint position, HitTestType type, Function<TraversalDecision(HitTestResult)> const& callback) const
 {
 {
     if (clip_rect().has_value() && !clip_rect()->contains(position))
     if (clip_rect().has_value() && !clip_rect()->contains(position))
-        return {};
+        return TraversalDecision::Continue;
 
 
     auto position_adjusted_by_scroll_offset = position;
     auto position_adjusted_by_scroll_offset = position;
     if (enclosing_scroll_frame_offset().has_value())
     if (enclosing_scroll_frame_offset().has_value())
         position_adjusted_by_scroll_offset.translate_by(-enclosing_scroll_frame_offset().value());
         position_adjusted_by_scroll_offset.translate_by(-enclosing_scroll_frame_offset().value());
 
 
-    if (!layout_box().children_are_inline() || m_fragments.is_empty())
-        return PaintableBox::hit_test(position, type);
+    if (!layout_box().children_are_inline() || m_fragments.is_empty()) {
+        return PaintableBox::hit_test(position, type, callback);
+    }
 
 
-    for (auto* child = first_child(); child; child = child->next_sibling()) {
-        auto result = child->hit_test(position, type);
-        if (!result.has_value())
-            continue;
-        if (!result->paintable->visible_for_hit_testing())
-            continue;
-        return result;
+    for (auto const* child = last_child(); child; child = child->previous_sibling()) {
+        if (child->hit_test(position, type, callback) == TraversalDecision::Break)
+            return TraversalDecision::Break;
     }
     }
 
 
     Optional<HitTestResult> last_good_candidate;
     Optional<HitTestResult> last_good_candidate;
@@ -785,9 +794,11 @@ Optional<HitTestResult> PaintableWithLines::hit_test(CSSPixelPoint position, Hit
             continue;
             continue;
         auto fragment_absolute_rect = fragment.absolute_rect();
         auto fragment_absolute_rect = fragment.absolute_rect();
         if (fragment_absolute_rect.contains(position_adjusted_by_scroll_offset)) {
         if (fragment_absolute_rect.contains(position_adjusted_by_scroll_offset)) {
-            if (auto result = fragment.paintable().hit_test(position, type); result.has_value())
-                return result;
-            return HitTestResult { const_cast<Paintable&>(fragment.paintable()), fragment.text_index_at(position_adjusted_by_scroll_offset.x()) };
+            if (fragment.paintable().hit_test(position, type, callback) == TraversalDecision::Break)
+                return TraversalDecision::Break;
+            HitTestResult hit_test_result { const_cast<Paintable&>(fragment.paintable()), fragment.text_index_at(position_adjusted_by_scroll_offset.x()) };
+            if (callback(hit_test_result) == TraversalDecision::Break)
+                return TraversalDecision::Break;
         }
         }
 
 
         // If we reached this point, the position is not within the fragment. However, the fragment start or end might be the place to place the cursor.
         // If we reached this point, the position is not within the fragment. However, the fragment start or end might be the place to place the cursor.
@@ -808,11 +819,16 @@ Optional<HitTestResult> PaintableWithLines::hit_test(CSSPixelPoint position, Hit
         }
         }
     }
     }
 
 
-    if (type == HitTestType::TextCursor && last_good_candidate.has_value())
-        return last_good_candidate;
-    if (is_visible() && absolute_border_box_rect().contains(position_adjusted_by_scroll_offset.x(), position_adjusted_by_scroll_offset.y()))
-        return HitTestResult { const_cast<PaintableWithLines&>(*this) };
-    return {};
+    if (type == HitTestType::TextCursor && last_good_candidate.has_value()) {
+        if (callback(last_good_candidate.value()) == TraversalDecision::Break)
+            return TraversalDecision::Break;
+    }
+    if (!stacking_context() && is_visible() && absolute_border_box_rect().contains(position_adjusted_by_scroll_offset.x(), position_adjusted_by_scroll_offset.y())) {
+        if (callback(HitTestResult { const_cast<PaintableWithLines&>(*this) }) == TraversalDecision::Break)
+            return TraversalDecision::Break;
+    }
+
+    return TraversalDecision::Continue;
 }
 }
 
 
 void PaintableBox::set_needs_display() const
 void PaintableBox::set_needs_display() const

+ 3 - 2
Userland/Libraries/LibWeb/Painting/PaintableBox.h

@@ -136,7 +136,8 @@ public:
     virtual void apply_clip_overflow_rect(PaintContext&, PaintPhase) const override;
     virtual void apply_clip_overflow_rect(PaintContext&, PaintPhase) const override;
     virtual void clear_clip_overflow_rect(PaintContext&, PaintPhase) const override;
     virtual void clear_clip_overflow_rect(PaintContext&, PaintPhase) const override;
 
 
-    virtual Optional<HitTestResult> hit_test(CSSPixelPoint, HitTestType) const override;
+    [[nodiscard]] virtual TraversalDecision hit_test(CSSPixelPoint position, HitTestType type, Function<TraversalDecision(HitTestResult)> const& callback) const override;
+    Optional<HitTestResult> hit_test(CSSPixelPoint, HitTestType) const;
 
 
     virtual bool handle_mousewheel(Badge<EventHandler>, CSSPixelPoint, unsigned buttons, unsigned modifiers, int wheel_delta_x, int wheel_delta_y) override;
     virtual bool handle_mousewheel(Badge<EventHandler>, CSSPixelPoint, unsigned buttons, unsigned modifiers, int wheel_delta_x, int wheel_delta_y) override;
 
 
@@ -286,7 +287,7 @@ public:
     virtual void paint(PaintContext&, PaintPhase) const override;
     virtual void paint(PaintContext&, PaintPhase) const override;
     virtual bool wants_mouse_events() const override { return false; }
     virtual bool wants_mouse_events() const override { return false; }
 
 
-    virtual Optional<HitTestResult> hit_test(CSSPixelPoint, HitTestType) const override;
+    [[nodiscard]] virtual TraversalDecision hit_test(CSSPixelPoint position, HitTestType type, Function<TraversalDecision(HitTestResult)> const& callback) const override;
 
 
     virtual void visit_edges(Cell::Visitor& visitor) override
     virtual void visit_edges(Cell::Visitor& visitor) override
     {
     {

+ 5 - 6
Userland/Libraries/LibWeb/Painting/SVGPathPaintable.cpp

@@ -26,15 +26,14 @@ Layout::SVGGraphicsBox const& SVGPathPaintable::layout_box() const
     return static_cast<Layout::SVGGraphicsBox const&>(layout_node());
     return static_cast<Layout::SVGGraphicsBox const&>(layout_node());
 }
 }
 
 
-Optional<HitTestResult> SVGPathPaintable::hit_test(CSSPixelPoint position, HitTestType type) const
+TraversalDecision SVGPathPaintable::hit_test(CSSPixelPoint position, HitTestType type, Function<TraversalDecision(HitTestResult)> const& callback) const
 {
 {
-    auto result = SVGGraphicsPaintable::hit_test(position, type);
-    if (!result.has_value() || !computed_path().has_value())
-        return {};
+    if (!computed_path().has_value())
+        return TraversalDecision::Continue;
     auto transformed_bounding_box = computed_transforms().svg_to_css_pixels_transform().map_to_quad(computed_path()->bounding_box());
     auto transformed_bounding_box = computed_transforms().svg_to_css_pixels_transform().map_to_quad(computed_path()->bounding_box());
     if (!transformed_bounding_box.contains(position.to_type<float>()))
     if (!transformed_bounding_box.contains(position.to_type<float>()))
-        return {};
-    return result;
+        return TraversalDecision::Continue;
+    return SVGGraphicsPaintable::hit_test(position, type, callback);
 }
 }
 
 
 static Gfx::Painter::WindingRule to_gfx_winding_rule(SVG::FillRule fill_rule)
 static Gfx::Painter::WindingRule to_gfx_winding_rule(SVG::FillRule fill_rule)

+ 1 - 1
Userland/Libraries/LibWeb/Painting/SVGPathPaintable.h

@@ -17,7 +17,7 @@ class SVGPathPaintable final : public SVGGraphicsPaintable {
 public:
 public:
     static JS::NonnullGCPtr<SVGPathPaintable> create(Layout::SVGGraphicsBox const&);
     static JS::NonnullGCPtr<SVGPathPaintable> create(Layout::SVGGraphicsBox const&);
 
 
-    virtual Optional<HitTestResult> hit_test(CSSPixelPoint, HitTestType) const override;
+    virtual TraversalDecision hit_test(CSSPixelPoint, HitTestType, Function<TraversalDecision(HitTestResult)> const& callback) const override;
 
 
     virtual void paint(PaintContext&, PaintPhase) const override;
     virtual void paint(PaintContext&, PaintPhase) const override;
 
 

+ 38 - 42
Userland/Libraries/LibWeb/Painting/StackingContext.cpp

@@ -356,10 +356,10 @@ static TraversalDecision for_each_in_subtree_within_same_stacking_context_in_rev
     return TraversalDecision::Continue;
     return TraversalDecision::Continue;
 }
 }
 
 
-Optional<HitTestResult> StackingContext::hit_test(CSSPixelPoint position, HitTestType type) const
+TraversalDecision StackingContext::hit_test(CSSPixelPoint position, HitTestType type, Function<TraversalDecision(HitTestResult)> const& callback) const
 {
 {
     if (!paintable().is_visible())
     if (!paintable().is_visible())
-        return {};
+        return TraversalDecision::Continue;
 
 
     CSSPixelPoint transform_origin { 0, 0 };
     CSSPixelPoint transform_origin { 0, 0 };
     if (paintable().is_paintable_box())
     if (paintable().is_paintable_box())
@@ -385,33 +385,33 @@ Optional<HitTestResult> StackingContext::hit_test(CSSPixelPoint position, HitTes
         auto const& child = *m_children[i];
         auto const& child = *m_children[i];
         if (child.paintable().computed_values().z_index().value_or(0) <= 0)
         if (child.paintable().computed_values().z_index().value_or(0) <= 0)
             break;
             break;
-        auto result = child.hit_test(transformed_position, type);
-        if (result.has_value() && result->paintable->visible_for_hit_testing())
-            return result;
+        if (child.hit_test(transformed_position, type, callback) == TraversalDecision::Break)
+            return TraversalDecision::Break;
     }
     }
 
 
+    bool should_exit = false;
+
     // 6. the child stacking contexts with stack level 0 and the positioned descendants with stack level 0.
     // 6. the child stacking contexts with stack level 0 and the positioned descendants with stack level 0.
-    Optional<HitTestResult> result;
     for_each_in_subtree_within_same_stacking_context_in_reverse(paintable(), [&](Paintable const& paintable) {
     for_each_in_subtree_within_same_stacking_context_in_reverse(paintable(), [&](Paintable const& paintable) {
+        VERIFY(!should_exit);
         if (!paintable.is_paintable_box())
         if (!paintable.is_paintable_box())
             return TraversalDecision::Continue;
             return TraversalDecision::Continue;
 
 
         auto const& paintable_box = verify_cast<PaintableBox>(paintable);
         auto const& paintable_box = verify_cast<PaintableBox>(paintable);
 
 
         auto const& z_index = paintable_box.computed_values().z_index();
         auto const& z_index = paintable_box.computed_values().z_index();
-        if (z_index.value_or(0) == 0 && paintable_box.is_positioned() && !paintable_box.stacking_context()) {
-            auto candidate = paintable_box.hit_test(transformed_position, type);
-            if (candidate.has_value() && candidate->paintable->visible_for_hit_testing()) {
-                result = move(candidate);
+        auto positioned_element_without_stacking_context = paintable_box.is_positioned() && !paintable_box.stacking_context();
+        if (z_index.value_or(0) == 0 && (positioned_element_without_stacking_context || paintable_box.layout_node().is_grid_item())) {
+            if (paintable_box.hit_test(transformed_position, type, callback) == TraversalDecision::Break) {
+                should_exit = true;
                 return TraversalDecision::Break;
                 return TraversalDecision::Break;
             }
             }
         }
         }
 
 
         if (paintable_box.stacking_context()) {
         if (paintable_box.stacking_context()) {
             if (z_index.value_or(0) == 0) {
             if (z_index.value_or(0) == 0) {
-                auto candidate = paintable_box.stacking_context()->hit_test(transformed_position, type);
-                if (candidate.has_value() && candidate->paintable->visible_for_hit_testing()) {
-                    result = move(candidate);
+                if (paintable_box.stacking_context()->hit_test(transformed_position, type, callback) == TraversalDecision::Break) {
+                    should_exit = true;
                     return TraversalDecision::Break;
                     return TraversalDecision::Break;
                 }
                 }
             }
             }
@@ -419,50 +419,47 @@ Optional<HitTestResult> StackingContext::hit_test(CSSPixelPoint position, HitTes
 
 
         return TraversalDecision::Continue;
         return TraversalDecision::Continue;
     });
     });
-    if (result.has_value())
-        return result;
+
+    if (should_exit)
+        return TraversalDecision::Break;
 
 
     // 5. the in-flow, inline-level, non-positioned descendants, including inline tables and inline blocks.
     // 5. the in-flow, inline-level, non-positioned descendants, including inline tables and inline blocks.
     if (paintable().layout_node().children_are_inline() && is<Layout::BlockContainer>(paintable().layout_node())) {
     if (paintable().layout_node().children_are_inline() && is<Layout::BlockContainer>(paintable().layout_node())) {
-        auto result = paintable_box().hit_test(transformed_position, type);
-        if (result.has_value() && result->paintable->visible_for_hit_testing())
-            return result;
+        if (paintable_box().hit_test(transformed_position, type, callback) == TraversalDecision::Break)
+            return TraversalDecision::Break;
     }
     }
 
 
     // 4. the non-positioned floats.
     // 4. the non-positioned floats.
     for_each_in_subtree_within_same_stacking_context_in_reverse(paintable(), [&](Paintable const& paintable) {
     for_each_in_subtree_within_same_stacking_context_in_reverse(paintable(), [&](Paintable const& paintable) {
+        VERIFY(!should_exit);
         if (!paintable.is_paintable_box())
         if (!paintable.is_paintable_box())
             return TraversalDecision::Continue;
             return TraversalDecision::Continue;
 
 
         auto const& paintable_box = verify_cast<PaintableBox>(paintable);
         auto const& paintable_box = verify_cast<PaintableBox>(paintable);
         if (paintable_box.is_floating()) {
         if (paintable_box.is_floating()) {
-            if (auto candidate = paintable_box.hit_test(transformed_position, type); candidate.has_value()) {
-                result = move(candidate);
+            if (paintable_box.hit_test(transformed_position, type, callback) == TraversalDecision::Break) {
+                should_exit = true;
                 return TraversalDecision::Break;
                 return TraversalDecision::Break;
             }
             }
         }
         }
         return TraversalDecision::Continue;
         return TraversalDecision::Continue;
     });
     });
-    if (result.has_value() && result->paintable->visible_for_hit_testing())
-        return result;
+
+    if (should_exit)
+        return TraversalDecision::Break;
 
 
     // 3. the in-flow, non-inline-level, non-positioned descendants.
     // 3. the in-flow, non-inline-level, non-positioned descendants.
     if (!paintable().layout_node().children_are_inline()) {
     if (!paintable().layout_node().children_are_inline()) {
-        for_each_in_subtree_within_same_stacking_context_in_reverse(paintable(), [&](Paintable const& paintable) {
-            if (!paintable.is_paintable_box())
-                return TraversalDecision::Continue;
-
-            auto const& paintable_box = verify_cast<PaintableBox>(paintable);
-            if (!paintable_box.is_absolutely_positioned() && !paintable_box.is_floating()) {
-                if (auto candidate = paintable_box.hit_test(transformed_position, type); candidate.has_value()) {
-                    result = move(candidate);
+        for (auto const* child = paintable().last_child(); child; child = child->previous_sibling()) {
+            if (!child->is_paintable_box())
+                continue;
+
+            auto const& paintable_box = verify_cast<PaintableBox>(*child);
+            if (!paintable_box.is_absolutely_positioned() && !paintable_box.is_floating() && !paintable_box.stacking_context()) {
+                if (paintable_box.hit_test(transformed_position, type, callback) == TraversalDecision::Break)
                     return TraversalDecision::Break;
                     return TraversalDecision::Break;
-                }
             }
             }
-            return TraversalDecision::Continue;
-        });
-        if (result.has_value() && result->paintable->visible_for_hit_testing())
-            return result;
+        }
     }
     }
 
 
     // 2. the child stacking contexts with negative stack levels (most negative first).
     // 2. the child stacking contexts with negative stack levels (most negative first).
@@ -471,21 +468,20 @@ Optional<HitTestResult> StackingContext::hit_test(CSSPixelPoint position, HitTes
         auto const& child = *m_children[i];
         auto const& child = *m_children[i];
         if (child.paintable().computed_values().z_index().value_or(0) >= 0)
         if (child.paintable().computed_values().z_index().value_or(0) >= 0)
             break;
             break;
-        auto result = child.hit_test(transformed_position, type);
-        if (result.has_value() && result->paintable->visible_for_hit_testing())
-            return result;
+        if (child.hit_test(transformed_position, type, callback) == TraversalDecision::Break)
+            return TraversalDecision::Break;
     }
     }
 
 
     // 1. the background and borders of the element forming the stacking context.
     // 1. the background and borders of the element forming the stacking context.
     if (paintable().is_paintable_box()) {
     if (paintable().is_paintable_box()) {
         if (paintable_box().absolute_border_box_rect().contains(transformed_position.x(), transformed_position.y())) {
         if (paintable_box().absolute_border_box_rect().contains(transformed_position.x(), transformed_position.y())) {
-            return HitTestResult {
-                .paintable = const_cast<PaintableBox&>(paintable_box()),
-            };
+            auto hit_test_result = HitTestResult { .paintable = const_cast<PaintableBox&>(paintable_box()) };
+            if (callback(hit_test_result) == TraversalDecision::Break)
+                return TraversalDecision::Break;
         }
         }
     }
     }
 
 
-    return {};
+    return TraversalDecision::Continue;
 }
 }
 
 
 void StackingContext::dump(int indent) const
 void StackingContext::dump(int indent) const

+ 2 - 1
Userland/Libraries/LibWeb/Painting/StackingContext.h

@@ -35,7 +35,8 @@ public:
     static void paint_node_as_stacking_context(Paintable const&, PaintContext&);
     static void paint_node_as_stacking_context(Paintable const&, PaintContext&);
     static void paint_descendants(PaintContext&, Paintable const&, StackingContextPaintPhase);
     static void paint_descendants(PaintContext&, Paintable const&, StackingContextPaintPhase);
     void paint(PaintContext&) const;
     void paint(PaintContext&) const;
-    Optional<HitTestResult> hit_test(CSSPixelPoint, HitTestType) const;
+
+    [[nodiscard]] TraversalDecision hit_test(CSSPixelPoint, HitTestType, Function<TraversalDecision(HitTestResult)> const& callback) const;
 
 
     Gfx::AffineTransform affine_transform_matrix() const;
     Gfx::AffineTransform affine_transform_matrix() const;