mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-07-29 12:19:54 +00:00
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:
parent
866608532a
commit
30b636e90b
Notes:
github-actions[bot]
2024-08-30 17:03:56 +00:00
Author: https://github.com/kalenikaliaksandr
Commit: 30b636e90b
Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/1229
20 changed files with 705 additions and 2 deletions
53
Tests/LibWeb/Ref/position-sticky-bottom.html
Normal file
53
Tests/LibWeb/Ref/position-sticky-bottom.html
Normal 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>
|
56
Tests/LibWeb/Ref/position-sticky-left.html
Normal file
56
Tests/LibWeb/Ref/position-sticky-left.html
Normal 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>
|
56
Tests/LibWeb/Ref/position-sticky-right.html
Normal file
56
Tests/LibWeb/Ref/position-sticky-right.html
Normal 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>
|
|
@ -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
Tests/LibWeb/Ref/position-sticky-top.html
Normal file
53
Tests/LibWeb/Ref/position-sticky-top.html
Normal 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>
|
58
Tests/LibWeb/Ref/reference/position-sticky-bottom-ref.html
Normal file
58
Tests/LibWeb/Ref/reference/position-sticky-bottom-ref.html
Normal 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>
|
43
Tests/LibWeb/Ref/reference/position-sticky-left-ref.html
Normal file
43
Tests/LibWeb/Ref/reference/position-sticky-left-ref.html
Normal 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>
|
43
Tests/LibWeb/Ref/reference/position-sticky-right-ref.html
Normal file
43
Tests/LibWeb/Ref/reference/position-sticky-right-ref.html
Normal 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>
|
|
@ -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
Tests/LibWeb/Ref/reference/position-sticky-top-ref.html
Normal file
58
Tests/LibWeb/Ref/reference/position-sticky-top-ref.html
Normal 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>
|
|
@ -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));
|
||||
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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>())
|
||||
|
|
|
@ -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; }
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue