1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
8 * compute sticky positioning, both during reflow and when the scrolling
12 #include "StickyScrollContainer.h"
14 #include "mozilla/OverflowChangedTracker.h"
16 #include "nsIFrameInlines.h"
17 #include "nsIScrollableFrame.h"
18 #include "nsLayoutUtils.h"
22 NS_DECLARE_FRAME_PROPERTY_DELETABLE(StickyScrollContainerProperty
,
23 StickyScrollContainer
)
25 StickyScrollContainer::StickyScrollContainer(nsIScrollableFrame
* aScrollFrame
)
26 : mScrollFrame(aScrollFrame
) {
27 mScrollFrame
->AddScrollPositionListener(this);
30 StickyScrollContainer::~StickyScrollContainer() {
31 mScrollFrame
->RemoveScrollPositionListener(this);
35 StickyScrollContainer
* StickyScrollContainer::GetStickyScrollContainerForFrame(
37 nsIScrollableFrame
* scrollFrame
= nsLayoutUtils::GetNearestScrollableFrame(
38 aFrame
->GetParent(), nsLayoutUtils::SCROLLABLE_SAME_DOC
|
39 nsLayoutUtils::SCROLLABLE_STOP_AT_PAGE
|
40 nsLayoutUtils::SCROLLABLE_INCLUDE_HIDDEN
);
42 // We might not find any, for instance in the case of
43 // <html style="position: fixed">
46 nsIFrame
* frame
= do_QueryFrame(scrollFrame
);
47 StickyScrollContainer
* s
=
48 frame
->GetProperty(StickyScrollContainerProperty());
50 s
= new StickyScrollContainer(scrollFrame
);
51 frame
->SetProperty(StickyScrollContainerProperty(), s
);
57 void StickyScrollContainer::NotifyReparentedFrameAcrossScrollFrameBoundary(
58 nsIFrame
* aFrame
, nsIFrame
* aOldParent
) {
59 nsIScrollableFrame
* oldScrollFrame
= nsLayoutUtils::GetNearestScrollableFrame(
60 aOldParent
, nsLayoutUtils::SCROLLABLE_SAME_DOC
|
61 nsLayoutUtils::SCROLLABLE_INCLUDE_HIDDEN
);
62 if (!oldScrollFrame
) {
63 // XXX maybe aFrame has sticky descendants that can be sticky now, but
64 // we aren't going to handle that.
68 StickyScrollContainer
* oldSSC
=
69 static_cast<nsIFrame
*>(do_QueryFrame(oldScrollFrame
))
70 ->GetProperty(StickyScrollContainerProperty());
72 // aOldParent had no sticky descendants, so aFrame doesn't have any sticky
73 // descendants, and we're done here.
77 auto i
= oldSSC
->mFrames
.Length();
79 nsIFrame
* f
= oldSSC
->mFrames
[i
];
80 StickyScrollContainer
* newSSC
= GetStickyScrollContainerForFrame(f
);
81 if (newSSC
!= oldSSC
) {
82 oldSSC
->RemoveFrame(f
);
91 StickyScrollContainer
*
92 StickyScrollContainer::GetStickyScrollContainerForScrollFrame(
94 return aFrame
->GetProperty(StickyScrollContainerProperty());
97 static nscoord
ComputeStickySideOffset(
98 Side aSide
, const StyleRect
<LengthPercentageOrAuto
>& aOffset
,
99 nscoord aPercentBasis
) {
100 auto& side
= aOffset
.Get(aSide
);
102 return NS_AUTOOFFSET
;
104 return nsLayoutUtils::ComputeCBDependentValue(aPercentBasis
, side
);
108 void StickyScrollContainer::ComputeStickyOffsets(nsIFrame
* aFrame
) {
109 nsIScrollableFrame
* scrollableFrame
=
110 nsLayoutUtils::GetNearestScrollableFrame(
111 aFrame
->GetParent(), nsLayoutUtils::SCROLLABLE_SAME_DOC
|
112 nsLayoutUtils::SCROLLABLE_INCLUDE_HIDDEN
);
114 if (!scrollableFrame
) {
119 nsSize scrollContainerSize
= scrollableFrame
->GetScrolledFrame()
120 ->GetContentRectRelativeToSelf()
123 nsMargin computedOffsets
;
124 const nsStylePosition
* position
= aFrame
->StylePosition();
126 computedOffsets
.left
= ComputeStickySideOffset(eSideLeft
, position
->mOffset
,
127 scrollContainerSize
.width
);
128 computedOffsets
.right
= ComputeStickySideOffset(eSideRight
, position
->mOffset
,
129 scrollContainerSize
.width
);
130 computedOffsets
.top
= ComputeStickySideOffset(eSideTop
, position
->mOffset
,
131 scrollContainerSize
.height
);
132 computedOffsets
.bottom
= ComputeStickySideOffset(
133 eSideBottom
, position
->mOffset
, scrollContainerSize
.height
);
136 nsMargin
* offsets
= aFrame
->GetProperty(nsIFrame::ComputedOffsetProperty());
138 *offsets
= computedOffsets
;
140 aFrame
->SetProperty(nsIFrame::ComputedOffsetProperty(),
141 new nsMargin(computedOffsets
));
145 static constexpr nscoord gUnboundedNegative
= nscoord_MIN
/ 2;
146 static constexpr nscoord gUnboundedExtent
= nscoord_MAX
;
147 static constexpr nscoord gUnboundedPositive
=
148 gUnboundedNegative
+ gUnboundedExtent
;
150 void StickyScrollContainer::ComputeStickyLimits(nsIFrame
* aFrame
,
152 nsRect
* aContain
) const {
153 NS_ASSERTION(nsLayoutUtils::IsFirstContinuationOrIBSplitSibling(aFrame
),
154 "Can't sticky position individual continuations");
156 aStick
->SetRect(gUnboundedNegative
, gUnboundedNegative
, gUnboundedExtent
,
158 aContain
->SetRect(gUnboundedNegative
, gUnboundedNegative
, gUnboundedExtent
,
161 const nsMargin
* computedOffsets
=
162 aFrame
->GetProperty(nsIFrame::ComputedOffsetProperty());
163 if (!computedOffsets
) {
164 // We haven't reflowed the scroll frame yet, so offsets haven't been
169 nsIFrame
* scrolledFrame
= mScrollFrame
->GetScrolledFrame();
170 nsIFrame
* cbFrame
= aFrame
->GetContainingBlock();
171 NS_ASSERTION(cbFrame
== scrolledFrame
||
172 nsLayoutUtils::IsProperAncestorFrame(scrolledFrame
, cbFrame
),
173 "Scroll frame should be an ancestor of the containing block");
176 nsLayoutUtils::GetAllInFlowRectsUnion(aFrame
, aFrame
->GetParent());
178 // FIXME(bug 1421660): Table row groups aren't supposed to be containing
179 // blocks, but we treat them as such (maybe it's the right thing to do!).
180 // Anyway, not having this basically disables position: sticky on table cells,
181 // which would be really unfortunate, and doesn't match what other browsers
183 if (cbFrame
!= scrolledFrame
&& cbFrame
->IsTableRowGroupFrame()) {
184 cbFrame
= cbFrame
->GetContainingBlock();
187 // Containing block limits for the position of aFrame relative to its parent.
188 // The margin box of the sticky element stays within the content box of the
189 // contaning-block element.
190 if (cbFrame
== scrolledFrame
) {
191 // cbFrame is the scrolledFrame, and it won't have continuations. Unlike the
192 // else clause, we consider scrollable overflow rect because and the union
193 // of its in-flow rects doesn't include the scrollable overflow area.
194 *aContain
= cbFrame
->ScrollableOverflowRectRelativeToSelf();
195 nsLayoutUtils::TransformRect(cbFrame
, aFrame
->GetParent(), *aContain
);
197 *aContain
= nsLayoutUtils::GetAllInFlowRectsUnion(
198 cbFrame
, aFrame
->GetParent(), nsLayoutUtils::RECTS_USE_CONTENT_BOX
);
201 nsRect marginRect
= nsLayoutUtils::GetAllInFlowRectsUnion(
202 aFrame
, aFrame
->GetParent(), nsLayoutUtils::RECTS_USE_MARGIN_BOX
);
204 // Deflate aContain by the difference between the union of aFrame's
205 // continuations' margin boxes and the union of their border boxes, so that
206 // by keeping aFrame within aContain, we keep the union of the margin boxes
207 // within the containing block's content box.
208 aContain
->Deflate(marginRect
- rect
);
210 // Deflate aContain by the border-box size, to form a constraint on the
211 // upper-left corner of aFrame and continuations.
212 aContain
->Deflate(nsMargin(0, rect
.width
, rect
.height
, 0));
214 nsMargin sfPadding
= scrolledFrame
->GetUsedPadding();
215 nsPoint sfOffset
= aFrame
->GetParent()->GetOffsetTo(scrolledFrame
);
218 if (computedOffsets
->top
!= NS_AUTOOFFSET
) {
219 aStick
->SetTopEdge(mScrollPosition
.y
+ sfPadding
.top
+
220 computedOffsets
->top
- sfOffset
.y
);
223 nsSize sfSize
= scrolledFrame
->GetContentRectRelativeToSelf().Size();
226 if (computedOffsets
->bottom
!= NS_AUTOOFFSET
&&
227 (computedOffsets
->top
== NS_AUTOOFFSET
||
228 rect
.height
<= sfSize
.height
- computedOffsets
->TopBottom())) {
229 aStick
->SetBottomEdge(mScrollPosition
.y
+ sfPadding
.top
+ sfSize
.height
-
230 computedOffsets
->bottom
- rect
.height
- sfOffset
.y
);
233 StyleDirection direction
= cbFrame
->StyleVisibility()->mDirection
;
236 if (computedOffsets
->left
!= NS_AUTOOFFSET
&&
237 (computedOffsets
->right
== NS_AUTOOFFSET
||
238 direction
== StyleDirection::Ltr
||
239 rect
.width
<= sfSize
.width
- computedOffsets
->LeftRight())) {
240 aStick
->SetLeftEdge(mScrollPosition
.x
+ sfPadding
.left
+
241 computedOffsets
->left
- sfOffset
.x
);
245 if (computedOffsets
->right
!= NS_AUTOOFFSET
&&
246 (computedOffsets
->left
== NS_AUTOOFFSET
||
247 direction
== StyleDirection::Rtl
||
248 rect
.width
<= sfSize
.width
- computedOffsets
->LeftRight())) {
249 aStick
->SetRightEdge(mScrollPosition
.x
+ sfPadding
.left
+ sfSize
.width
-
250 computedOffsets
->right
- rect
.width
- sfOffset
.x
);
253 // These limits are for the bounding box of aFrame's continuations. Convert
254 // to limits for aFrame itself.
255 nsPoint frameOffset
= aFrame
->GetPosition() - rect
.TopLeft();
256 aStick
->MoveBy(frameOffset
);
257 aContain
->MoveBy(frameOffset
);
260 nsPoint
StickyScrollContainer::ComputePosition(nsIFrame
* aFrame
) const {
263 ComputeStickyLimits(aFrame
, &stick
, &contain
);
265 nsPoint position
= aFrame
->GetNormalPosition();
267 // For each sticky direction (top, bottom, left, right), move the frame along
268 // the appropriate axis, based on the scroll position, but limit this to keep
269 // the element's margin box within the containing block.
270 position
.y
= std::max(position
.y
, std::min(stick
.y
, contain
.YMost()));
271 position
.y
= std::min(position
.y
, std::max(stick
.YMost(), contain
.y
));
272 position
.x
= std::max(position
.x
, std::min(stick
.x
, contain
.XMost()));
273 position
.x
= std::min(position
.x
, std::max(stick
.XMost(), contain
.x
));
278 bool StickyScrollContainer::IsStuckInYDirection(nsIFrame
* aFrame
) const {
279 nsPoint position
= ComputePosition(aFrame
);
280 return position
.y
!= aFrame
->GetNormalPosition().y
;
283 void StickyScrollContainer::GetScrollRanges(nsIFrame
* aFrame
,
284 nsRectAbsolute
* aOuter
,
285 nsRectAbsolute
* aInner
) const {
286 // We need to use the first in flow; continuation frames should not move
287 // relative to each other and should get identical scroll ranges.
288 // Also, ComputeStickyLimits requires this.
289 nsIFrame
* firstCont
=
290 nsLayoutUtils::FirstContinuationOrIBSplitSibling(aFrame
);
294 ComputeStickyLimits(firstCont
, &stickRect
, &containRect
);
296 nsRectAbsolute stick
= nsRectAbsolute::FromRect(stickRect
);
297 nsRectAbsolute contain
= nsRectAbsolute::FromRect(containRect
);
299 aOuter
->SetBox(gUnboundedNegative
, gUnboundedNegative
, gUnboundedPositive
,
301 aInner
->SetBox(gUnboundedNegative
, gUnboundedNegative
, gUnboundedPositive
,
304 const nsPoint normalPosition
= firstCont
->GetNormalPosition();
307 if (stick
.YMost() != gUnboundedPositive
) {
308 aOuter
->SetTopEdge(contain
.Y() - stick
.YMost());
309 aInner
->SetTopEdge(normalPosition
.y
- stick
.YMost());
312 if (stick
.Y() != gUnboundedNegative
) {
313 aInner
->SetBottomEdge(normalPosition
.y
- stick
.Y());
314 aOuter
->SetBottomEdge(contain
.YMost() - stick
.Y());
318 if (stick
.XMost() != gUnboundedPositive
) {
319 aOuter
->SetLeftEdge(contain
.X() - stick
.XMost());
320 aInner
->SetLeftEdge(normalPosition
.x
- stick
.XMost());
323 if (stick
.X() != gUnboundedNegative
) {
324 aInner
->SetRightEdge(normalPosition
.x
- stick
.X());
325 aOuter
->SetRightEdge(contain
.XMost() - stick
.X());
328 // Make sure |inner| does not extend outside of |outer|. (The consumers of
329 // the Layers API, to which this information is propagated, expect this
330 // invariant to hold.) The calculated value of |inner| can sometimes extend
331 // outside of |outer|, for example due to margin collapsing, since
332 // GetNormalPosition() returns the actual position after margin collapsing,
333 // while |contain| is calculated based on the frame's GetUsedMargin() which
334 // is pre-collapsing.
335 // Note that this doesn't necessarily solve all problems stemming from
336 // comparing pre- and post-collapsing margins (TODO: find a proper solution).
337 *aInner
= aInner
->Intersect(*aOuter
);
338 if (aInner
->IsEmpty()) {
339 // This might happen if aInner didn't intersect aOuter at all initially,
340 // in which case aInner is empty and outside aOuter. Make sure it doesn't
341 // extend outside aOuter.
342 *aInner
= aInner
->MoveInsideAndClamp(*aOuter
);
346 void StickyScrollContainer::PositionContinuations(nsIFrame
* aFrame
) {
347 NS_ASSERTION(nsLayoutUtils::IsFirstContinuationOrIBSplitSibling(aFrame
),
348 "Should be starting from the first continuation");
350 nsPoint translation
=
351 ComputePosition(aFrame
) - aFrame
->GetNormalPosition(&hadProperty
);
352 if (NS_WARN_IF(!hadProperty
)) {
353 // If the frame was never relatively positioned, don't move its position
354 // dynamically. There are a variety of frames for which `position` doesn't
355 // really apply like frames inside svg which would get here and be sticky
356 // only in one direction.
360 // Move all continuation frames by the same amount.
361 for (nsIFrame
* cont
= aFrame
; cont
;
362 cont
= nsLayoutUtils::GetNextContinuationOrIBSplitSibling(cont
)) {
363 cont
->SetPosition(cont
->GetNormalPosition() + translation
);
367 void StickyScrollContainer::UpdatePositions(nsPoint aScrollPosition
,
368 nsIFrame
* aSubtreeRoot
) {
371 nsIFrame
* scrollFrameAsFrame
= do_QueryFrame(mScrollFrame
);
372 NS_ASSERTION(!aSubtreeRoot
|| aSubtreeRoot
== scrollFrameAsFrame
,
373 "If reflowing, should be reflowing the scroll frame");
376 mScrollPosition
= aScrollPosition
;
378 OverflowChangedTracker oct
;
379 oct
.SetSubtreeRoot(aSubtreeRoot
);
380 for (nsTArray
<nsIFrame
*>::size_type i
= 0; i
< mFrames
.Length(); i
++) {
381 nsIFrame
* f
= mFrames
[i
];
382 if (!nsLayoutUtils::IsFirstContinuationOrIBSplitSibling(f
)) {
383 // This frame was added in nsIFrame::Init before we knew it wasn't
384 // the first ib-split-sibling.
385 mFrames
.RemoveElementAt(i
);
391 // Reflowing the scroll frame, so recompute offsets.
392 ComputeStickyOffsets(f
);
394 // mFrames will only contain first continuations, because we filter in
396 PositionContinuations(f
);
399 if (f
!= aSubtreeRoot
) {
400 for (nsIFrame
* cont
= f
; cont
;
401 cont
= nsLayoutUtils::GetNextContinuationOrIBSplitSibling(cont
)) {
402 oct
.AddFrame(cont
, OverflowChangedTracker::CHILDREN_CHANGED
);
409 void StickyScrollContainer::ScrollPositionWillChange(nscoord aX
, nscoord aY
) {}
411 void StickyScrollContainer::ScrollPositionDidChange(nscoord aX
, nscoord aY
) {
412 UpdatePositions(nsPoint(aX
, aY
), nullptr);
415 } // namespace mozilla