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 
14 #include "mozilla/OverflowChangedTracker.h"
15 #include "nsIFrame.h"
16 #include "nsIFrameInlines.h"
17 #include "nsIScrollableFrame.h"
18 #include "nsLayoutUtils.h"
19 
20 namespace mozilla {
21 
NS_DECLARE_FRAME_PROPERTY_DELETABLE(StickyScrollContainerProperty,StickyScrollContainer)22 NS_DECLARE_FRAME_PROPERTY_DELETABLE(StickyScrollContainerProperty,
23                                     StickyScrollContainer)
24 
25 StickyScrollContainer::StickyScrollContainer(nsIScrollableFrame* aScrollFrame)
26     : mScrollFrame(aScrollFrame), mScrollPosition() {
27   mScrollFrame->AddScrollPositionListener(this);
28 }
29 
~StickyScrollContainer()30 StickyScrollContainer::~StickyScrollContainer() {
31   mScrollFrame->RemoveScrollPositionListener(this);
32 }
33 
34 // static
GetStickyScrollContainerForFrame(nsIFrame * aFrame)35 StickyScrollContainer* StickyScrollContainer::GetStickyScrollContainerForFrame(
36     nsIFrame* aFrame) {
37   nsIScrollableFrame* scrollFrame = nsLayoutUtils::GetNearestScrollableFrame(
38       aFrame->GetParent(), nsLayoutUtils::SCROLLABLE_SAME_DOC |
39                                nsLayoutUtils::SCROLLABLE_STOP_AT_PAGE |
40                                nsLayoutUtils::SCROLLABLE_INCLUDE_HIDDEN);
41   if (!scrollFrame) {
42     // We might not find any, for instance in the case of
43     // <html style="position: fixed">
44     return nullptr;
45   }
46   nsIFrame* frame = do_QueryFrame(scrollFrame);
47   StickyScrollContainer* s =
48       frame->GetProperty(StickyScrollContainerProperty());
49   if (!s) {
50     s = new StickyScrollContainer(scrollFrame);
51     frame->SetProperty(StickyScrollContainerProperty(), s);
52   }
53   return s;
54 }
55 
56 // static
NotifyReparentedFrameAcrossScrollFrameBoundary(nsIFrame * aFrame,nsIFrame * aOldParent)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.
65     return;
66   }
67 
68   StickyScrollContainer* oldSSC =
69       static_cast<nsIFrame*>(do_QueryFrame(oldScrollFrame))
70           ->GetProperty(StickyScrollContainerProperty());
71   if (!oldSSC) {
72     // aOldParent had no sticky descendants, so aFrame doesn't have any sticky
73     // descendants, and we're done here.
74     return;
75   }
76 
77   auto i = oldSSC->mFrames.Length();
78   while (i-- > 0) {
79     nsIFrame* f = oldSSC->mFrames[i];
80     StickyScrollContainer* newSSC = GetStickyScrollContainerForFrame(f);
81     if (newSSC != oldSSC) {
82       oldSSC->RemoveFrame(f);
83       if (newSSC) {
84         newSSC->AddFrame(f);
85       }
86     }
87   }
88 }
89 
90 // static
91 StickyScrollContainer*
GetStickyScrollContainerForScrollFrame(nsIFrame * aFrame)92 StickyScrollContainer::GetStickyScrollContainerForScrollFrame(
93     nsIFrame* aFrame) {
94   return aFrame->GetProperty(StickyScrollContainerProperty());
95 }
96 
ComputeStickySideOffset(Side aSide,const StyleRect<LengthPercentageOrAuto> & aOffset,nscoord aPercentBasis)97 static nscoord ComputeStickySideOffset(
98     Side aSide, const StyleRect<LengthPercentageOrAuto>& aOffset,
99     nscoord aPercentBasis) {
100   auto& side = aOffset.Get(aSide);
101   if (side.IsAuto()) {
102     return NS_AUTOOFFSET;
103   }
104   return nsLayoutUtils::ComputeCBDependentValue(aPercentBasis, side);
105 }
106 
107 // static
ComputeStickyOffsets(nsIFrame * aFrame)108 void StickyScrollContainer::ComputeStickyOffsets(nsIFrame* aFrame) {
109   nsIScrollableFrame* scrollableFrame =
110       nsLayoutUtils::GetNearestScrollableFrame(
111           aFrame->GetParent(), nsLayoutUtils::SCROLLABLE_SAME_DOC |
112                                    nsLayoutUtils::SCROLLABLE_INCLUDE_HIDDEN);
113 
114   if (!scrollableFrame) {
115     // Bail.
116     return;
117   }
118 
119   nsSize scrollContainerSize = scrollableFrame->GetScrolledFrame()
120                                    ->GetContentRectRelativeToSelf()
121                                    .Size();
122 
123   nsMargin computedOffsets;
124   const nsStylePosition* position = aFrame->StylePosition();
125 
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);
134 
135   // Store the offset
136   nsMargin* offsets = aFrame->GetProperty(nsIFrame::ComputedOffsetProperty());
137   if (offsets) {
138     *offsets = computedOffsets;
139   } else {
140     aFrame->SetProperty(nsIFrame::ComputedOffsetProperty(),
141                         new nsMargin(computedOffsets));
142   }
143 }
144 
145 static nscoord gUnboundedNegative = nscoord_MIN / 2;
146 static nscoord gUnboundedExtent = nscoord_MAX;
147 static nscoord gUnboundedPositive = gUnboundedNegative + gUnboundedExtent;
148 
ComputeStickyLimits(nsIFrame * aFrame,nsRect * aStick,nsRect * aContain) const149 void StickyScrollContainer::ComputeStickyLimits(nsIFrame* aFrame,
150                                                 nsRect* aStick,
151                                                 nsRect* aContain) const {
152   NS_ASSERTION(nsLayoutUtils::IsFirstContinuationOrIBSplitSibling(aFrame),
153                "Can't sticky position individual continuations");
154 
155   aStick->SetRect(gUnboundedNegative, gUnboundedNegative, gUnboundedExtent,
156                   gUnboundedExtent);
157   aContain->SetRect(gUnboundedNegative, gUnboundedNegative, gUnboundedExtent,
158                     gUnboundedExtent);
159 
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
164     // computed. Bail.
165     return;
166   }
167 
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");
173 
174   nsRect rect =
175       nsLayoutUtils::GetAllInFlowRectsUnion(aFrame, aFrame->GetParent());
176 
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
181   // do.
182   if (cbFrame != scrolledFrame && cbFrame->IsTableRowGroupFrame()) {
183     cbFrame = cbFrame->GetContainingBlock();
184   }
185 
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);
195   } else {
196     *aContain = nsLayoutUtils::GetAllInFlowRectsUnion(
197         cbFrame, aFrame->GetParent(), nsLayoutUtils::RECTS_USE_CONTENT_BOX);
198   }
199 
200   nsRect marginRect = nsLayoutUtils::GetAllInFlowRectsUnion(
201       aFrame, aFrame->GetParent(), nsLayoutUtils::RECTS_USE_MARGIN_BOX);
202 
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);
208 
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));
212 
213   nsMargin sfPadding = scrolledFrame->GetUsedPadding();
214   nsPoint sfOffset = aFrame->GetParent()->GetOffsetTo(scrolledFrame);
215 
216   // Top
217   if (computedOffsets->top != NS_AUTOOFFSET) {
218     aStick->SetTopEdge(mScrollPosition.y + sfPadding.top +
219                        computedOffsets->top - sfOffset.y);
220   }
221 
222   nsSize sfSize = scrolledFrame->GetContentRectRelativeToSelf().Size();
223 
224   // Bottom
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);
230   }
231 
232   StyleDirection direction = cbFrame->StyleVisibility()->mDirection;
233 
234   // Left
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);
241   }
242 
243   // Right
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);
250   }
251 
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);
257 }
258 
ComputePosition(nsIFrame * aFrame) const259 nsPoint StickyScrollContainer::ComputePosition(nsIFrame* aFrame) const {
260   nsRect stick;
261   nsRect contain;
262   ComputeStickyLimits(aFrame, &stick, &contain);
263 
264   nsPoint position = aFrame->GetNormalPosition();
265 
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));
273 
274   return position;
275 }
276 
IsStuckInYDirection(nsIFrame * aFrame) const277 bool StickyScrollContainer::IsStuckInYDirection(nsIFrame* aFrame) const {
278   nsPoint position = ComputePosition(aFrame);
279   return position.y != aFrame->GetNormalPosition().y;
280 }
281 
GetScrollRanges(nsIFrame * aFrame,nsRectAbsolute * aOuter,nsRectAbsolute * aInner) const282 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);
290 
291   nsRect stickRect;
292   nsRect containRect;
293   ComputeStickyLimits(firstCont, &stickRect, &containRect);
294 
295   nsRectAbsolute stick = nsRectAbsolute::FromRect(stickRect);
296   nsRectAbsolute contain = nsRectAbsolute::FromRect(containRect);
297 
298   aOuter->SetBox(gUnboundedNegative, gUnboundedNegative, gUnboundedPositive,
299                  gUnboundedPositive);
300   aInner->SetBox(gUnboundedNegative, gUnboundedNegative, gUnboundedPositive,
301                  gUnboundedPositive);
302 
303   const nsPoint normalPosition = firstCont->GetNormalPosition();
304 
305   // Bottom and top
306   if (stick.YMost() != gUnboundedPositive) {
307     aOuter->SetTopEdge(contain.Y() - stick.YMost());
308     aInner->SetTopEdge(normalPosition.y - stick.YMost());
309   }
310 
311   if (stick.Y() != gUnboundedNegative) {
312     aInner->SetBottomEdge(normalPosition.y - stick.Y());
313     aOuter->SetBottomEdge(contain.YMost() - stick.Y());
314   }
315 
316   // Right and left
317   if (stick.XMost() != gUnboundedPositive) {
318     aOuter->SetLeftEdge(contain.X() - stick.XMost());
319     aInner->SetLeftEdge(normalPosition.x - stick.XMost());
320   }
321 
322   if (stick.X() != gUnboundedNegative) {
323     aInner->SetRightEdge(normalPosition.x - stick.X());
324     aOuter->SetRightEdge(contain.XMost() - stick.X());
325   }
326 
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);
342   }
343 }
344 
PositionContinuations(nsIFrame * aFrame)345 void StickyScrollContainer::PositionContinuations(nsIFrame* aFrame) {
346   NS_ASSERTION(nsLayoutUtils::IsFirstContinuationOrIBSplitSibling(aFrame),
347                "Should be starting from the first continuation");
348   nsPoint translation = ComputePosition(aFrame) - aFrame->GetNormalPosition();
349 
350   // Move all continuation frames by the same amount.
351   for (nsIFrame* cont = aFrame; cont;
352        cont = nsLayoutUtils::GetNextContinuationOrIBSplitSibling(cont)) {
353     cont->SetPosition(cont->GetNormalPosition() + translation);
354   }
355 }
356 
UpdatePositions(nsPoint aScrollPosition,nsIFrame * aSubtreeRoot)357 void StickyScrollContainer::UpdatePositions(nsPoint aScrollPosition,
358                                             nsIFrame* aSubtreeRoot) {
359 #ifdef DEBUG
360   {
361     nsIFrame* scrollFrameAsFrame = do_QueryFrame(mScrollFrame);
362     NS_ASSERTION(!aSubtreeRoot || aSubtreeRoot == scrollFrameAsFrame,
363                  "If reflowing, should be reflowing the scroll frame");
364   }
365 #endif
366   mScrollPosition = aScrollPosition;
367 
368   OverflowChangedTracker oct;
369   oct.SetSubtreeRoot(aSubtreeRoot);
370   for (nsTArray<nsIFrame*>::size_type i = 0; i < mFrames.Length(); i++) {
371     nsIFrame* f = mFrames[i];
372     if (!nsLayoutUtils::IsFirstContinuationOrIBSplitSibling(f)) {
373       // This frame was added in nsIFrame::Init before we knew it wasn't
374       // the first ib-split-sibling.
375       mFrames.RemoveElementAt(i);
376       --i;
377       continue;
378     }
379 
380     if (aSubtreeRoot) {
381       // Reflowing the scroll frame, so recompute offsets.
382       ComputeStickyOffsets(f);
383     }
384     // mFrames will only contain first continuations, because we filter in
385     // nsIFrame::Init.
386     PositionContinuations(f);
387 
388     f = f->GetParent();
389     if (f != aSubtreeRoot) {
390       for (nsIFrame* cont = f; cont;
391            cont = nsLayoutUtils::GetNextContinuationOrIBSplitSibling(cont)) {
392         oct.AddFrame(cont, OverflowChangedTracker::CHILDREN_CHANGED);
393       }
394     }
395   }
396   oct.Flush();
397 }
398 
ScrollPositionWillChange(nscoord aX,nscoord aY)399 void StickyScrollContainer::ScrollPositionWillChange(nscoord aX, nscoord aY) {}
400 
ScrollPositionDidChange(nscoord aX,nscoord aY)401 void StickyScrollContainer::ScrollPositionDidChange(nscoord aX, nscoord aY) {
402   UpdatePositions(nsPoint(aX, aY), nullptr);
403 }
404 
405 }  // namespace mozilla
406