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