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
), mScrollPosition() {
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 nscoord gUnboundedNegative
= nscoord_MIN
/ 2;
146 static nscoord gUnboundedExtent
= nscoord_MAX
;
147 static nscoord gUnboundedPositive
= gUnboundedNegative
+ gUnboundedExtent
;
149 void StickyScrollContainer::ComputeStickyLimits(nsIFrame
* aFrame
,
151 nsRect
* aContain
) const {
152 NS_ASSERTION(nsLayoutUtils::IsFirstContinuationOrIBSplitSibling(aFrame
),
153 "Can't sticky position individual continuations");
155 aStick
->SetRect(gUnboundedNegative
, gUnboundedNegative
, gUnboundedExtent
,
157 aContain
->SetRect(gUnboundedNegative
, gUnboundedNegative
, gUnboundedExtent
,
160 const nsMargin
* computedOffsets
=
161 aFrame
->GetProperty(nsIFrame::ComputedOffsetProperty());
162 if (!computedOffsets
) {
163 // We haven't reflowed the scroll frame yet, so offsets haven't been
168 nsIFrame
* scrolledFrame
= mScrollFrame
->GetScrolledFrame();
169 nsIFrame
* cbFrame
= aFrame
->GetContainingBlock();
170 NS_ASSERTION(cbFrame
== scrolledFrame
||
171 nsLayoutUtils::IsProperAncestorFrame(scrolledFrame
, cbFrame
),
172 "Scroll frame should be an ancestor of the containing block");
175 nsLayoutUtils::GetAllInFlowRectsUnion(aFrame
, aFrame
->GetParent());
177 // FIXME(bug 1421660): Table row groups aren't supposed to be containing
178 // blocks, but we treat them as such (maybe it's the right thing to do!).
179 // Anyway, not having this basically disables position: sticky on table cells,
180 // which would be really unfortunate, and doesn't match what other browsers
182 if (cbFrame
!= scrolledFrame
&& cbFrame
->IsTableRowGroupFrame()) {
183 cbFrame
= cbFrame
->GetContainingBlock();
186 // Containing block limits for the position of aFrame relative to its parent.
187 // The margin box of the sticky element stays within the content box of the
188 // contaning-block element.
189 if (cbFrame
== scrolledFrame
) {
190 // cbFrame is the scrolledFrame, and it won't have continuations. Unlike the
191 // else clause, we consider scrollable overflow rect because and the union
192 // of its in-flow rects doesn't include the scrollable overflow area.
193 *aContain
= cbFrame
->ScrollableOverflowRectRelativeToSelf();
194 nsLayoutUtils::TransformRect(cbFrame
, aFrame
->GetParent(), *aContain
);
196 *aContain
= nsLayoutUtils::GetAllInFlowRectsUnion(
197 cbFrame
, aFrame
->GetParent(), nsLayoutUtils::RECTS_USE_CONTENT_BOX
);
200 nsRect marginRect
= nsLayoutUtils::GetAllInFlowRectsUnion(
201 aFrame
, aFrame
->GetParent(), nsLayoutUtils::RECTS_USE_MARGIN_BOX
);
203 // Deflate aContain by the difference between the union of aFrame's
204 // continuations' margin boxes and the union of their border boxes, so that
205 // by keeping aFrame within aContain, we keep the union of the margin boxes
206 // within the containing block's content box.
207 aContain
->Deflate(marginRect
- rect
);
209 // Deflate aContain by the border-box size, to form a constraint on the
210 // upper-left corner of aFrame and continuations.
211 aContain
->Deflate(nsMargin(0, rect
.width
, rect
.height
, 0));
213 nsMargin sfPadding
= scrolledFrame
->GetUsedPadding();
214 nsPoint sfOffset
= aFrame
->GetParent()->GetOffsetTo(scrolledFrame
);
217 if (computedOffsets
->top
!= NS_AUTOOFFSET
) {
218 aStick
->SetTopEdge(mScrollPosition
.y
+ sfPadding
.top
+
219 computedOffsets
->top
- sfOffset
.y
);
222 nsSize sfSize
= scrolledFrame
->GetContentRectRelativeToSelf().Size();
225 if (computedOffsets
->bottom
!= NS_AUTOOFFSET
&&
226 (computedOffsets
->top
== NS_AUTOOFFSET
||
227 rect
.height
<= sfSize
.height
- computedOffsets
->TopBottom())) {
228 aStick
->SetBottomEdge(mScrollPosition
.y
+ sfPadding
.top
+ sfSize
.height
-
229 computedOffsets
->bottom
- rect
.height
- sfOffset
.y
);
232 StyleDirection direction
= cbFrame
->StyleVisibility()->mDirection
;
235 if (computedOffsets
->left
!= NS_AUTOOFFSET
&&
236 (computedOffsets
->right
== NS_AUTOOFFSET
||
237 direction
== StyleDirection::Ltr
||
238 rect
.width
<= sfSize
.width
- computedOffsets
->LeftRight())) {
239 aStick
->SetLeftEdge(mScrollPosition
.x
+ sfPadding
.left
+
240 computedOffsets
->left
- sfOffset
.x
);
244 if (computedOffsets
->right
!= NS_AUTOOFFSET
&&
245 (computedOffsets
->left
== NS_AUTOOFFSET
||
246 direction
== StyleDirection::Rtl
||
247 rect
.width
<= sfSize
.width
- computedOffsets
->LeftRight())) {
248 aStick
->SetRightEdge(mScrollPosition
.x
+ sfPadding
.left
+ sfSize
.width
-
249 computedOffsets
->right
- rect
.width
- sfOffset
.x
);
252 // These limits are for the bounding box of aFrame's continuations. Convert
253 // to limits for aFrame itself.
254 nsPoint frameOffset
= aFrame
->GetPosition() - rect
.TopLeft();
255 aStick
->MoveBy(frameOffset
);
256 aContain
->MoveBy(frameOffset
);
259 nsPoint
StickyScrollContainer::ComputePosition(nsIFrame
* aFrame
) const {
262 ComputeStickyLimits(aFrame
, &stick
, &contain
);
264 nsPoint position
= aFrame
->GetNormalPosition();
266 // For each sticky direction (top, bottom, left, right), move the frame along
267 // the appropriate axis, based on the scroll position, but limit this to keep
268 // the element's margin box within the containing block.
269 position
.y
= std::max(position
.y
, std::min(stick
.y
, contain
.YMost()));
270 position
.y
= std::min(position
.y
, std::max(stick
.YMost(), contain
.y
));
271 position
.x
= std::max(position
.x
, std::min(stick
.x
, contain
.XMost()));
272 position
.x
= std::min(position
.x
, std::max(stick
.XMost(), contain
.x
));
277 bool StickyScrollContainer::IsStuckInYDirection(nsIFrame
* aFrame
) const {
278 nsPoint position
= ComputePosition(aFrame
);
279 return position
.y
!= aFrame
->GetNormalPosition().y
;
282 void StickyScrollContainer::GetScrollRanges(nsIFrame
* aFrame
,
283 nsRectAbsolute
* aOuter
,
284 nsRectAbsolute
* aInner
) const {
285 // We need to use the first in flow; continuation frames should not move
286 // relative to each other and should get identical scroll ranges.
287 // Also, ComputeStickyLimits requires this.
288 nsIFrame
* firstCont
=
289 nsLayoutUtils::FirstContinuationOrIBSplitSibling(aFrame
);
293 ComputeStickyLimits(firstCont
, &stickRect
, &containRect
);
295 nsRectAbsolute stick
= nsRectAbsolute::FromRect(stickRect
);
296 nsRectAbsolute contain
= nsRectAbsolute::FromRect(containRect
);
298 aOuter
->SetBox(gUnboundedNegative
, gUnboundedNegative
, gUnboundedPositive
,
300 aInner
->SetBox(gUnboundedNegative
, gUnboundedNegative
, gUnboundedPositive
,
303 const nsPoint normalPosition
= firstCont
->GetNormalPosition();
306 if (stick
.YMost() != gUnboundedPositive
) {
307 aOuter
->SetTopEdge(contain
.Y() - stick
.YMost());
308 aInner
->SetTopEdge(normalPosition
.y
- stick
.YMost());
311 if (stick
.Y() != gUnboundedNegative
) {
312 aInner
->SetBottomEdge(normalPosition
.y
- stick
.Y());
313 aOuter
->SetBottomEdge(contain
.YMost() - stick
.Y());
317 if (stick
.XMost() != gUnboundedPositive
) {
318 aOuter
->SetLeftEdge(contain
.X() - stick
.XMost());
319 aInner
->SetLeftEdge(normalPosition
.x
- stick
.XMost());
322 if (stick
.X() != gUnboundedNegative
) {
323 aInner
->SetRightEdge(normalPosition
.x
- stick
.X());
324 aOuter
->SetRightEdge(contain
.XMost() - stick
.X());
327 // Make sure |inner| does not extend outside of |outer|. (The consumers of
328 // the Layers API, to which this information is propagated, expect this
329 // invariant to hold.) The calculated value of |inner| can sometimes extend
330 // outside of |outer|, for example due to margin collapsing, since
331 // GetNormalPosition() returns the actual position after margin collapsing,
332 // while |contain| is calculated based on the frame's GetUsedMargin() which
333 // is pre-collapsing.
334 // Note that this doesn't necessarily solve all problems stemming from
335 // comparing pre- and post-collapsing margins (TODO: find a proper solution).
336 *aInner
= aInner
->Intersect(*aOuter
);
337 if (aInner
->IsEmpty()) {
338 // This might happen if aInner didn't intersect aOuter at all initially,
339 // in which case aInner is empty and outside aOuter. Make sure it doesn't
340 // extend outside aOuter.
341 *aInner
= aInner
->MoveInsideAndClamp(*aOuter
);
345 void StickyScrollContainer::PositionContinuations(nsIFrame
* aFrame
) {
346 NS_ASSERTION(nsLayoutUtils::IsFirstContinuationOrIBSplitSibling(aFrame
),
347 "Should be starting from the first continuation");
349 nsPoint translation
=
350 ComputePosition(aFrame
) - aFrame
->GetNormalPosition(&hadProperty
);
351 if (NS_WARN_IF(!hadProperty
)) {
352 // If the frame was never relatively positioned, don't move its position
353 // dynamically. There are a variety of frames for which `position` doesn't
354 // really apply like frames inside svg which would get here and be sticky
355 // only in one direction.
359 // Move all continuation frames by the same amount.
360 for (nsIFrame
* cont
= aFrame
; cont
;
361 cont
= nsLayoutUtils::GetNextContinuationOrIBSplitSibling(cont
)) {
362 cont
->SetPosition(cont
->GetNormalPosition() + translation
);
366 void StickyScrollContainer::UpdatePositions(nsPoint aScrollPosition
,
367 nsIFrame
* aSubtreeRoot
) {
370 nsIFrame
* scrollFrameAsFrame
= do_QueryFrame(mScrollFrame
);
371 NS_ASSERTION(!aSubtreeRoot
|| aSubtreeRoot
== scrollFrameAsFrame
,
372 "If reflowing, should be reflowing the scroll frame");
375 mScrollPosition
= aScrollPosition
;
377 OverflowChangedTracker oct
;
378 oct
.SetSubtreeRoot(aSubtreeRoot
);
379 for (nsTArray
<nsIFrame
*>::size_type i
= 0; i
< mFrames
.Length(); i
++) {
380 nsIFrame
* f
= mFrames
[i
];
381 if (!nsLayoutUtils::IsFirstContinuationOrIBSplitSibling(f
)) {
382 // This frame was added in nsIFrame::Init before we knew it wasn't
383 // the first ib-split-sibling.
384 mFrames
.RemoveElementAt(i
);
390 // Reflowing the scroll frame, so recompute offsets.
391 ComputeStickyOffsets(f
);
393 // mFrames will only contain first continuations, because we filter in
395 PositionContinuations(f
);
398 if (f
!= aSubtreeRoot
) {
399 for (nsIFrame
* cont
= f
; cont
;
400 cont
= nsLayoutUtils::GetNextContinuationOrIBSplitSibling(cont
)) {
401 oct
.AddFrame(cont
, OverflowChangedTracker::CHILDREN_CHANGED
);
408 void StickyScrollContainer::ScrollPositionWillChange(nscoord aX
, nscoord aY
) {}
410 void StickyScrollContainer::ScrollPositionDidChange(nscoord aX
, nscoord aY
) {
411 UpdatePositions(nsPoint(aX
, aY
), nullptr);
414 } // namespace mozilla