Bug 1763869 [wpt PR 33577] - Fix adb command to find webview package, a=testonly
[gecko.git] / layout / generic / StickyScrollContainer.cpp
blob5616ad3fbe4b9debf1fbe173f210a8744e37c0f0
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/. */
7 /**
8 * compute sticky positioning, both during reflow and when the scrolling
9 * container scrolls
12 #include "StickyScrollContainer.h"
14 #include "mozilla/OverflowChangedTracker.h"
15 #include "nsIFrame.h"
16 #include "nsIFrameInlines.h"
17 #include "nsIScrollableFrame.h"
18 #include "nsLayoutUtils.h"
20 namespace mozilla {
22 NS_DECLARE_FRAME_PROPERTY_DELETABLE(StickyScrollContainerProperty,
23 StickyScrollContainer)
25 StickyScrollContainer::StickyScrollContainer(nsIScrollableFrame* aScrollFrame)
26 : mScrollFrame(aScrollFrame), mScrollPosition() {
27 mScrollFrame->AddScrollPositionListener(this);
30 StickyScrollContainer::~StickyScrollContainer() {
31 mScrollFrame->RemoveScrollPositionListener(this);
34 // static
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;
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);
53 return s;
56 // static
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;
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;
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);
90 // static
91 StickyScrollContainer*
92 StickyScrollContainer::GetStickyScrollContainerForScrollFrame(
93 nsIFrame* aFrame) {
94 return aFrame->GetProperty(StickyScrollContainerProperty());
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;
104 return nsLayoutUtils::ComputeCBDependentValue(aPercentBasis, side);
107 // static
108 void StickyScrollContainer::ComputeStickyOffsets(nsIFrame* aFrame) {
109 nsIScrollableFrame* scrollableFrame =
110 nsLayoutUtils::GetNearestScrollableFrame(
111 aFrame->GetParent(), nsLayoutUtils::SCROLLABLE_SAME_DOC |
112 nsLayoutUtils::SCROLLABLE_INCLUDE_HIDDEN);
114 if (!scrollableFrame) {
115 // Bail.
116 return;
119 nsSize scrollContainerSize = scrollableFrame->GetScrolledFrame()
120 ->GetContentRectRelativeToSelf()
121 .Size();
123 nsMargin computedOffsets;
124 const nsStylePosition* position = aFrame->StylePosition();
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);
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));
145 static nscoord gUnboundedNegative = nscoord_MIN / 2;
146 static nscoord gUnboundedExtent = nscoord_MAX;
147 static nscoord gUnboundedPositive = gUnboundedNegative + gUnboundedExtent;
149 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");
155 aStick->SetRect(gUnboundedNegative, gUnboundedNegative, gUnboundedExtent,
156 gUnboundedExtent);
157 aContain->SetRect(gUnboundedNegative, gUnboundedNegative, gUnboundedExtent,
158 gUnboundedExtent);
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;
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");
174 nsRect rect =
175 nsLayoutUtils::GetAllInFlowRectsUnion(aFrame, aFrame->GetParent());
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();
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);
200 nsRect marginRect = nsLayoutUtils::GetAllInFlowRectsUnion(
201 aFrame, aFrame->GetParent(), nsLayoutUtils::RECTS_USE_MARGIN_BOX);
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);
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));
213 nsMargin sfPadding = scrolledFrame->GetUsedPadding();
214 nsPoint sfOffset = aFrame->GetParent()->GetOffsetTo(scrolledFrame);
216 // Top
217 if (computedOffsets->top != NS_AUTOOFFSET) {
218 aStick->SetTopEdge(mScrollPosition.y + sfPadding.top +
219 computedOffsets->top - sfOffset.y);
222 nsSize sfSize = scrolledFrame->GetContentRectRelativeToSelf().Size();
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);
232 StyleDirection direction = cbFrame->StyleVisibility()->mDirection;
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);
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);
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);
259 nsPoint StickyScrollContainer::ComputePosition(nsIFrame* aFrame) const {
260 nsRect stick;
261 nsRect contain;
262 ComputeStickyLimits(aFrame, &stick, &contain);
264 nsPoint position = aFrame->GetNormalPosition();
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));
274 return position;
277 bool StickyScrollContainer::IsStuckInYDirection(nsIFrame* aFrame) const {
278 nsPoint position = ComputePosition(aFrame);
279 return position.y != aFrame->GetNormalPosition().y;
282 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);
291 nsRect stickRect;
292 nsRect containRect;
293 ComputeStickyLimits(firstCont, &stickRect, &containRect);
295 nsRectAbsolute stick = nsRectAbsolute::FromRect(stickRect);
296 nsRectAbsolute contain = nsRectAbsolute::FromRect(containRect);
298 aOuter->SetBox(gUnboundedNegative, gUnboundedNegative, gUnboundedPositive,
299 gUnboundedPositive);
300 aInner->SetBox(gUnboundedNegative, gUnboundedNegative, gUnboundedPositive,
301 gUnboundedPositive);
303 const nsPoint normalPosition = firstCont->GetNormalPosition();
305 // Bottom and top
306 if (stick.YMost() != gUnboundedPositive) {
307 aOuter->SetTopEdge(contain.Y() - stick.YMost());
308 aInner->SetTopEdge(normalPosition.y - stick.YMost());
311 if (stick.Y() != gUnboundedNegative) {
312 aInner->SetBottomEdge(normalPosition.y - stick.Y());
313 aOuter->SetBottomEdge(contain.YMost() - stick.Y());
316 // Right and left
317 if (stick.XMost() != gUnboundedPositive) {
318 aOuter->SetLeftEdge(contain.X() - stick.XMost());
319 aInner->SetLeftEdge(normalPosition.x - stick.XMost());
322 if (stick.X() != gUnboundedNegative) {
323 aInner->SetRightEdge(normalPosition.x - stick.X());
324 aOuter->SetRightEdge(contain.XMost() - stick.X());
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);
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();
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);
357 void StickyScrollContainer::UpdatePositions(nsPoint aScrollPosition,
358 nsIFrame* aSubtreeRoot) {
359 #ifdef DEBUG
361 nsIFrame* scrollFrameAsFrame = do_QueryFrame(mScrollFrame);
362 NS_ASSERTION(!aSubtreeRoot || aSubtreeRoot == scrollFrameAsFrame,
363 "If reflowing, should be reflowing the scroll frame");
365 #endif
366 mScrollPosition = aScrollPosition;
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;
380 if (aSubtreeRoot) {
381 // Reflowing the scroll frame, so recompute offsets.
382 ComputeStickyOffsets(f);
384 // mFrames will only contain first continuations, because we filter in
385 // nsIFrame::Init.
386 PositionContinuations(f);
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);
396 oct.Flush();
399 void StickyScrollContainer::ScrollPositionWillChange(nscoord aX, nscoord aY) {}
401 void StickyScrollContainer::ScrollPositionDidChange(nscoord aX, nscoord aY) {
402 UpdatePositions(nsPoint(aX, aY), nullptr);
405 } // namespace mozilla