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