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.
This commit is contained in:
Aliaksandr Kalenik 2024-02-13 21:34:07 +01:00 committed by Andreas Kling
parent 15d151ee66
commit 9c99182b1e
Notes: sideshowbarker 2024-07-16 20:12:13 +09:00
11 changed files with 120 additions and 104 deletions

View file

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

View file

@ -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))
return {};
return TraversalDecision::Continue;
auto position_adjusted_by_scroll_offset = position;
if (enclosing_scroll_frame_offset().has_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())
continue;
auto fragment_absolute_rect = fragment.absolute_rect();
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) {
if (should_exit)
return;
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

View file

@ -32,7 +32,7 @@ public:
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); }
Vector<ShadowData> const& box_shadow_data() const { return m_box_shadow_data; }

View file

@ -98,9 +98,9 @@ bool Paintable::handle_mousewheel(Badge<EventHandler>, CSSPixelPoint, unsigned,
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()

View file

@ -143,7 +143,7 @@ public:
virtual void apply_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; }

View file

@ -719,17 +719,17 @@ Layout::BlockContainer& PaintableWithLines::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))
return {};
return TraversalDecision::Continue;
auto position_adjusted_by_scroll_offset = position;
if (enclosing_scroll_frame_offset().has_value())
position_adjusted_by_scroll_offset.translate_by(-enclosing_scroll_frame_offset().value());
if (!is_visible())
return {};
return TraversalDecision::Continue;
if (layout_box().is_viewport()) {
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.refresh_scroll_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()))
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())
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;
if (!result->paintable->visible_for_hit_testing())
continue;
return result;
if (child->hit_test(position, type, callback) == TraversalDecision::Break)
return TraversalDecision::Break;
}
if (!visible_for_hit_testing())
return {};
return TraversalDecision::Continue;
return HitTestResult { const_cast<PaintableBox&>(*this) };
return callback(HitTestResult { const_cast<PaintableBox&>(*this) });
}
Optional<HitTestResult> PaintableWithLines::hit_test(CSSPixelPoint position, HitTestType type) const
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;
}
TraversalDecision PaintableWithLines::hit_test(CSSPixelPoint position, HitTestType type, Function<TraversalDecision(HitTestResult)> const& callback) const
{
if (clip_rect().has_value() && !clip_rect()->contains(position))
return {};
return TraversalDecision::Continue;
auto position_adjusted_by_scroll_offset = position;
if (enclosing_scroll_frame_offset().has_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;
@ -785,9 +794,11 @@ Optional<HitTestResult> PaintableWithLines::hit_test(CSSPixelPoint position, Hit
continue;
auto fragment_absolute_rect = fragment.absolute_rect();
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.
@ -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

View file

@ -136,7 +136,8 @@ public:
virtual void apply_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;
@ -286,7 +287,7 @@ public:
virtual void paint(PaintContext&, PaintPhase) const override;
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
{

View file

@ -26,15 +26,14 @@ Layout::SVGGraphicsBox const& SVGPathPaintable::layout_box() const
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());
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)

View file

@ -17,7 +17,7 @@ class SVGPathPaintable final : public SVGGraphicsPaintable {
public:
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;

View file

@ -356,10 +356,10 @@ static TraversalDecision for_each_in_subtree_within_same_stacking_context_in_rev
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())
return {};
return TraversalDecision::Continue;
CSSPixelPoint transform_origin { 0, 0 };
if (paintable().is_paintable_box())
@ -385,33 +385,33 @@ Optional<HitTestResult> StackingContext::hit_test(CSSPixelPoint position, HitTes
auto const& child = *m_children[i];
if (child.paintable().computed_values().z_index().value_or(0) <= 0)
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.
Optional<HitTestResult> result;
for_each_in_subtree_within_same_stacking_context_in_reverse(paintable(), [&](Paintable const& paintable) {
VERIFY(!should_exit);
if (!paintable.is_paintable_box())
return TraversalDecision::Continue;
auto const& paintable_box = verify_cast<PaintableBox>(paintable);
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;
}
}
if (paintable_box.stacking_context()) {
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;
}
}
@ -419,50 +419,47 @@ Optional<HitTestResult> StackingContext::hit_test(CSSPixelPoint position, HitTes
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.
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.
for_each_in_subtree_within_same_stacking_context_in_reverse(paintable(), [&](Paintable const& paintable) {
VERIFY(!should_exit);
if (!paintable.is_paintable_box())
return TraversalDecision::Continue;
auto const& paintable_box = verify_cast<PaintableBox>(paintable);
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::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.
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;
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>(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);
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::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).
@ -471,21 +468,20 @@ Optional<HitTestResult> StackingContext::hit_test(CSSPixelPoint position, HitTes
auto const& child = *m_children[i];
if (child.paintable().computed_values().z_index().value_or(0) >= 0)
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.
if (paintable().is_paintable_box()) {
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

View file

@ -35,7 +35,8 @@ public:
static void paint_node_as_stacking_context(Paintable const&, PaintContext&);
static void paint_descendants(PaintContext&, Paintable const&, StackingContextPaintPhase);
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;