1
0
mirror of https://github.com/SerenityOS/serenity synced 2024-07-09 03:50:45 +00:00

LibWeb: Account for scroll offset in hit-testing

The hit-testing position is now shifted by the scroll offsets before
performing any checks for containment. This is implemented by assigning
each PaintableBox/InlinePaintable an offset corresponding to the scroll
frame in which it is contained. The non-scroll-adjusted position is
still passed down when recursing to children because the assigned
offset accumulated for nested scroll frames.

With this change, hit testing works in the Inspector.
Fixes https://github.com/SerenityOS/serenity/issues/22068
This commit is contained in:
Aliaksandr Kalenik 2024-01-29 06:28:17 +01:00 committed by Andreas Kling
parent beb1c85b6b
commit 556679fedd
7 changed files with 81 additions and 10 deletions

View File

@ -0,0 +1,2 @@
Line 1 Line 2 Line 3 Line 4 Line 5 Line 6 Line 7 Line 8 Line 9 Line 10 Line 11 Line 12 Line 13 Line 14 Line 15 Line 16 Line 17 Line 18 Line 19 Line 20 <P id="line-6" >
<SPAN id="line-8" >

View File

@ -0,0 +1,49 @@
<script src="../include.js"></script>
<style type="text/css">
#container {
border: 1px solid black;
height: 200px;
overflow: scroll;
}
p {
margin: 0;
}
p:hover {
background-color: yellow;
}
</style>
<body>
<div id=container>
<p id="line-1">Line 1</p>
<p id="line-2">Line 2</p>
<p id="line-3">Line 3</p>
<p id="line-4">Line 4</p>
<p id="line-5">Line 5</p>
<p id="line-6">Line 6</p>
<p id="line-7">Line 7</p>
<span id="line-8">Line 8</span>
<p id="line-9">Line 9</p>
<p id="line-10">Line 10</p>
<p id="line-11">Line 11</p>
<p id="line-12">Line 12</p>
<p id="line-13">Line 13</p>
<p id="line-14">Line 14</p>
<p id="line-15">Line 15</p>
<p id="line-16">Line 16</p>
<p id="line-17">Line 17</p>
<p id="line-18">Line 18</p>
<p id="line-19">Line 19</p>
<p id="line-20">Line 20</p>
</div>
</body>
<script>
const scrollContainer = document.getElementById("container");
scrollContainer.scrollTop = 100;
test(() => {
printElement(internals.hitTest(10, 10).node.parentNode);
printElement(internals.hitTest(10, 30).node.parentNode);
});
</script>

View File

@ -182,15 +182,19 @@ void InlinePaintable::for_each_fragment(Callback callback) const
Optional<HitTestResult> InlinePaintable::hit_test(CSSPixelPoint position, HitTestType type) const
{
auto position_adjusted_by_scroll_offset = position;
if (m_enclosing_scroll_frame_offset.has_value())
position_adjusted_by_scroll_offset.translate_by(-m_enclosing_scroll_frame_offset.value());
for (auto& fragment : m_fragments) {
if (fragment.paintable().stacking_context())
continue;
auto fragment_absolute_rect = fragment.absolute_rect();
if (fragment_absolute_rect.contains(position)) {
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.x()) };
fragment.text_index_at(position_adjusted_by_scroll_offset.x()) };
}
}

View File

@ -38,6 +38,7 @@ public:
Vector<ShadowData> const& box_shadow_data() const { return m_box_shadow_data; }
void set_scroll_frame_id(int id) { m_scroll_frame_id = id; }
void set_enclosing_scroll_frame_offset(CSSPixelPoint offset) { m_enclosing_scroll_frame_offset = offset; }
void set_clip_rect(Optional<CSSPixelRect> rect) { m_clip_rect = rect; }
private:
@ -47,6 +48,7 @@ private:
void for_each_fragment(Callback) const;
Optional<int> m_scroll_frame_id;
Optional<CSSPixelPoint> m_enclosing_scroll_frame_offset;
Optional<CSSPixelRect> m_clip_rect;
Vector<ShadowData> m_box_shadow_data;

View File

@ -672,6 +672,10 @@ Layout::BlockContainer& PaintableWithLines::layout_box()
Optional<HitTestResult> PaintableBox::hit_test(CSSPixelPoint position, HitTestType type) const
{
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 {};
@ -680,7 +684,7 @@ Optional<HitTestResult> PaintableBox::hit_test(CSSPixelPoint position, HitTestTy
return stacking_context()->hit_test(position, type);
}
if (!absolute_border_box_rect().contains(position.x(), position.y()))
if (!absolute_border_box_rect().contains(position_adjusted_by_scroll_offset.x(), position_adjusted_by_scroll_offset.y()))
return {};
for (auto* child = first_child(); child; child = child->next_sibling()) {
@ -700,6 +704,10 @@ Optional<HitTestResult> PaintableBox::hit_test(CSSPixelPoint position, HitTestTy
Optional<HitTestResult> PaintableWithLines::hit_test(CSSPixelPoint position, HitTestType type) const
{
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);
@ -717,10 +725,10 @@ Optional<HitTestResult> PaintableWithLines::hit_test(CSSPixelPoint position, Hit
if (fragment.paintable().stacking_context())
continue;
auto fragment_absolute_rect = fragment.absolute_rect();
if (fragment_absolute_rect.contains(position)) {
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.x()) };
return HitTestResult { const_cast<Paintable&>(fragment.paintable()), fragment.text_index_at(position_adjusted_by_scroll_offset.x()) };
}
// 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.
@ -728,11 +736,11 @@ Optional<HitTestResult> PaintableWithLines::hit_test(CSSPixelPoint position, Hit
// The best candidate is either the end of the line above, the beginning of the line below, or the beginning or end of the current line.
// We arbitrarily choose to consider the end of the line above and ignore the beginning of the line below.
// If we knew the direction of selection, we could make a better choice.
if (fragment_absolute_rect.bottom() - 1 <= position.y()) { // fully below the fragment
if (fragment_absolute_rect.bottom() - 1 <= position_adjusted_by_scroll_offset.y()) { // fully below the fragment
last_good_candidate = HitTestResult { const_cast<Paintable&>(fragment.paintable()), fragment.start() + fragment.length() };
} else if (fragment_absolute_rect.top() <= position.y()) { // vertically within the fragment
if (position.x() < fragment_absolute_rect.left()) { // left of the fragment
if (!last_good_candidate.has_value()) { // first fragment of the line
} else if (fragment_absolute_rect.top() <= position_adjusted_by_scroll_offset.y()) { // vertically within the fragment
if (position_adjusted_by_scroll_offset.x() < fragment_absolute_rect.left()) { // left of the fragment
if (!last_good_candidate.has_value()) { // first fragment of the line
last_good_candidate = HitTestResult { const_cast<Paintable&>(fragment.paintable()), fragment.start() };
}
} else { // right of the fragment
@ -743,7 +751,7 @@ 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.x(), position.y()))
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 {};
}

View File

@ -195,6 +195,7 @@ public:
void set_clip_rect(Optional<CSSPixelRect> rect) { m_clip_rect = rect; }
void set_scroll_frame_id(int id) { m_scroll_frame_id = id; }
void set_enclosing_scroll_frame_offset(CSSPixelPoint offset) { m_enclosing_scroll_frame_offset = offset; }
void set_corner_clip_radii(CornerRadii const& corner_radii) { m_corner_clip_radii = corner_radii; }
protected:
@ -208,6 +209,8 @@ protected:
virtual CSSPixelRect compute_absolute_rect() const;
virtual CSSPixelRect compute_absolute_paint_rect() const;
Optional<CSSPixelPoint> enclosing_scroll_frame_offset() const { return m_enclosing_scroll_frame_offset; }
private:
[[nodiscard]] virtual bool is_paintable_box() const final { return true; }
@ -224,6 +227,7 @@ private:
Optional<CSSPixelRect> m_clip_rect;
Optional<int> m_scroll_frame_id;
Optional<CSSPixelPoint> m_enclosing_scroll_frame_offset;
Optional<CornerRadii> m_corner_clip_radii;
Optional<BordersDataWithElementKind> m_override_borders_data;

View File

@ -82,9 +82,11 @@ void ViewportPaintable::assign_scroll_frame_ids(HashMap<Painting::PaintableBox c
if (paintable.is_paintable_box()) {
auto const& paintable_box = static_cast<PaintableBox const&>(paintable);
const_cast<PaintableBox&>(paintable_box).set_scroll_frame_id(scroll_frame_id->id);
const_cast<PaintableBox&>(paintable_box).set_enclosing_scroll_frame_offset(scroll_frame_id->offset);
} else if (paintable.is_inline_paintable()) {
auto const& inline_paintable = static_cast<InlinePaintable const&>(paintable);
const_cast<InlinePaintable&>(inline_paintable).set_scroll_frame_id(scroll_frame_id->id);
const_cast<InlinePaintable&>(inline_paintable).set_enclosing_scroll_frame_offset(scroll_frame_id->offset);
}
break;
}