فهرست منبع

LibWeb: Add "position: sticky" support

Sticky positioning is implemented by modifying the algorithm for
assigning and refreshing scroll frames. Now, elements with
"position: sticky" are assigned their own scroll frame, and their
position is refreshed independently from regular scroll boxes.
Refreshing the scroll offsets for sticky boxes does not require display
list invalidation.

A separate hash map is used for the scroll frames of sticky boxes. This
is necessary because a single paintable box can have two scroll frames
if it 1) has "position: sticky" and 2) contains scrollable overflow.
Aliaksandr Kalenik 10 ماه پیش
والد
کامیت
30b636e90b

+ 53 - 0
Tests/LibWeb/Ref/position-sticky-bottom.html

@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<link rel="match" href="reference/position-sticky-bottom-ref.html" />
+<style>
+    .scrollable-container {
+        width: 300px;
+        height: 300px;
+        overflow-y: scroll;
+        border: 5px solid red;
+    }
+
+    .sticky-box {
+        position: sticky;
+        bottom: 50px;
+        width: 240px;
+        height: 80px;
+        background: #3498db;
+        color: white;
+    }
+
+    .box {
+        height: 500px;
+        background-color: orange;
+    }
+</style>
+
+<div class="scrollable-container" id="sticky-is-below-scrollport">
+    <div class="box"></div>
+    <div class="sticky-box">I stick within this scrollable box!</div>
+    <div class="box"></div>
+</div>
+
+<div class="scrollable-container" id="sticky-is-inside-scrollport">
+    <div class="box"></div>
+    <div class="sticky-box">I stick within this scrollable box!</div>
+    <div class="box"></div>
+</div>
+
+<div class="scrollable-container" id="sticky-is-above-scrollport">
+    <div class="box"></div>
+    <div class="sticky-box">I stick within this scrollable box!</div>
+    <div class="box"></div>
+</div>
+
+<script>
+    const stickyIsBelowScrollport = document.getElementById("sticky-is-below-scrollport");
+    stickyIsBelowScrollport.scrollTop = 0;
+
+    const stickyIsInsideScrollport = document.getElementById("sticky-is-inside-scrollport");
+    stickyIsInsideScrollport.scrollTop = 390;
+
+    const stickyIsAboveScrollport = document.getElementById("sticky-is-above-scrollport");
+    stickyIsAboveScrollport.scrollTop = 780;
+</script>

+ 56 - 0
Tests/LibWeb/Ref/position-sticky-left.html

@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<link rel="match" href="reference/position-sticky-left-ref.html" />
+<style>
+    * {
+        scrollbar-width: none;
+    }
+
+    .scroll-container {
+        width: 500px;
+        overflow-x: scroll;
+        white-space: nowrap;
+        background-color: #f0f0f0;
+        display: grid;
+        grid-template-columns: 1000px 300px 1000px;
+        border: 5px solid yellowgreen;
+    }
+
+    .section {
+        height: 200px;
+        background-color: orangered;
+    }
+
+    .sticky-element {
+        position: sticky;
+        left: 0;
+        background-color: blueviolet;
+        height: 200px;
+        line-height: 200px;
+        color: white;
+    }
+</style>
+<div class="scroll-container" id="a">
+    <div class="section"></div>
+    <div class="sticky-element"></div>
+    <div class="section"></div>
+</div>
+<div class="scroll-container" id="b">
+    <div class="section"></div>
+    <div class="sticky-element"></div>
+    <div class="section"></div>
+</div>
+<div class="scroll-container" id="c">
+    <div class="section"></div>
+    <div class="sticky-element"></div>
+    <div class="section"></div>
+</div>
+<script>
+    const a = document.getElementById("a");
+    a.scrollLeft = 0;
+
+    const b = document.getElementById("b");
+    b.scrollLeft = 900;
+
+    const c = document.getElementById("c");
+    c.scrollLeft = 1400;
+</script>

+ 56 - 0
Tests/LibWeb/Ref/position-sticky-right.html

@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<link rel="match" href="reference/position-sticky-right-ref.html" />
+<style>
+    * {
+        scrollbar-width: none;
+    }
+
+    .scroll-container {
+        width: 500px;
+        overflow-x: scroll;
+        white-space: nowrap;
+        background-color: #f0f0f0;
+        display: grid;
+        grid-template-columns: 1000px 300px 1000px;
+        border: 5px solid yellowgreen;
+    }
+
+    .section {
+        height: 200px;
+        background-color: orangered;
+    }
+
+    .sticky-element {
+        position: sticky;
+        right: 0;
+        background-color: blueviolet;
+        height: 200px;
+        line-height: 200px;
+        color: white;
+    }
+</style>
+<div class="scroll-container" id="a">
+    <div class="section"></div>
+    <div class="sticky-element"></div>
+    <div class="section"></div>
+</div>
+<div class="scroll-container" id="b">
+    <div class="section"></div>
+    <div class="sticky-element"></div>
+    <div class="section"></div>
+</div>
+<div class="scroll-container" id="c">
+    <div class="section"></div>
+    <div class="sticky-element"></div>
+    <div class="section"></div>
+</div>
+<script>
+    const a = document.getElementById("a");
+    a.scrollLeft = 0;
+
+    const b = document.getElementById("b");
+    b.scrollLeft = 900;
+
+    const c = document.getElementById("c");
+    c.scrollLeft = 1400;
+</script>

+ 60 - 0
Tests/LibWeb/Ref/position-sticky-should-stay-within-containing-block.html

@@ -0,0 +1,60 @@
+<!DOCTYPE html>
+<link rel="match" href="reference/position-sticky-should-stay-within-containing-block-ref.html" />
+<style>
+    .scrollable-container {
+        width: 300px;
+        height: 300px;
+        overflow-y: scroll;
+        border: 5px solid red;
+    }
+
+    .sticky-box {
+        position: sticky;
+        top: 0;
+        bottom: 0;
+        width: 240px;
+        height: 80px;
+        background: #3498db;
+        color: white;
+    }
+
+    .box {
+        height: 500px;
+        background-color: orange;
+    }
+</style>
+
+<div class="scrollable-container" id="sticky-is-below-scrollport">
+    <div class="box"></div>
+    <div>
+        <div class="sticky-box">I stick within this scrollable box!</div>
+    </div>
+    <div class="box"></div>
+</div>
+
+<div class="scrollable-container" id="sticky-is-inside-scrollport">
+    <div class="box"></div>
+    <div>
+        <div class="sticky-box">I stick within this scrollable box!</div>
+    </div>
+    <div class="box"></div>
+</div>
+
+<div class="scrollable-container" id="sticky-is-above-scrollport">
+    <div class="box"></div>
+    <div>
+        <div class="sticky-box">I stick within this scrollable box!</div>
+    </div>
+    <div class="box"></div>
+</div>
+
+<script>
+    const stickyIsBelowScrollport = document.getElementById("sticky-is-below-scrollport");
+    stickyIsBelowScrollport.scrollTop = 0;
+
+    const stickyIsInsideScrollport = document.getElementById("sticky-is-inside-scrollport");
+    stickyIsInsideScrollport.scrollTop = 390;
+
+    const stickyIsAboveScrollport = document.getElementById("sticky-is-above-scrollport");
+    stickyIsAboveScrollport.scrollTop = 780;
+</script>

+ 53 - 0
Tests/LibWeb/Ref/position-sticky-top.html

@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<link rel="match" href="reference/position-sticky-top-ref.html" />
+<style>
+    .scrollable-container {
+        width: 300px;
+        height: 300px;
+        overflow-y: scroll;
+        border: 5px solid red;
+    }
+
+    .sticky-box {
+        position: sticky;
+        top: 50px;
+        width: 240px;
+        height: 80px;
+        background: #3498db;
+        color: white;
+    }
+
+    .box {
+        height: 500px;
+        background-color: orange;
+    }
+</style>
+
+<div class="scrollable-container" id="sticky-is-below-scrollport">
+    <div class="box"></div>
+    <div class="sticky-box">I stick within this scrollable box!</div>
+    <div class="box"></div>
+</div>
+
+<div class="scrollable-container" id="sticky-is-inside-scrollport">
+    <div class="box"></div>
+    <div class="sticky-box">I stick within this scrollable box!</div>
+    <div class="box"></div>
+</div>
+
+<div class="scrollable-container" id="sticky-is-above-scrollport">
+    <div class="box"></div>
+    <div class="sticky-box">I stick within this scrollable box!</div>
+    <div class="box"></div>
+</div>
+
+<script>
+    const stickyIsBelowScrollport = document.getElementById("sticky-is-below-scrollport");
+    stickyIsBelowScrollport.scrollTop = 0;
+
+    const stickyIsInsideScrollport = document.getElementById("sticky-is-inside-scrollport");
+    stickyIsInsideScrollport.scrollTop = 390;
+
+    const stickyIsAboveScrollport = document.getElementById("sticky-is-above-scrollport");
+    stickyIsAboveScrollport.scrollTop = 780;
+</script>

+ 58 - 0
Tests/LibWeb/Ref/reference/position-sticky-bottom-ref.html

@@ -0,0 +1,58 @@
+<!DOCTYPE html>
+<style>
+    .scrollable-container {
+        width: 300px;
+        height: 300px;
+        overflow-y: scroll;
+        border: 5px solid red;
+        position: relative;
+    }
+
+    .sticky-box {
+        width: 240px;
+        height: 80px;
+        background: #3498db;
+        color: white;
+    }
+
+    .box {
+        height: 500px;
+        background-color: orange;
+    }
+
+    .fill-abspos-box-space {
+        height: 80px;
+    }
+</style>
+
+<div class="scrollable-container" id="sticky-is-below-scrollport">
+    <div class="box"></div>
+    <div class="sticky-box" style="position: absolute; bottom: 50px">
+        I stick within this scrollable box!
+    </div>
+    <div class="fill-abspos-box-space"></div>
+    <div class="box"></div>
+</div>
+
+<div class="scrollable-container" id="sticky-is-inside-scrollport">
+    <div class="box"></div>
+    <div class="sticky-box">I stick within this scrollable box!</div>
+    <div class="box"></div>
+</div>
+
+<div class="scrollable-container" id="sticky-is-above-scrollport">
+    <div class="box"></div>
+    <div class="sticky-box">I stick within this scrollable box!</div>
+    <div class="box"></div>
+</div>
+
+<script>
+    const stickyIsBelowScrollport = document.getElementById("sticky-is-below-scrollport");
+    stickyIsBelowScrollport.scrollTop = 0;
+
+    const stickyIsInsideScrollport = document.getElementById("sticky-is-inside-scrollport");
+    stickyIsInsideScrollport.scrollTop = 390;
+
+    const stickyIsAboveScrollport = document.getElementById("sticky-is-above-scrollport");
+    stickyIsAboveScrollport.scrollTop = 780;
+</script>

+ 43 - 0
Tests/LibWeb/Ref/reference/position-sticky-left-ref.html

@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<style>
+    * {
+        scrollbar-width: none;
+    }
+
+    .scroll-container {
+        width: 500px;
+        overflow-x: scroll;
+        white-space: nowrap;
+        background-color: #f0f0f0;
+        display: flex;
+        border: 5px solid yellowgreen;
+    }
+
+    .section {
+        flex: 0 0 1000px;
+        height: 200px;
+        background-color: orangered;
+    }
+
+    .sticky-element {
+        position: sticky;
+        left: 0;
+        background-color: blueviolet;
+        flex: 0 0 300px;
+        height: 200px;
+        line-height: 200px;
+        color: white;
+    }
+</style>
+<div class="scroll-container" id="a">
+    <div class="section"></div>
+</div>
+<div class="scroll-container" id="b">
+    <div class="section" style="flex-basis: 100px"></div>
+    <div class="sticky-element"></div>
+    <div class="section" style="flex-basis: 100px"></div>
+</div>
+<div class="scroll-container" id="c">
+    <div class="sticky-element"></div>
+    <div class="section"></div>
+</div>

+ 43 - 0
Tests/LibWeb/Ref/reference/position-sticky-right-ref.html

@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<style>
+    * {
+        scrollbar-width: none;
+    }
+
+    .scroll-container {
+        width: 500px;
+        overflow-x: scroll;
+        white-space: nowrap;
+        background-color: #f0f0f0;
+        display: flex;
+        border: 5px solid yellowgreen;
+    }
+
+    .section {
+        flex: 0 0 1000px;
+        height: 200px;
+        background-color: orangered;
+    }
+
+    .sticky-element {
+        position: sticky;
+        left: 0;
+        background-color: blueviolet;
+        flex: 0 0 300px;
+        height: 200px;
+        line-height: 200px;
+        color: white;
+    }
+</style>
+<div class="scroll-container" id="a">
+    <div class="section" style="flex-basis: 200px"></div>
+    <div class="sticky-element"></div>
+</div>
+<div class="scroll-container" id="b">
+    <div class="section" style="flex-basis: 100px"></div>
+    <div class="sticky-element"></div>
+    <div class="section" style="flex-basis: 100px"></div>
+</div>
+<div class="scroll-container" id="c">
+    <div class="section"></div>
+</div>

+ 51 - 0
Tests/LibWeb/Ref/reference/position-sticky-should-stay-within-containing-block-ref.html

@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<style>
+    .scrollable-container {
+        width: 300px;
+        height: 300px;
+        overflow-y: scroll;
+        border: 5px solid red;
+        position: relative;
+    }
+
+    .sticky-box {
+        width: 240px;
+        height: 80px;
+        background: #3498db;
+        color: white;
+    }
+
+    .box {
+        height: 500px;
+        background-color: orange;
+    }
+</style>
+
+<div class="scrollable-container" id="sticky-is-below-scrollport">
+    <div class="box"></div>
+    <div class="sticky-box">I stick within this scrollable box!</div>
+    <div class="box"></div>
+</div>
+
+<div class="scrollable-container" id="sticky-is-inside-scrollport">
+    <div class="box"></div>
+    <div class="sticky-box">I stick within this scrollable box!</div>
+    <div class="box"></div>
+</div>
+
+<div class="scrollable-container" id="sticky-is-above-scrollport">
+    <div class="box"></div>
+    <div class="sticky-box">I stick within this scrollable box!</div>
+    <div class="box"></div>
+</div>
+
+<script>
+    const stickyIsBelowScrollport = document.getElementById("sticky-is-below-scrollport");
+    stickyIsBelowScrollport.scrollTop = 0;
+
+    const stickyIsInsideScrollport = document.getElementById("sticky-is-inside-scrollport");
+    stickyIsInsideScrollport.scrollTop = 390;
+
+    const stickyIsAboveScrollport = document.getElementById("sticky-is-above-scrollport");
+    stickyIsAboveScrollport.scrollTop = 780;
+</script>

+ 58 - 0
Tests/LibWeb/Ref/reference/position-sticky-top-ref.html

@@ -0,0 +1,58 @@
+<!DOCTYPE html>
+<style>
+    .scrollable-container {
+        width: 300px;
+        height: 300px;
+        overflow-y: scroll;
+        border: 5px solid red;
+        position: relative;
+    }
+
+    .sticky-box {
+        width: 240px;
+        height: 80px;
+        background: #3498db;
+        color: white;
+    }
+
+    .box {
+        height: 500px;
+        background-color: orange;
+    }
+
+    .fill-abspos-box-space {
+        height: 80px;
+    }
+</style>
+
+<div class="scrollable-container" id="sticky-is-below-scrollport">
+    <div class="box"></div>
+    <div class="sticky-box">I stick within this scrollable box!</div>
+    <div class="box"></div>
+</div>
+
+<div class="scrollable-container" id="sticky-is-inside-scrollport">
+    <div class="box"></div>
+    <div class="sticky-box">I stick within this scrollable box!</div>
+    <div class="box"></div>
+</div>
+
+<div class="scrollable-container" id="sticky-is-above-scrollport">
+    <div class="box"></div>
+    <div class="sticky-box" style="position: absolute; top: 830px">
+        I stick within this scrollable box!
+    </div>
+    <div class="fill-abspos-box-space"></div>
+    <div class="box"></div>
+</div>
+
+<script>
+    const stickyIsBelowScrollport = document.getElementById("sticky-is-below-scrollport");
+    stickyIsBelowScrollport.scrollTop = 0;
+
+    const stickyIsInsideScrollport = document.getElementById("sticky-is-inside-scrollport");
+    stickyIsInsideScrollport.scrollTop = 390;
+
+    const stickyIsAboveScrollport = document.getElementById("sticky-is-above-scrollport");
+    stickyIsAboveScrollport.scrollTop = 780;
+</script>

+ 4 - 1
Userland/Libraries/LibWeb/DOM/Document.cpp

@@ -5454,10 +5454,13 @@ RefPtr<Painting::DisplayList> Document::record_display_list(PaintConfig config)
     display_list->set_device_pixels_per_css_pixel(page().client().device_pixels_per_css_pixel());
 
     Vector<RefPtr<Painting::ScrollFrame>> scroll_state;
-    scroll_state.resize(viewport_paintable.scroll_state.size());
+    scroll_state.resize(viewport_paintable.scroll_state.size() + viewport_paintable.sticky_state.size());
     for (auto& [_, scrollable_frame] : viewport_paintable.scroll_state) {
         scroll_state[scrollable_frame->id] = scrollable_frame;
     }
+    for (auto& [_, scrollable_frame] : viewport_paintable.sticky_state) {
+        scroll_state[scrollable_frame->id] = scrollable_frame;
+    }
 
     display_list->set_scroll_state(move(scroll_state));
 

+ 18 - 0
Userland/Libraries/LibWeb/Layout/LayoutState.cpp

@@ -376,6 +376,24 @@ void LayoutState::commit(Box& root)
         auto& paintable_box = const_cast<Painting::PaintableBox&>(*box.paintable_box());
         if (!paintable_box.scroll_offset().is_zero())
             paintable_box.set_scroll_offset(paintable_box.scroll_offset());
+
+        if (box.is_sticky_position()) {
+            auto sticky_insets = make<Painting::PaintableBox::StickyInsets>();
+            auto const& inset = box.computed_values().inset();
+            if (!inset.top().is_auto()) {
+                sticky_insets->top = inset.top().to_px(box, used_values.containing_block_used_values()->content_height());
+            }
+            if (!inset.right().is_auto()) {
+                sticky_insets->right = inset.right().to_px(box, used_values.containing_block_used_values()->content_width());
+            }
+            if (!inset.bottom().is_auto()) {
+                sticky_insets->bottom = inset.bottom().to_px(box, used_values.containing_block_used_values()->content_height());
+            }
+            if (!inset.left().is_auto()) {
+                sticky_insets->left = inset.left().to_px(box, used_values.containing_block_used_values()->content_width());
+            }
+            paintable_box.set_sticky_insets(move(sticky_insets));
+        }
     }
 }
 

+ 8 - 0
Userland/Libraries/LibWeb/Layout/Node.cpp

@@ -262,6 +262,14 @@ bool Node::is_fixed_position() const
     return position == CSS::Positioning::Fixed;
 }
 
+bool Node::is_sticky_position() const
+{
+    if (!has_style())
+        return false;
+    auto position = computed_values().position();
+    return position == CSS::Positioning::Sticky;
+}
+
 NodeWithStyle::NodeWithStyle(DOM::Document& document, DOM::Node* node, NonnullRefPtr<CSS::StyleProperties> computed_style)
     : Node(document, node)
     , m_computed_values(make<CSS::ComputedValues>())

+ 1 - 0
Userland/Libraries/LibWeb/Layout/Node.h

@@ -122,6 +122,7 @@ public:
     bool is_positioned() const;
     bool is_absolutely_positioned() const;
     bool is_fixed_position() const;
+    bool is_sticky_position() const;
 
     bool is_flex_item() const { return m_is_flex_item; }
     void set_flex_item(bool b) { m_is_flex_item = b; }

+ 1 - 0
Userland/Libraries/LibWeb/Painting/Paintable.cpp

@@ -26,6 +26,7 @@ Paintable::Paintable(Layout::Node const& layout_node)
     }
 
     m_fixed_position = computed_values.position() == CSS::Positioning::Fixed;
+    m_sticky_position = computed_values.position() == CSS::Positioning::Sticky;
     m_absolutely_positioned = computed_values.position() == CSS::Positioning::Absolute;
     m_floating = layout_node.is_floating();
     m_inline = layout_node.is_inline();

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

@@ -54,6 +54,7 @@ public:
     [[nodiscard]] bool is_visible() const;
     [[nodiscard]] bool is_positioned() const { return m_positioned; }
     [[nodiscard]] bool is_fixed_position() const { return m_fixed_position; }
+    [[nodiscard]] bool is_sticky_position() const { return m_sticky_position; }
     [[nodiscard]] bool is_absolutely_positioned() const { return m_absolutely_positioned; }
     [[nodiscard]] bool is_floating() const { return m_floating; }
     [[nodiscard]] bool is_inline() const { return m_inline; }
@@ -258,6 +259,7 @@ private:
 
     bool m_positioned : 1 { false };
     bool m_fixed_position : 1 { false };
+    bool m_sticky_position : 1 { false };
     bool m_absolutely_positioned : 1 { false };
     bool m_floating : 1 { false };
     bool m_inline : 1 { false };

+ 26 - 0
Userland/Libraries/LibWeb/Painting/PaintableBox.cpp

@@ -243,6 +243,11 @@ bool PaintableBox::is_scrollable(ScrollDirection direction) const
     return overflow == CSS::Overflow::Scroll;
 }
 
+bool PaintableBox::is_scrollable() const
+{
+    return is_scrollable(ScrollDirection::Horizontal) || is_scrollable(ScrollDirection::Vertical);
+}
+
 static constexpr CSSPixels scrollbar_thumb_thickness = 8;
 
 Optional<CSSPixelRect> PaintableBox::scroll_thumb_rect(ScrollDirection direction) const
@@ -1111,4 +1116,25 @@ RefPtr<ScrollFrame const> PaintableBox::nearest_scroll_frame() const
     return nullptr;
 }
 
+CSSPixelRect PaintableBox::padding_box_rect_relative_to_nearest_scrollable_ancestor() const
+{
+    auto result = absolute_padding_box_rect();
+    auto const* nearest_scrollable_ancestor = this->nearest_scrollable_ancestor();
+    if (nearest_scrollable_ancestor) {
+        result.set_location(result.location() - nearest_scrollable_ancestor->absolute_rect().top_left());
+    }
+    return result;
+}
+
+PaintableBox const* PaintableBox::nearest_scrollable_ancestor() const
+{
+    auto const* paintable = this->containing_block();
+    while (paintable) {
+        if (paintable->is_scrollable())
+            return paintable;
+        paintable = paintable->containing_block();
+    }
+    return nullptr;
+}
+
 }

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

@@ -212,6 +212,20 @@ public:
 
     RefPtr<ScrollFrame const> nearest_scroll_frame() const;
 
+    CSSPixelRect padding_box_rect_relative_to_nearest_scrollable_ancestor() const;
+    PaintableBox const* nearest_scrollable_ancestor() const;
+
+    struct StickyInsets {
+        Optional<CSSPixels> top;
+        Optional<CSSPixels> right;
+        Optional<CSSPixels> bottom;
+        Optional<CSSPixels> left;
+    };
+    StickyInsets const& sticky_insets() const { return *m_sticky_insets; }
+    void set_sticky_insets(OwnPtr<StickyInsets> sticky_insets) { m_sticky_insets = move(sticky_insets); }
+
+    [[nodiscard]] bool is_scrollable() const;
+
 protected:
     explicit PaintableBox(Layout::Box const&);
 
@@ -270,6 +284,8 @@ private:
     Optional<ScrollDirection> m_scroll_thumb_dragging_direction;
 
     ResolvedBackground m_resolved_background;
+
+    OwnPtr<StickyInsets> m_sticky_insets;
 };
 
 class PaintableWithLines : public PaintableBox {

+ 97 - 1
Userland/Libraries/LibWeb/Painting/ViewportPaintable.cpp

@@ -68,13 +68,31 @@ void ViewportPaintable::assign_scroll_frames()
 {
     int next_id = 0;
     for_each_in_inclusive_subtree_of_type<PaintableBox>([&](auto& paintable_box) {
+        RefPtr<ScrollFrame> sticky_scroll_frame;
+        if (paintable_box.is_sticky_position()) {
+            sticky_scroll_frame = adopt_ref(*new ScrollFrame());
+            sticky_scroll_frame->id = next_id++;
+            auto const* nearest_scrollable_ancestor = paintable_box.nearest_scrollable_ancestor();
+            if (nearest_scrollable_ancestor) {
+                sticky_scroll_frame->parent = nearest_scrollable_ancestor->nearest_scroll_frame();
+            }
+            const_cast<PaintableBox&>(paintable_box).set_enclosing_scroll_frame(sticky_scroll_frame);
+            const_cast<PaintableBox&>(paintable_box).set_own_scroll_frame(sticky_scroll_frame);
+            sticky_state.set(paintable_box, sticky_scroll_frame);
+        }
+
         if (paintable_box.has_scrollable_overflow() || is<ViewportPaintable>(paintable_box)) {
             auto scroll_frame = adopt_ref(*new ScrollFrame());
             scroll_frame->id = next_id++;
-            scroll_frame->parent = paintable_box.nearest_scroll_frame();
+            if (sticky_scroll_frame) {
+                scroll_frame->parent = sticky_scroll_frame;
+            } else {
+                scroll_frame->parent = paintable_box.nearest_scroll_frame();
+            }
             paintable_box.set_own_scroll_frame(scroll_frame);
             scroll_state.set(paintable_box, move(scroll_frame));
         }
+
         return TraversalDecision::Continue;
     });
 
@@ -82,6 +100,9 @@ void ViewportPaintable::assign_scroll_frames()
         if (paintable.is_fixed_position()) {
             return TraversalDecision::Continue;
         }
+        if (paintable.is_sticky_position()) {
+            return TraversalDecision::Continue;
+        }
         for (auto block = paintable.containing_block(); block; block = block->containing_block()) {
             if (auto scroll_frame = block->own_scroll_frame(); scroll_frame) {
                 if (paintable.is_paintable_box()) {
@@ -160,6 +181,80 @@ void ViewportPaintable::refresh_scroll_state()
         return;
     m_needs_to_refresh_scroll_state = false;
 
+    for (auto& it : sticky_state) {
+        auto const& sticky_box = *it.key;
+        auto& scroll_frame = *it.value;
+        auto const& sticky_insets = sticky_box.sticky_insets();
+
+        auto const* nearest_scrollable_ancestor = sticky_box.nearest_scrollable_ancestor();
+        if (!nearest_scrollable_ancestor) {
+            continue;
+        }
+
+        // Min and max offsets are needed to clamp the sticky box's position to stay within bounds of containing block.
+        CSSPixels min_y_offset_relative_to_nearest_scrollable_ancestor;
+        CSSPixels max_y_offset_relative_to_nearest_scrollable_ancestor;
+        CSSPixels min_x_offset_relative_to_nearest_scrollable_ancestor;
+        CSSPixels max_x_offset_relative_to_nearest_scrollable_ancestor;
+        auto const* containing_block_of_sticky_box = sticky_box.containing_block();
+        if (containing_block_of_sticky_box->is_scrollable()) {
+            min_y_offset_relative_to_nearest_scrollable_ancestor = 0;
+            max_y_offset_relative_to_nearest_scrollable_ancestor = containing_block_of_sticky_box->scrollable_overflow_rect()->height() - sticky_box.absolute_padding_box_rect().height();
+            min_x_offset_relative_to_nearest_scrollable_ancestor = 0;
+            max_x_offset_relative_to_nearest_scrollable_ancestor = containing_block_of_sticky_box->scrollable_overflow_rect()->width() - sticky_box.absolute_padding_box_rect().width();
+        } else {
+            auto containing_block_rect_relative_to_nearest_scrollable_ancestor = containing_block_of_sticky_box->absolute_padding_box_rect().translated(-nearest_scrollable_ancestor->absolute_rect().top_left());
+            min_y_offset_relative_to_nearest_scrollable_ancestor = containing_block_rect_relative_to_nearest_scrollable_ancestor.top();
+            max_y_offset_relative_to_nearest_scrollable_ancestor = containing_block_rect_relative_to_nearest_scrollable_ancestor.bottom() - sticky_box.absolute_padding_box_rect().height();
+            min_x_offset_relative_to_nearest_scrollable_ancestor = containing_block_rect_relative_to_nearest_scrollable_ancestor.left();
+            max_x_offset_relative_to_nearest_scrollable_ancestor = containing_block_rect_relative_to_nearest_scrollable_ancestor.right() - sticky_box.absolute_padding_box_rect().width();
+        }
+
+        auto padding_rect_of_sticky_box_relative_to_nearest_scrollable_ancestor = sticky_box.padding_box_rect_relative_to_nearest_scrollable_ancestor();
+
+        // By default, the sticky box is shifted by the scroll offset of the nearest scrollable ancestor.
+        CSSPixelPoint sticky_offset = -nearest_scrollable_ancestor->scroll_offset();
+        CSSPixelRect const scrollport_rect { nearest_scrollable_ancestor->scroll_offset(), nearest_scrollable_ancestor->absolute_rect().size() };
+
+        if (sticky_insets.top.has_value()) {
+            auto top_inset = sticky_insets.top.value();
+            auto stick_to_top_scroll_offset_threshold = padding_rect_of_sticky_box_relative_to_nearest_scrollable_ancestor.top() - top_inset;
+            if (scrollport_rect.top() > stick_to_top_scroll_offset_threshold) {
+                sticky_offset.translate_by({ 0, -padding_rect_of_sticky_box_relative_to_nearest_scrollable_ancestor.top() });
+                sticky_offset.translate_by({ 0, min(scrollport_rect.top() + top_inset, max_y_offset_relative_to_nearest_scrollable_ancestor) });
+            }
+        }
+
+        if (sticky_insets.left.has_value()) {
+            auto left_inset = sticky_insets.left.value();
+            auto stick_to_left_scroll_offset_threshold = padding_rect_of_sticky_box_relative_to_nearest_scrollable_ancestor.left() - left_inset;
+            if (scrollport_rect.left() > stick_to_left_scroll_offset_threshold) {
+                sticky_offset.translate_by({ -padding_rect_of_sticky_box_relative_to_nearest_scrollable_ancestor.left(), 0 });
+                sticky_offset.translate_by({ min(scrollport_rect.left() + left_inset, max_x_offset_relative_to_nearest_scrollable_ancestor), 0 });
+            }
+        }
+
+        if (sticky_insets.bottom.has_value()) {
+            auto bottom_inset = sticky_insets.bottom.value();
+            auto stick_to_bottom_scroll_offset_threshold = padding_rect_of_sticky_box_relative_to_nearest_scrollable_ancestor.bottom() + bottom_inset;
+            if (scrollport_rect.bottom() < stick_to_bottom_scroll_offset_threshold) {
+                sticky_offset.translate_by({ 0, -padding_rect_of_sticky_box_relative_to_nearest_scrollable_ancestor.top() });
+                sticky_offset.translate_by({ 0, max(scrollport_rect.bottom() - sticky_box.absolute_padding_box_rect().height() - bottom_inset, min_y_offset_relative_to_nearest_scrollable_ancestor) });
+            }
+        }
+
+        if (sticky_insets.right.has_value()) {
+            auto right_inset = sticky_insets.right.value();
+            auto stick_to_right_scroll_offset_threshold = padding_rect_of_sticky_box_relative_to_nearest_scrollable_ancestor.right() + right_inset;
+            if (scrollport_rect.right() < stick_to_right_scroll_offset_threshold) {
+                sticky_offset.translate_by({ -padding_rect_of_sticky_box_relative_to_nearest_scrollable_ancestor.left(), 0 });
+                sticky_offset.translate_by({ max(scrollport_rect.right() - sticky_box.absolute_padding_box_rect().width() - right_inset, min_x_offset_relative_to_nearest_scrollable_ancestor), 0 });
+            }
+        }
+
+        scroll_frame.own_offset = sticky_offset;
+    }
+
     for (auto& it : scroll_state) {
         auto const& paintable_box = *it.key;
         auto& scroll_frame = *it.value;
@@ -255,6 +350,7 @@ void ViewportPaintable::visit_edges(Visitor& visitor)
 {
     Base::visit_edges(visitor);
     visitor.visit(scroll_state);
+    visitor.visit(sticky_state);
     visitor.visit(clip_state);
 }
 

+ 1 - 0
Userland/Libraries/LibWeb/Painting/ViewportPaintable.h

@@ -22,6 +22,7 @@ public:
     void build_stacking_context_tree_if_needed();
 
     HashMap<JS::GCPtr<PaintableBox const>, RefPtr<ScrollFrame>> scroll_state;
+    HashMap<JS::GCPtr<PaintableBox const>, RefPtr<ScrollFrame>> sticky_state;
     void assign_scroll_frames();
     void refresh_scroll_state();