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.
This commit is contained in:
Aliaksandr Kalenik 2024-08-24 19:20:31 +02:00 committed by Alexander Kalenik
commit 30b636e90b
Notes: github-actions[bot] 2024-08-30 17:03:56 +00:00
20 changed files with 705 additions and 2 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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