From 30b636e90b2b42c859d60e3aaf6187c80f4c669b Mon Sep 17 00:00:00 2001 From: Aliaksandr Kalenik Date: Sat, 24 Aug 2024 19:20:31 +0200 Subject: [PATCH] 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. --- Tests/LibWeb/Ref/position-sticky-bottom.html | 53 ++++++++++ Tests/LibWeb/Ref/position-sticky-left.html | 56 +++++++++++ Tests/LibWeb/Ref/position-sticky-right.html | 56 +++++++++++ ...y-should-stay-within-containing-block.html | 60 ++++++++++++ Tests/LibWeb/Ref/position-sticky-top.html | 53 ++++++++++ .../reference/position-sticky-bottom-ref.html | 58 +++++++++++ .../reference/position-sticky-left-ref.html | 43 ++++++++ .../reference/position-sticky-right-ref.html | 43 ++++++++ ...ould-stay-within-containing-block-ref.html | 51 ++++++++++ .../reference/position-sticky-top-ref.html | 58 +++++++++++ Userland/Libraries/LibWeb/DOM/Document.cpp | 5 +- .../Libraries/LibWeb/Layout/LayoutState.cpp | 18 ++++ Userland/Libraries/LibWeb/Layout/Node.cpp | 8 ++ Userland/Libraries/LibWeb/Layout/Node.h | 1 + .../Libraries/LibWeb/Painting/Paintable.cpp | 1 + .../Libraries/LibWeb/Painting/Paintable.h | 2 + .../LibWeb/Painting/PaintableBox.cpp | 26 +++++ .../Libraries/LibWeb/Painting/PaintableBox.h | 16 +++ .../LibWeb/Painting/ViewportPaintable.cpp | 98 ++++++++++++++++++- .../LibWeb/Painting/ViewportPaintable.h | 1 + 20 files changed, 705 insertions(+), 2 deletions(-) create mode 100644 Tests/LibWeb/Ref/position-sticky-bottom.html create mode 100644 Tests/LibWeb/Ref/position-sticky-left.html create mode 100644 Tests/LibWeb/Ref/position-sticky-right.html create mode 100644 Tests/LibWeb/Ref/position-sticky-should-stay-within-containing-block.html create mode 100644 Tests/LibWeb/Ref/position-sticky-top.html create mode 100644 Tests/LibWeb/Ref/reference/position-sticky-bottom-ref.html create mode 100644 Tests/LibWeb/Ref/reference/position-sticky-left-ref.html create mode 100644 Tests/LibWeb/Ref/reference/position-sticky-right-ref.html create mode 100644 Tests/LibWeb/Ref/reference/position-sticky-should-stay-within-containing-block-ref.html create mode 100644 Tests/LibWeb/Ref/reference/position-sticky-top-ref.html diff --git a/Tests/LibWeb/Ref/position-sticky-bottom.html b/Tests/LibWeb/Ref/position-sticky-bottom.html new file mode 100644 index 00000000000..30aaabd294b --- /dev/null +++ b/Tests/LibWeb/Ref/position-sticky-bottom.html @@ -0,0 +1,53 @@ + + + + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + diff --git a/Tests/LibWeb/Ref/position-sticky-left.html b/Tests/LibWeb/Ref/position-sticky-left.html new file mode 100644 index 00000000000..5cb1f33e072 --- /dev/null +++ b/Tests/LibWeb/Ref/position-sticky-left.html @@ -0,0 +1,56 @@ + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ diff --git a/Tests/LibWeb/Ref/position-sticky-right.html b/Tests/LibWeb/Ref/position-sticky-right.html new file mode 100644 index 00000000000..e57d19491a1 --- /dev/null +++ b/Tests/LibWeb/Ref/position-sticky-right.html @@ -0,0 +1,56 @@ + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ diff --git a/Tests/LibWeb/Ref/position-sticky-should-stay-within-containing-block.html b/Tests/LibWeb/Ref/position-sticky-should-stay-within-containing-block.html new file mode 100644 index 00000000000..af3952c8f95 --- /dev/null +++ b/Tests/LibWeb/Ref/position-sticky-should-stay-within-containing-block.html @@ -0,0 +1,60 @@ + + + + +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ + diff --git a/Tests/LibWeb/Ref/position-sticky-top.html b/Tests/LibWeb/Ref/position-sticky-top.html new file mode 100644 index 00000000000..7ecb3996160 --- /dev/null +++ b/Tests/LibWeb/Ref/position-sticky-top.html @@ -0,0 +1,53 @@ + + + + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + diff --git a/Tests/LibWeb/Ref/reference/position-sticky-bottom-ref.html b/Tests/LibWeb/Ref/reference/position-sticky-bottom-ref.html new file mode 100644 index 00000000000..1f4dcbf4505 --- /dev/null +++ b/Tests/LibWeb/Ref/reference/position-sticky-bottom-ref.html @@ -0,0 +1,58 @@ + + + +
+
+ +
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + diff --git a/Tests/LibWeb/Ref/reference/position-sticky-left-ref.html b/Tests/LibWeb/Ref/reference/position-sticky-left-ref.html new file mode 100644 index 00000000000..9a0d3f320e2 --- /dev/null +++ b/Tests/LibWeb/Ref/reference/position-sticky-left-ref.html @@ -0,0 +1,43 @@ + + +
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Tests/LibWeb/Ref/reference/position-sticky-right-ref.html b/Tests/LibWeb/Ref/reference/position-sticky-right-ref.html new file mode 100644 index 00000000000..8074c8905b6 --- /dev/null +++ b/Tests/LibWeb/Ref/reference/position-sticky-right-ref.html @@ -0,0 +1,43 @@ + + +
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Tests/LibWeb/Ref/reference/position-sticky-should-stay-within-containing-block-ref.html b/Tests/LibWeb/Ref/reference/position-sticky-should-stay-within-containing-block-ref.html new file mode 100644 index 00000000000..068a7331361 --- /dev/null +++ b/Tests/LibWeb/Ref/reference/position-sticky-should-stay-within-containing-block-ref.html @@ -0,0 +1,51 @@ + + + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + diff --git a/Tests/LibWeb/Ref/reference/position-sticky-top-ref.html b/Tests/LibWeb/Ref/reference/position-sticky-top-ref.html new file mode 100644 index 00000000000..296901ca26c --- /dev/null +++ b/Tests/LibWeb/Ref/reference/position-sticky-top-ref.html @@ -0,0 +1,58 @@ + + + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ + diff --git a/Userland/Libraries/LibWeb/DOM/Document.cpp b/Userland/Libraries/LibWeb/DOM/Document.cpp index b3acd46cb9d..64fd3af8935 100644 --- a/Userland/Libraries/LibWeb/DOM/Document.cpp +++ b/Userland/Libraries/LibWeb/DOM/Document.cpp @@ -5454,10 +5454,13 @@ RefPtr Document::record_display_list(PaintConfig config) display_list->set_device_pixels_per_css_pixel(page().client().device_pixels_per_css_pixel()); Vector> 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)); diff --git a/Userland/Libraries/LibWeb/Layout/LayoutState.cpp b/Userland/Libraries/LibWeb/Layout/LayoutState.cpp index d324ab4df29..7dd63dd875a 100644 --- a/Userland/Libraries/LibWeb/Layout/LayoutState.cpp +++ b/Userland/Libraries/LibWeb/Layout/LayoutState.cpp @@ -376,6 +376,24 @@ void LayoutState::commit(Box& root) auto& paintable_box = const_cast(*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(); + 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)); + } } } diff --git a/Userland/Libraries/LibWeb/Layout/Node.cpp b/Userland/Libraries/LibWeb/Layout/Node.cpp index 5eddfc09f5e..fffae81ee66 100644 --- a/Userland/Libraries/LibWeb/Layout/Node.cpp +++ b/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 computed_style) : Node(document, node) , m_computed_values(make()) diff --git a/Userland/Libraries/LibWeb/Layout/Node.h b/Userland/Libraries/LibWeb/Layout/Node.h index 5598ab0722f..67a43190054 100644 --- a/Userland/Libraries/LibWeb/Layout/Node.h +++ b/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; } diff --git a/Userland/Libraries/LibWeb/Painting/Paintable.cpp b/Userland/Libraries/LibWeb/Painting/Paintable.cpp index 79dbbe456e8..28d8c6169bd 100644 --- a/Userland/Libraries/LibWeb/Painting/Paintable.cpp +++ b/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(); diff --git a/Userland/Libraries/LibWeb/Painting/Paintable.h b/Userland/Libraries/LibWeb/Painting/Paintable.h index 112e549cec4..2b166ebc095 100644 --- a/Userland/Libraries/LibWeb/Painting/Paintable.h +++ b/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 }; diff --git a/Userland/Libraries/LibWeb/Painting/PaintableBox.cpp b/Userland/Libraries/LibWeb/Painting/PaintableBox.cpp index 55713b2f48a..b5ca0d22cc5 100644 --- a/Userland/Libraries/LibWeb/Painting/PaintableBox.cpp +++ b/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 PaintableBox::scroll_thumb_rect(ScrollDirection direction) const @@ -1111,4 +1116,25 @@ RefPtr 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; +} + } diff --git a/Userland/Libraries/LibWeb/Painting/PaintableBox.h b/Userland/Libraries/LibWeb/Painting/PaintableBox.h index e854c1425f5..f975deeb9cc 100644 --- a/Userland/Libraries/LibWeb/Painting/PaintableBox.h +++ b/Userland/Libraries/LibWeb/Painting/PaintableBox.h @@ -212,6 +212,20 @@ public: RefPtr nearest_scroll_frame() const; + CSSPixelRect padding_box_rect_relative_to_nearest_scrollable_ancestor() const; + PaintableBox const* nearest_scrollable_ancestor() const; + + struct StickyInsets { + Optional top; + Optional right; + Optional bottom; + Optional left; + }; + StickyInsets const& sticky_insets() const { return *m_sticky_insets; } + void set_sticky_insets(OwnPtr 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 m_scroll_thumb_dragging_direction; ResolvedBackground m_resolved_background; + + OwnPtr m_sticky_insets; }; class PaintableWithLines : public PaintableBox { diff --git a/Userland/Libraries/LibWeb/Painting/ViewportPaintable.cpp b/Userland/Libraries/LibWeb/Painting/ViewportPaintable.cpp index 64473423e56..7ded677639e 100644 --- a/Userland/Libraries/LibWeb/Painting/ViewportPaintable.cpp +++ b/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([&](auto& paintable_box) { + RefPtr 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(paintable_box).set_enclosing_scroll_frame(sticky_scroll_frame); + const_cast(paintable_box).set_own_scroll_frame(sticky_scroll_frame); + sticky_state.set(paintable_box, sticky_scroll_frame); + } + if (paintable_box.has_scrollable_overflow() || is(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); } diff --git a/Userland/Libraries/LibWeb/Painting/ViewportPaintable.h b/Userland/Libraries/LibWeb/Painting/ViewportPaintable.h index 096c1316879..70444b3bd78 100644 --- a/Userland/Libraries/LibWeb/Painting/ViewportPaintable.h +++ b/Userland/Libraries/LibWeb/Painting/ViewportPaintable.h @@ -22,6 +22,7 @@ public: void build_stacking_context_tree_if_needed(); HashMap, RefPtr> scroll_state; + HashMap, RefPtr> sticky_state; void assign_scroll_frames(); void refresh_scroll_state();