Bläddra i källkod

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
Aliaksandr Kalenik 1 år sedan
förälder
incheckning
556679fedd

+ 2 - 0
Tests/LibWeb/Text/expected/hit_testing/overflow-scroll.txt

@@ -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" >

+ 49 - 0
Tests/LibWeb/Text/input/hit_testing/overflow-scroll.html

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

+ 6 - 2
Userland/Libraries/LibWeb/Painting/InlinePaintable.cpp

@@ -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()) };
         }
     }
 

+ 2 - 0
Userland/Libraries/LibWeb/Painting/InlinePaintable.h

@@ -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;

+ 16 - 8
Userland/Libraries/LibWeb/Painting/PaintableBox.cpp

@@ -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 {};
 }

+ 4 - 0
Userland/Libraries/LibWeb/Painting/PaintableBox.h

@@ -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;

+ 2 - 0
Userland/Libraries/LibWeb/Painting/ViewportPaintable.cpp

@@ -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;
             }