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/. */
6
7 /**
8 * compute sticky positioning, both during reflow and when the scrolling
9 * container scrolls
10 */
11
12 #include "StickyScrollContainer.h"
13 #include "nsIFrame.h"
14 #include "nsIScrollableFrame.h"
15 #include "nsLayoutUtils.h"
16 #include "RestyleTracker.h"
17
18 namespace mozilla {
19
NS_DECLARE_FRAME_PROPERTY_DELETABLE(StickyScrollContainerProperty,StickyScrollContainer)20 NS_DECLARE_FRAME_PROPERTY_DELETABLE(StickyScrollContainerProperty,
21 StickyScrollContainer)
22
23 StickyScrollContainer::StickyScrollContainer(nsIScrollableFrame* aScrollFrame)
24 : mScrollFrame(aScrollFrame), mScrollPosition() {
25 mScrollFrame->AddScrollPositionListener(this);
26 }
27
~StickyScrollContainer()28 StickyScrollContainer::~StickyScrollContainer() {
29 mScrollFrame->RemoveScrollPositionListener(this);
30 }
31
32 // static
GetStickyScrollContainerForFrame(nsIFrame * aFrame)33 StickyScrollContainer* StickyScrollContainer::GetStickyScrollContainerForFrame(
34 nsIFrame* aFrame) {
35 nsIScrollableFrame* scrollFrame = nsLayoutUtils::GetNearestScrollableFrame(
36 aFrame->GetParent(), nsLayoutUtils::SCROLLABLE_SAME_DOC |
37 nsLayoutUtils::SCROLLABLE_INCLUDE_HIDDEN);
38 if (!scrollFrame) {
39 // We might not find any, for instance in the case of
40 // <html style="position: fixed">
41 return nullptr;
42 }
43 nsIFrame* frame = do_QueryFrame(scrollFrame);
44 StickyScrollContainer* s =
45 frame->GetProperty(StickyScrollContainerProperty());
46 if (!s) {
47 s = new StickyScrollContainer(scrollFrame);
48 frame->SetProperty(StickyScrollContainerProperty(), s);
49 }
50 return s;
51 }
52
53 // static
NotifyReparentedFrameAcrossScrollFrameBoundary(nsIFrame * aFrame,nsIFrame * aOldParent)54 void StickyScrollContainer::NotifyReparentedFrameAcrossScrollFrameBoundary(
55 nsIFrame* aFrame, nsIFrame* aOldParent) {
56 nsIScrollableFrame* oldScrollFrame = nsLayoutUtils::GetNearestScrollableFrame(
57 aOldParent, nsLayoutUtils::SCROLLABLE_SAME_DOC |
58 nsLayoutUtils::SCROLLABLE_INCLUDE_HIDDEN);
59 if (!oldScrollFrame) {
60 // XXX maybe aFrame has sticky descendants that can be sticky now, but
61 // we aren't going to handle that.
62 return;
63 }
64
65 StickyScrollContainer* oldSSC =
66 static_cast<nsIFrame*>(do_QueryFrame(oldScrollFrame))
67 ->GetProperty(StickyScrollContainerProperty());
68 if (!oldSSC) {
69 // aOldParent had no sticky descendants, so aFrame doesn't have any sticky
70 // descendants, and we're done here.
71 return;
72 }
73
74 auto i = oldSSC->mFrames.Length();
75 while (i-- > 0) {
76 nsIFrame* f = oldSSC->mFrames[i];
77 StickyScrollContainer* newSSC = GetStickyScrollContainerForFrame(f);
78 if (newSSC != oldSSC) {
79 oldSSC->RemoveFrame(f);
80 if (newSSC) {
81 newSSC->AddFrame(f);
82 }
83 }
84 }
85 }
86
87 // static
88 StickyScrollContainer*
GetStickyScrollContainerForScrollFrame(nsIFrame * aFrame)89 StickyScrollContainer::GetStickyScrollContainerForScrollFrame(
90 nsIFrame* aFrame) {
91 return aFrame->GetProperty(StickyScrollContainerProperty());
92 }
93
ComputeStickySideOffset(Side aSide,const nsStyleSides & aOffset,nscoord aPercentBasis)94 static nscoord ComputeStickySideOffset(Side aSide, const nsStyleSides& aOffset,
95 nscoord aPercentBasis) {
96 if (eStyleUnit_Auto == aOffset.GetUnit(aSide)) {
97 return NS_AUTOOFFSET;
98 } else {
99 return nsLayoutUtils::ComputeCBDependentValue(aPercentBasis,
100 aOffset.Get(aSide));
101 }
102 }
103
104 // static
ComputeStickyOffsets(nsIFrame * aFrame)105 void StickyScrollContainer::ComputeStickyOffsets(nsIFrame* aFrame) {
106 nsIScrollableFrame* scrollableFrame =
107 nsLayoutUtils::GetNearestScrollableFrame(
108 aFrame->GetParent(), nsLayoutUtils::SCROLLABLE_SAME_DOC |
109 nsLayoutUtils::SCROLLABLE_INCLUDE_HIDDEN);
110
111 if (!scrollableFrame) {
112 // Bail.
113 return;
114 }
115
116 nsSize scrollContainerSize = scrollableFrame->GetScrolledFrame()
117 ->GetContentRectRelativeToSelf()
118 .Size();
119
120 nsMargin computedOffsets;
121 const nsStylePosition* position = aFrame->StylePosition();
122
123 computedOffsets.left = ComputeStickySideOffset(eSideLeft, position->mOffset,
124 scrollContainerSize.width);
125 computedOffsets.right = ComputeStickySideOffset(eSideRight, position->mOffset,
126 scrollContainerSize.width);
127 computedOffsets.top = ComputeStickySideOffset(eSideTop, position->mOffset,
128 scrollContainerSize.height);
129 computedOffsets.bottom = ComputeStickySideOffset(
130 eSideBottom, position->mOffset, scrollContainerSize.height);
131
132 // Store the offset
133 nsMargin* offsets = aFrame->GetProperty(nsIFrame::ComputedOffsetProperty());
134 if (offsets) {
135 *offsets = computedOffsets;
136 } else {
137 aFrame->SetProperty(nsIFrame::ComputedOffsetProperty(),
138 new nsMargin(computedOffsets));
139 }
140 }
141
142 static nscoord gUnboundedNegative = nscoord_MIN / 2;
143 static nscoord gUnboundedExtent = nscoord_MAX;
144 static nscoord gUnboundedPositive = gUnboundedNegative + gUnboundedExtent;
145
ComputeStickyLimits(nsIFrame * aFrame,nsRect * aStick,nsRect * aContain) const146 void StickyScrollContainer::ComputeStickyLimits(nsIFrame* aFrame,
147 nsRect* aStick,
148 nsRect* aContain) const {
149 NS_ASSERTION(nsLayoutUtils::IsFirstContinuationOrIBSplitSibling(aFrame),
150 "Can't sticky position individual continuations");
151
152 aStick->SetRect(gUnboundedNegative, gUnboundedNegative, gUnboundedExtent,
153 gUnboundedExtent);
154 aContain->SetRect(gUnboundedNegative, gUnboundedNegative, gUnboundedExtent,
155 gUnboundedExtent);
156
157 const nsMargin* computedOffsets =
158 aFrame->GetProperty(nsIFrame::ComputedOffsetProperty());
159 if (!computedOffsets) {
160 // We haven't reflowed the scroll frame yet, so offsets haven't been
161 // computed. Bail.
162 return;
163 }
164
165 nsIFrame* scrolledFrame = mScrollFrame->GetScrolledFrame();
166 nsIFrame* cbFrame = aFrame->GetContainingBlock();
167 NS_ASSERTION(cbFrame == scrolledFrame ||
168 nsLayoutUtils::IsProperAncestorFrame(scrolledFrame, cbFrame),
169 "Scroll frame should be an ancestor of the containing block");
170
171 nsRect rect =
172 nsLayoutUtils::GetAllInFlowRectsUnion(aFrame, aFrame->GetParent());
173
174 // FIXME(bug 1421660): Table row groups aren't supposed to be containing
175 // blocks, but we treat them as such (maybe it's the right thing to do!).
176 // Anyway, not having this basically disables position: sticky on table cells,
177 // which would be really unfortunate, and doesn't match what other browsers
178 // do.
179 if (cbFrame != scrolledFrame && cbFrame->IsTableRowGroupFrame()) {
180 cbFrame = cbFrame->GetContainingBlock();
181 }
182
183 // Containing block limits for the position of aFrame relative to its parent.
184 // The margin box of the sticky element stays within the content box of the
185 // contaning-block element.
186 if (cbFrame != scrolledFrame) {
187 *aContain = nsLayoutUtils::GetAllInFlowRectsUnion(
188 cbFrame, aFrame->GetParent(), nsLayoutUtils::RECTS_USE_CONTENT_BOX);
189 nsRect marginRect = nsLayoutUtils::GetAllInFlowRectsUnion(
190 aFrame, aFrame->GetParent(), nsLayoutUtils::RECTS_USE_MARGIN_BOX);
191
192 // Deflate aContain by the difference between the union of aFrame's
193 // continuations' margin boxes and the union of their border boxes, so that
194 // by keeping aFrame within aContain, we keep the union of the margin boxes
195 // within the containing block's content box.
196 aContain->Deflate(marginRect - rect);
197
198 // Deflate aContain by the border-box size, to form a constraint on the
199 // upper-left corner of aFrame and continuations.
200 aContain->Deflate(nsMargin(0, rect.width, rect.height, 0));
201 }
202
203 nsMargin sfPadding = scrolledFrame->GetUsedPadding();
204 nsPoint sfOffset = aFrame->GetParent()->GetOffsetTo(scrolledFrame);
205
206 // Top
207 if (computedOffsets->top != NS_AUTOOFFSET) {
208 aStick->SetTopEdge(mScrollPosition.y + sfPadding.top +
209 computedOffsets->top - sfOffset.y);
210 }
211
212 nsSize sfSize = scrolledFrame->GetContentRectRelativeToSelf().Size();
213
214 // Bottom
215 if (computedOffsets->bottom != NS_AUTOOFFSET &&
216 (computedOffsets->top == NS_AUTOOFFSET ||
217 rect.height <= sfSize.height - computedOffsets->TopBottom())) {
218 aStick->SetBottomEdge(mScrollPosition.y + sfPadding.top + sfSize.height -
219 computedOffsets->bottom - rect.height - sfOffset.y);
220 }
221
222 uint8_t direction = cbFrame->StyleVisibility()->mDirection;
223
224 // Left
225 if (computedOffsets->left != NS_AUTOOFFSET &&
226 (computedOffsets->right == NS_AUTOOFFSET ||
227 direction == NS_STYLE_DIRECTION_LTR ||
228 rect.width <= sfSize.width - computedOffsets->LeftRight())) {
229 aStick->SetLeftEdge(mScrollPosition.x + sfPadding.left +
230 computedOffsets->left - sfOffset.x);
231 }
232
233 // Right
234 if (computedOffsets->right != NS_AUTOOFFSET &&
235 (computedOffsets->left == NS_AUTOOFFSET ||
236 direction == NS_STYLE_DIRECTION_RTL ||
237 rect.width <= sfSize.width - computedOffsets->LeftRight())) {
238 aStick->SetRightEdge(mScrollPosition.x + sfPadding.left + sfSize.width -
239 computedOffsets->right - rect.width - sfOffset.x);
240 }
241
242 // These limits are for the bounding box of aFrame's continuations. Convert
243 // to limits for aFrame itself.
244 nsPoint frameOffset = aFrame->GetPosition() - rect.TopLeft();
245 aStick->MoveBy(frameOffset);
246 aContain->MoveBy(frameOffset);
247 }
248
ComputePosition(nsIFrame * aFrame) const249 nsPoint StickyScrollContainer::ComputePosition(nsIFrame* aFrame) const {
250 nsRect stick;
251 nsRect contain;
252 ComputeStickyLimits(aFrame, &stick, &contain);
253
254 nsPoint position = aFrame->GetNormalPosition();
255
256 // For each sticky direction (top, bottom, left, right), move the frame along
257 // the appropriate axis, based on the scroll position, but limit this to keep
258 // the element's margin box within the containing block.
259 position.y = std::max(position.y, std::min(stick.y, contain.YMost()));
260 position.y = std::min(position.y, std::max(stick.YMost(), contain.y));
261 position.x = std::max(position.x, std::min(stick.x, contain.XMost()));
262 position.x = std::min(position.x, std::max(stick.XMost(), contain.x));
263
264 return position;
265 }
266
GetScrollRanges(nsIFrame * aFrame,nsRectAbsolute * aOuter,nsRectAbsolute * aInner) const267 void StickyScrollContainer::GetScrollRanges(nsIFrame* aFrame,
268 nsRectAbsolute* aOuter,
269 nsRectAbsolute* aInner) const {
270 // We need to use the first in flow; continuation frames should not move
271 // relative to each other and should get identical scroll ranges.
272 // Also, ComputeStickyLimits requires this.
273 nsIFrame* firstCont =
274 nsLayoutUtils::FirstContinuationOrIBSplitSibling(aFrame);
275
276 nsRect stickRect;
277 nsRect containRect;
278 ComputeStickyLimits(firstCont, &stickRect, &containRect);
279
280 nsRectAbsolute stick = nsRectAbsolute::FromRect(stickRect);
281 nsRectAbsolute contain = nsRectAbsolute::FromRect(containRect);
282
283 aOuter->SetBox(gUnboundedNegative, gUnboundedNegative, gUnboundedPositive,
284 gUnboundedPositive);
285 aInner->SetBox(gUnboundedNegative, gUnboundedNegative, gUnboundedPositive,
286 gUnboundedPositive);
287
288 const nsPoint normalPosition = firstCont->GetNormalPosition();
289
290 // Bottom and top
291 if (stick.YMost() != gUnboundedPositive) {
292 aOuter->SetTopEdge(contain.Y() - stick.YMost());
293 aInner->SetTopEdge(normalPosition.y - stick.YMost());
294 }
295
296 if (stick.Y() != gUnboundedNegative) {
297 aInner->SetBottomEdge(normalPosition.y - stick.Y());
298 aOuter->SetBottomEdge(contain.YMost() - stick.Y());
299 }
300
301 // Right and left
302 if (stick.XMost() != gUnboundedPositive) {
303 aOuter->SetLeftEdge(contain.X() - stick.XMost());
304 aInner->SetLeftEdge(normalPosition.x - stick.XMost());
305 }
306
307 if (stick.X() != gUnboundedNegative) {
308 aInner->SetRightEdge(normalPosition.x - stick.X());
309 aOuter->SetRightEdge(contain.XMost() - stick.X());
310 }
311
312 // Make sure |inner| does not extend outside of |outer|. (The consumers of
313 // the Layers API, to which this information is propagated, expect this
314 // invariant to hold.) The calculated value of |inner| can sometimes extend
315 // outside of |outer|, for example due to margin collapsing, since
316 // GetNormalPosition() returns the actual position after margin collapsing,
317 // while |contain| is calculated based on the frame's GetUsedMargin() which
318 // is pre-collapsing.
319 // Note that this doesn't necessarily solve all problems stemming from
320 // comparing pre- and post-collapsing margins (TODO: find a proper solution).
321 *aInner = aInner->Intersect(*aOuter);
322 }
323
PositionContinuations(nsIFrame * aFrame)324 void StickyScrollContainer::PositionContinuations(nsIFrame* aFrame) {
325 NS_ASSERTION(nsLayoutUtils::IsFirstContinuationOrIBSplitSibling(aFrame),
326 "Should be starting from the first continuation");
327 nsPoint translation = ComputePosition(aFrame) - aFrame->GetNormalPosition();
328
329 // Move all continuation frames by the same amount.
330 for (nsIFrame* cont = aFrame; cont;
331 cont = nsLayoutUtils::GetNextContinuationOrIBSplitSibling(cont)) {
332 cont->SetPosition(cont->GetNormalPosition() + translation);
333 }
334 }
335
UpdatePositions(nsPoint aScrollPosition,nsIFrame * aSubtreeRoot)336 void StickyScrollContainer::UpdatePositions(nsPoint aScrollPosition,
337 nsIFrame* aSubtreeRoot) {
338 #ifdef DEBUG
339 {
340 nsIFrame* scrollFrameAsFrame = do_QueryFrame(mScrollFrame);
341 NS_ASSERTION(!aSubtreeRoot || aSubtreeRoot == scrollFrameAsFrame,
342 "If reflowing, should be reflowing the scroll frame");
343 }
344 #endif
345 mScrollPosition = aScrollPosition;
346
347 OverflowChangedTracker oct;
348 oct.SetSubtreeRoot(aSubtreeRoot);
349 for (nsTArray<nsIFrame*>::size_type i = 0; i < mFrames.Length(); i++) {
350 nsIFrame* f = mFrames[i];
351 if (!nsLayoutUtils::IsFirstContinuationOrIBSplitSibling(f)) {
352 // This frame was added in nsFrame::Init before we knew it wasn't
353 // the first ib-split-sibling.
354 mFrames.RemoveElementAt(i);
355 --i;
356 continue;
357 }
358
359 if (aSubtreeRoot) {
360 // Reflowing the scroll frame, so recompute offsets.
361 ComputeStickyOffsets(f);
362 }
363 // mFrames will only contain first continuations, because we filter in
364 // nsIFrame::Init.
365 PositionContinuations(f);
366
367 f = f->GetParent();
368 if (f != aSubtreeRoot) {
369 for (nsIFrame* cont = f; cont;
370 cont = nsLayoutUtils::GetNextContinuationOrIBSplitSibling(cont)) {
371 oct.AddFrame(cont, OverflowChangedTracker::CHILDREN_CHANGED);
372 }
373 }
374 }
375 oct.Flush();
376 }
377
ScrollPositionWillChange(nscoord aX,nscoord aY)378 void StickyScrollContainer::ScrollPositionWillChange(nscoord aX, nscoord aY) {}
379
ScrollPositionDidChange(nscoord aX,nscoord aY)380 void StickyScrollContainer::ScrollPositionDidChange(nscoord aX, nscoord aY) {
381 UpdatePositions(nsPoint(aX, aY), nullptr);
382 }
383
384 } // namespace mozilla
385