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