Backed out changeset 496886cb30a5 (bug 1867152) for bc failures on browser_user_input...
[gecko.git] / layout / generic / StickyScrollContainer.cpp
blob2e9d32ab5fca2de807eb4cdc871aea19550405c0
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) {
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 constexpr nscoord gUnboundedNegative = nscoord_MIN / 2;
146 static constexpr nscoord gUnboundedExtent = nscoord_MAX;
147 static constexpr nscoord gUnboundedPositive =
148 gUnboundedNegative + gUnboundedExtent;
150 void StickyScrollContainer::ComputeStickyLimits(nsIFrame* aFrame,
151 nsRect* aStick,
152 nsRect* aContain) const {
153 NS_ASSERTION(nsLayoutUtils::IsFirstContinuationOrIBSplitSibling(aFrame),
154 "Can't sticky position individual continuations");
156 aStick->SetRect(gUnboundedNegative, gUnboundedNegative, gUnboundedExtent,
157 gUnboundedExtent);
158 aContain->SetRect(gUnboundedNegative, gUnboundedNegative, gUnboundedExtent,
159 gUnboundedExtent);
161 const nsMargin* computedOffsets =
162 aFrame->GetProperty(nsIFrame::ComputedOffsetProperty());
163 if (!computedOffsets) {
164 // We haven't reflowed the scroll frame yet, so offsets haven't been
165 // computed. Bail.
166 return;
169 nsIFrame* scrolledFrame = mScrollFrame->GetScrolledFrame();
170 nsIFrame* cbFrame = aFrame->GetContainingBlock();
171 NS_ASSERTION(cbFrame == scrolledFrame ||
172 nsLayoutUtils::IsProperAncestorFrame(scrolledFrame, cbFrame),
173 "Scroll frame should be an ancestor of the containing block");
175 nsRect rect =
176 nsLayoutUtils::GetAllInFlowRectsUnion(aFrame, aFrame->GetParent());
178 // FIXME(bug 1421660): Table row groups aren't supposed to be containing
179 // blocks, but we treat them as such (maybe it's the right thing to do!).
180 // Anyway, not having this basically disables position: sticky on table cells,
181 // which would be really unfortunate, and doesn't match what other browsers
182 // do.
183 if (cbFrame != scrolledFrame && cbFrame->IsTableRowGroupFrame()) {
184 cbFrame = cbFrame->GetContainingBlock();
187 // Containing block limits for the position of aFrame relative to its parent.
188 // The margin box of the sticky element stays within the content box of the
189 // contaning-block element.
190 if (cbFrame == scrolledFrame) {
191 // cbFrame is the scrolledFrame, and it won't have continuations. Unlike the
192 // else clause, we consider scrollable overflow rect because and the union
193 // of its in-flow rects doesn't include the scrollable overflow area.
194 *aContain = cbFrame->ScrollableOverflowRectRelativeToSelf();
195 nsLayoutUtils::TransformRect(cbFrame, aFrame->GetParent(), *aContain);
196 } else {
197 *aContain = nsLayoutUtils::GetAllInFlowRectsUnion(
198 cbFrame, aFrame->GetParent(), nsLayoutUtils::RECTS_USE_CONTENT_BOX);
201 nsRect marginRect = nsLayoutUtils::GetAllInFlowRectsUnion(
202 aFrame, aFrame->GetParent(), nsLayoutUtils::RECTS_USE_MARGIN_BOX);
204 // Deflate aContain by the difference between the union of aFrame's
205 // continuations' margin boxes and the union of their border boxes, so that
206 // by keeping aFrame within aContain, we keep the union of the margin boxes
207 // within the containing block's content box.
208 aContain->Deflate(marginRect - rect);
210 // Deflate aContain by the border-box size, to form a constraint on the
211 // upper-left corner of aFrame and continuations.
212 aContain->Deflate(nsMargin(0, rect.width, rect.height, 0));
214 nsMargin sfPadding = scrolledFrame->GetUsedPadding();
215 nsPoint sfOffset = aFrame->GetParent()->GetOffsetTo(scrolledFrame);
217 // Top
218 if (computedOffsets->top != NS_AUTOOFFSET) {
219 aStick->SetTopEdge(mScrollPosition.y + sfPadding.top +
220 computedOffsets->top - sfOffset.y);
223 nsSize sfSize = scrolledFrame->GetContentRectRelativeToSelf().Size();
225 // Bottom
226 if (computedOffsets->bottom != NS_AUTOOFFSET &&
227 (computedOffsets->top == NS_AUTOOFFSET ||
228 rect.height <= sfSize.height - computedOffsets->TopBottom())) {
229 aStick->SetBottomEdge(mScrollPosition.y + sfPadding.top + sfSize.height -
230 computedOffsets->bottom - rect.height - sfOffset.y);
233 StyleDirection direction = cbFrame->StyleVisibility()->mDirection;
235 // Left
236 if (computedOffsets->left != NS_AUTOOFFSET &&
237 (computedOffsets->right == NS_AUTOOFFSET ||
238 direction == StyleDirection::Ltr ||
239 rect.width <= sfSize.width - computedOffsets->LeftRight())) {
240 aStick->SetLeftEdge(mScrollPosition.x + sfPadding.left +
241 computedOffsets->left - sfOffset.x);
244 // Right
245 if (computedOffsets->right != NS_AUTOOFFSET &&
246 (computedOffsets->left == NS_AUTOOFFSET ||
247 direction == StyleDirection::Rtl ||
248 rect.width <= sfSize.width - computedOffsets->LeftRight())) {
249 aStick->SetRightEdge(mScrollPosition.x + sfPadding.left + sfSize.width -
250 computedOffsets->right - rect.width - sfOffset.x);
253 // These limits are for the bounding box of aFrame's continuations. Convert
254 // to limits for aFrame itself.
255 nsPoint frameOffset = aFrame->GetPosition() - rect.TopLeft();
256 aStick->MoveBy(frameOffset);
257 aContain->MoveBy(frameOffset);
260 nsPoint StickyScrollContainer::ComputePosition(nsIFrame* aFrame) const {
261 nsRect stick;
262 nsRect contain;
263 ComputeStickyLimits(aFrame, &stick, &contain);
265 nsPoint position = aFrame->GetNormalPosition();
267 // For each sticky direction (top, bottom, left, right), move the frame along
268 // the appropriate axis, based on the scroll position, but limit this to keep
269 // the element's margin box within the containing block.
270 position.y = std::max(position.y, std::min(stick.y, contain.YMost()));
271 position.y = std::min(position.y, std::max(stick.YMost(), contain.y));
272 position.x = std::max(position.x, std::min(stick.x, contain.XMost()));
273 position.x = std::min(position.x, std::max(stick.XMost(), contain.x));
275 return position;
278 bool StickyScrollContainer::IsStuckInYDirection(nsIFrame* aFrame) const {
279 nsPoint position = ComputePosition(aFrame);
280 return position.y != aFrame->GetNormalPosition().y;
283 void StickyScrollContainer::GetScrollRanges(nsIFrame* aFrame,
284 nsRectAbsolute* aOuter,
285 nsRectAbsolute* aInner) const {
286 // We need to use the first in flow; continuation frames should not move
287 // relative to each other and should get identical scroll ranges.
288 // Also, ComputeStickyLimits requires this.
289 nsIFrame* firstCont =
290 nsLayoutUtils::FirstContinuationOrIBSplitSibling(aFrame);
292 nsRect stickRect;
293 nsRect containRect;
294 ComputeStickyLimits(firstCont, &stickRect, &containRect);
296 nsRectAbsolute stick = nsRectAbsolute::FromRect(stickRect);
297 nsRectAbsolute contain = nsRectAbsolute::FromRect(containRect);
299 aOuter->SetBox(gUnboundedNegative, gUnboundedNegative, gUnboundedPositive,
300 gUnboundedPositive);
301 aInner->SetBox(gUnboundedNegative, gUnboundedNegative, gUnboundedPositive,
302 gUnboundedPositive);
304 const nsPoint normalPosition = firstCont->GetNormalPosition();
306 // Bottom and top
307 if (stick.YMost() != gUnboundedPositive) {
308 aOuter->SetTopEdge(contain.Y() - stick.YMost());
309 aInner->SetTopEdge(normalPosition.y - stick.YMost());
312 if (stick.Y() != gUnboundedNegative) {
313 aInner->SetBottomEdge(normalPosition.y - stick.Y());
314 aOuter->SetBottomEdge(contain.YMost() - stick.Y());
317 // Right and left
318 if (stick.XMost() != gUnboundedPositive) {
319 aOuter->SetLeftEdge(contain.X() - stick.XMost());
320 aInner->SetLeftEdge(normalPosition.x - stick.XMost());
323 if (stick.X() != gUnboundedNegative) {
324 aInner->SetRightEdge(normalPosition.x - stick.X());
325 aOuter->SetRightEdge(contain.XMost() - stick.X());
328 // Make sure |inner| does not extend outside of |outer|. (The consumers of
329 // the Layers API, to which this information is propagated, expect this
330 // invariant to hold.) The calculated value of |inner| can sometimes extend
331 // outside of |outer|, for example due to margin collapsing, since
332 // GetNormalPosition() returns the actual position after margin collapsing,
333 // while |contain| is calculated based on the frame's GetUsedMargin() which
334 // is pre-collapsing.
335 // Note that this doesn't necessarily solve all problems stemming from
336 // comparing pre- and post-collapsing margins (TODO: find a proper solution).
337 *aInner = aInner->Intersect(*aOuter);
338 if (aInner->IsEmpty()) {
339 // This might happen if aInner didn't intersect aOuter at all initially,
340 // in which case aInner is empty and outside aOuter. Make sure it doesn't
341 // extend outside aOuter.
342 *aInner = aInner->MoveInsideAndClamp(*aOuter);
346 void StickyScrollContainer::PositionContinuations(nsIFrame* aFrame) {
347 NS_ASSERTION(nsLayoutUtils::IsFirstContinuationOrIBSplitSibling(aFrame),
348 "Should be starting from the first continuation");
349 bool hadProperty;
350 nsPoint translation =
351 ComputePosition(aFrame) - aFrame->GetNormalPosition(&hadProperty);
352 if (NS_WARN_IF(!hadProperty)) {
353 // If the frame was never relatively positioned, don't move its position
354 // dynamically. There are a variety of frames for which `position` doesn't
355 // really apply like frames inside svg which would get here and be sticky
356 // only in one direction.
357 return;
360 // Move all continuation frames by the same amount.
361 for (nsIFrame* cont = aFrame; cont;
362 cont = nsLayoutUtils::GetNextContinuationOrIBSplitSibling(cont)) {
363 cont->SetPosition(cont->GetNormalPosition() + translation);
367 void StickyScrollContainer::UpdatePositions(nsPoint aScrollPosition,
368 nsIFrame* aSubtreeRoot) {
369 #ifdef DEBUG
371 nsIFrame* scrollFrameAsFrame = do_QueryFrame(mScrollFrame);
372 NS_ASSERTION(!aSubtreeRoot || aSubtreeRoot == scrollFrameAsFrame,
373 "If reflowing, should be reflowing the scroll frame");
375 #endif
376 mScrollPosition = aScrollPosition;
378 OverflowChangedTracker oct;
379 oct.SetSubtreeRoot(aSubtreeRoot);
380 for (nsTArray<nsIFrame*>::size_type i = 0; i < mFrames.Length(); i++) {
381 nsIFrame* f = mFrames[i];
382 if (!nsLayoutUtils::IsFirstContinuationOrIBSplitSibling(f)) {
383 // This frame was added in nsIFrame::Init before we knew it wasn't
384 // the first ib-split-sibling.
385 mFrames.RemoveElementAt(i);
386 --i;
387 continue;
390 if (aSubtreeRoot) {
391 // Reflowing the scroll frame, so recompute offsets.
392 ComputeStickyOffsets(f);
394 // mFrames will only contain first continuations, because we filter in
395 // nsIFrame::Init.
396 PositionContinuations(f);
398 f = f->GetParent();
399 if (f != aSubtreeRoot) {
400 for (nsIFrame* cont = f; cont;
401 cont = nsLayoutUtils::GetNextContinuationOrIBSplitSibling(cont)) {
402 oct.AddFrame(cont, OverflowChangedTracker::CHILDREN_CHANGED);
406 oct.Flush();
409 void StickyScrollContainer::ScrollPositionWillChange(nscoord aX, nscoord aY) {}
411 void StickyScrollContainer::ScrollPositionDidChange(nscoord aX, nscoord aY) {
412 UpdatePositions(nsPoint(aX, aY), nullptr);
415 } // namespace mozilla