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