StickyScrollContainer.cpp (15919B)
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 "PresShell.h" 15 #include "mozilla/OverflowChangedTracker.h" 16 #include "mozilla/ScrollContainerFrame.h" 17 #include "nsIFrame.h" 18 #include "nsIFrameInlines.h" 19 #include "nsLayoutUtils.h" 20 21 namespace mozilla { 22 23 StickyScrollContainer::StickyScrollContainer( 24 ScrollContainerFrame* aScrollContainerFrame) 25 : mScrollContainerFrame(aScrollContainerFrame) {} 26 27 StickyScrollContainer::~StickyScrollContainer() = default; 28 29 // static 30 StickyScrollContainer* StickyScrollContainer::GetOrCreateForFrame( 31 nsIFrame* aFrame) { 32 ScrollContainerFrame* scrollContainerFrame = 33 nsLayoutUtils::GetNearestScrollContainerFrame( 34 aFrame->GetParent(), nsLayoutUtils::SCROLLABLE_SAME_DOC | 35 nsLayoutUtils::SCROLLABLE_STOP_AT_PAGE | 36 nsLayoutUtils::SCROLLABLE_INCLUDE_HIDDEN); 37 if (!scrollContainerFrame) { 38 // We might not find any, for instance in the case of 39 // <html style="position: fixed"> 40 return nullptr; 41 } 42 return &scrollContainerFrame->EnsureStickyContainer(); 43 } 44 45 static nscoord ComputeStickySideOffset(Side aSide, 46 const nsStylePosition& aPosition, 47 nscoord aPercentBasis) { 48 // Guaranteed to resolve any use of anchor function as invalid. 49 const auto& side = aPosition.GetAnchorResolvedInset( 50 aSide, AnchorPosOffsetResolutionParams::UseCBFrameSize( 51 {nullptr, StylePositionProperty::Sticky})); 52 if (side->IsAuto()) { 53 return NS_AUTOOFFSET; 54 } 55 return nsLayoutUtils::ComputeCBDependentValue(aPercentBasis, 56 side->AsLengthPercentage()); 57 } 58 59 // static 60 void StickyScrollContainer::ComputeStickyOffsets(nsIFrame* aFrame) { 61 ScrollContainerFrame* scrollContainerFrame = 62 nsLayoutUtils::GetNearestScrollContainerFrame( 63 aFrame->GetParent(), nsLayoutUtils::SCROLLABLE_SAME_DOC | 64 nsLayoutUtils::SCROLLABLE_INCLUDE_HIDDEN); 65 66 if (!scrollContainerFrame) { 67 // Bail. 68 return; 69 } 70 71 nsSize scrollContainerSize = 72 scrollContainerFrame->GetScrolledFrameSizeAccountingForDynamicToolbar(); 73 74 nsMargin computedOffsets; 75 const nsStylePosition* position = aFrame->StylePosition(); 76 77 computedOffsets.left = 78 ComputeStickySideOffset(eSideLeft, *position, scrollContainerSize.width); 79 computedOffsets.right = 80 ComputeStickySideOffset(eSideRight, *position, scrollContainerSize.width); 81 computedOffsets.top = 82 ComputeStickySideOffset(eSideTop, *position, scrollContainerSize.height); 83 computedOffsets.bottom = ComputeStickySideOffset(eSideBottom, *position, 84 scrollContainerSize.height); 85 86 // Store the offset 87 nsMargin* offsets = aFrame->GetProperty(nsIFrame::ComputedOffsetProperty()); 88 if (offsets) { 89 *offsets = computedOffsets; 90 } else { 91 aFrame->SetProperty(nsIFrame::ComputedOffsetProperty(), 92 new nsMargin(computedOffsets)); 93 } 94 } 95 96 static constexpr nscoord gUnboundedNegative = nscoord_MIN / 2; 97 static constexpr nscoord gUnboundedExtent = nscoord_MAX; 98 static constexpr nscoord gUnboundedPositive = 99 gUnboundedNegative + gUnboundedExtent; 100 101 void StickyScrollContainer::ComputeStickyLimits(nsIFrame* aFrame, 102 nsRect* aStick, 103 nsRect* aContain) const { 104 NS_ASSERTION(nsLayoutUtils::IsFirstContinuationOrIBSplitSibling(aFrame), 105 "Can't sticky position individual continuations"); 106 107 aStick->SetRect(gUnboundedNegative, gUnboundedNegative, gUnboundedExtent, 108 gUnboundedExtent); 109 aContain->SetRect(gUnboundedNegative, gUnboundedNegative, gUnboundedExtent, 110 gUnboundedExtent); 111 112 const nsMargin* computedOffsets = 113 aFrame->GetProperty(nsIFrame::ComputedOffsetProperty()); 114 if (!computedOffsets) { 115 // We haven't reflowed the scroll frame yet, so offsets haven't been 116 // computed. Bail. 117 return; 118 } 119 120 nsIFrame* scrolledFrame = mScrollContainerFrame->GetScrolledFrame(); 121 nsIFrame* cbFrame = aFrame->GetContainingBlock(); 122 NS_ASSERTION(cbFrame == scrolledFrame || 123 nsLayoutUtils::IsProperAncestorFrame(scrolledFrame, cbFrame), 124 "Scroll frame should be an ancestor of the containing block"); 125 126 nsRect rect = 127 nsLayoutUtils::GetAllInFlowRectsUnion(aFrame, aFrame->GetParent()); 128 129 // FIXME(bug 1421660): Table row groups aren't supposed to be containing 130 // blocks, but we treat them as such (maybe it's the right thing to do!). 131 // Anyway, not having this basically disables position: sticky on table cells, 132 // which would be really unfortunate, and doesn't match what other browsers 133 // do. 134 if (cbFrame != scrolledFrame && cbFrame->IsTableRowGroupFrame()) { 135 cbFrame = cbFrame->GetContainingBlock(); 136 } 137 138 // Containing block limits for the position of aFrame relative to its parent. 139 // The margin box of the sticky element stays within the content box of the 140 // containing-block element. 141 if (cbFrame == scrolledFrame) { 142 // cbFrame is the scrolledFrame, and it won't have continuations. Unlike the 143 // else clause, we consider scrollable overflow rect because the union of 144 // its in-flow rects doesn't include the scrollable overflow area. We need 145 // to subtract the padding however, which _is_ included in the scrollable 146 // area, since we want the content box. 147 MOZ_ASSERT(cbFrame->GetUsedBorder() == nsMargin(), 148 "How did the ::-moz-scrolled-frame end up with border?"); 149 *aContain = cbFrame->ScrollableOverflowRectRelativeToSelf(); 150 aContain->Deflate(cbFrame->GetUsedPadding()); 151 nsLayoutUtils::TransformRect(cbFrame, aFrame->GetParent(), *aContain); 152 } else { 153 *aContain = nsLayoutUtils::GetAllInFlowRectsUnion( 154 cbFrame, aFrame->GetParent(), 155 nsLayoutUtils::GetAllInFlowRectsFlag::UseContentBox); 156 } 157 158 nsRect marginRect = nsLayoutUtils::GetAllInFlowRectsUnion( 159 aFrame, aFrame->GetParent(), 160 nsLayoutUtils::GetAllInFlowRectsFlag::UseMarginBoxWithAutoResolvedAsZero); 161 162 // Deflate aContain by the difference between the union of aFrame's 163 // continuations' margin boxes and the union of their border boxes, so that 164 // by keeping aFrame within aContain, we keep the union of the margin boxes 165 // within the containing block's content box. 166 aContain->Deflate(marginRect - rect); 167 168 // Deflate aContain by the border-box size, to form a constraint on the 169 // upper-left corner of aFrame and continuations. 170 aContain->Deflate(nsMargin(0, rect.width, rect.height, 0)); 171 172 nsMargin sfPadding = scrolledFrame->GetUsedPadding(); 173 nsPoint sfOffset = aFrame->GetParent()->GetOffsetTo(scrolledFrame); 174 nsSize sfSize = 175 mScrollContainerFrame->GetScrolledFrameSizeAccountingForDynamicToolbar(); 176 StyleDirection direction = cbFrame->StyleVisibility()->mDirection; 177 nsMargin effectiveOffsets = *computedOffsets; 178 179 if (computedOffsets->top != NS_AUTOOFFSET && 180 computedOffsets->bottom != NS_AUTOOFFSET) { 181 // Decrease the effective end-edge inset of the sticky view rectangle 182 // when its height is less than the height of the border-box of the sticky 183 // box. 184 nscoord stickyViewHeight = sfSize.height - computedOffsets->TopBottom(); 185 if (rect.height > stickyViewHeight) { 186 nscoord delta = rect.height - stickyViewHeight; 187 effectiveOffsets.bottom -= delta; 188 } 189 } 190 191 if (computedOffsets->left != NS_AUTOOFFSET && 192 computedOffsets->right != NS_AUTOOFFSET) { 193 // Decrease the effective end-edge inset of the sticky view rectangle 194 // when its width is less than the width of the border-box of the sticky 195 // box. 196 nscoord stickyViewWidth = sfSize.width - computedOffsets->LeftRight(); 197 if (rect.width > stickyViewWidth) { 198 nscoord delta = rect.width - stickyViewWidth; 199 if (direction == StyleDirection::Ltr) { 200 effectiveOffsets.right -= delta; 201 } else { 202 effectiveOffsets.left -= delta; 203 } 204 } 205 } 206 207 // Top 208 if (computedOffsets->top != NS_AUTOOFFSET) { 209 aStick->SetTopEdge(mScrollPosition.y + sfPadding.top + 210 effectiveOffsets.top - sfOffset.y); 211 } 212 213 // Bottom 214 if (computedOffsets->bottom != NS_AUTOOFFSET) { 215 aStick->SetBottomEdge(mScrollPosition.y + sfPadding.top + sfSize.height - 216 effectiveOffsets.bottom - rect.height - sfOffset.y); 217 } 218 219 // Left 220 if (computedOffsets->left != NS_AUTOOFFSET) { 221 aStick->SetLeftEdge(mScrollPosition.x + sfPadding.left + 222 effectiveOffsets.left - sfOffset.x); 223 } 224 225 // Right 226 if (computedOffsets->right != NS_AUTOOFFSET) { 227 aStick->SetRightEdge(mScrollPosition.x + sfPadding.left + sfSize.width - 228 effectiveOffsets.right - rect.width - sfOffset.x); 229 } 230 231 // These limits are for the bounding box of aFrame's continuations. Convert 232 // to limits for aFrame itself. 233 nsPoint frameOffset = aFrame->GetPosition() - rect.TopLeft(); 234 aStick->MoveBy(frameOffset); 235 aContain->MoveBy(frameOffset); 236 } 237 238 nsPoint StickyScrollContainer::ComputePosition(nsIFrame* aFrame) const { 239 nsRect stick; 240 nsRect contain; 241 ComputeStickyLimits(aFrame, &stick, &contain); 242 243 nsPoint position = aFrame->GetNormalPosition(); 244 245 // For each sticky direction (top, bottom, left, right), move the frame along 246 // the appropriate axis, based on the scroll position, but limit this to keep 247 // the element's margin box within the containing block. 248 position.y = std::max(position.y, std::min(stick.y, contain.YMost())); 249 position.y = std::min(position.y, std::max(stick.YMost(), contain.y)); 250 position.x = std::max(position.x, std::min(stick.x, contain.XMost())); 251 position.x = std::min(position.x, std::max(stick.XMost(), contain.x)); 252 253 return position; 254 } 255 256 bool StickyScrollContainer::IsStuckInYDirection(nsIFrame* aFrame) const { 257 nsPoint position = ComputePosition(aFrame); 258 return position.y != aFrame->GetNormalPosition().y; 259 } 260 261 void StickyScrollContainer::GetScrollRanges(nsIFrame* aFrame, 262 nsRectAbsolute* aOuter, 263 nsRectAbsolute* aInner) const { 264 // We need to use the first in flow; continuation frames should not move 265 // relative to each other and should get identical scroll ranges. 266 // Also, ComputeStickyLimits requires this. 267 nsIFrame* firstCont = 268 nsLayoutUtils::FirstContinuationOrIBSplitSibling(aFrame); 269 270 nsRect stickRect; 271 nsRect containRect; 272 ComputeStickyLimits(firstCont, &stickRect, &containRect); 273 274 nsRectAbsolute stick = nsRectAbsolute::FromRect(stickRect); 275 nsRectAbsolute contain = nsRectAbsolute::FromRect(containRect); 276 277 aOuter->SetBox(gUnboundedNegative, gUnboundedNegative, gUnboundedPositive, 278 gUnboundedPositive); 279 aInner->SetBox(gUnboundedNegative, gUnboundedNegative, gUnboundedPositive, 280 gUnboundedPositive); 281 282 const nsPoint normalPosition = firstCont->GetNormalPosition(); 283 284 // Bottom and top 285 if (stick.YMost() != gUnboundedPositive) { 286 aOuter->SetTopEdge(contain.Y() - stick.YMost()); 287 aInner->SetTopEdge(normalPosition.y - stick.YMost()); 288 } 289 290 if (stick.Y() != gUnboundedNegative) { 291 aInner->SetBottomEdge(normalPosition.y - stick.Y()); 292 aOuter->SetBottomEdge(contain.YMost() - stick.Y()); 293 } 294 295 // Right and left 296 if (stick.XMost() != gUnboundedPositive) { 297 aOuter->SetLeftEdge(contain.X() - stick.XMost()); 298 aInner->SetLeftEdge(normalPosition.x - stick.XMost()); 299 } 300 301 if (stick.X() != gUnboundedNegative) { 302 aInner->SetRightEdge(normalPosition.x - stick.X()); 303 aOuter->SetRightEdge(contain.XMost() - stick.X()); 304 } 305 306 // Make sure |inner| does not extend outside of |outer|. (The consumers of 307 // the Layers API, to which this information is propagated, expect this 308 // invariant to hold.) The calculated value of |inner| can sometimes extend 309 // outside of |outer|, for example due to margin collapsing, since 310 // GetNormalPosition() returns the actual position after margin collapsing, 311 // while |contain| is calculated based on the frame's GetUsedMargin() which 312 // is pre-collapsing. 313 // Note that this doesn't necessarily solve all problems stemming from 314 // comparing pre- and post-collapsing margins (TODO: find a proper solution). 315 *aInner = aInner->Intersect(*aOuter); 316 if (aInner->IsEmpty()) { 317 // This might happen if aInner didn't intersect aOuter at all initially, 318 // in which case aInner is empty and outside aOuter. Make sure it doesn't 319 // extend outside aOuter. 320 *aInner = aInner->MoveInsideAndClamp(*aOuter); 321 } 322 } 323 324 void StickyScrollContainer::PositionContinuations(nsIFrame* aFrame) { 325 NS_ASSERTION(nsLayoutUtils::IsFirstContinuationOrIBSplitSibling(aFrame), 326 "Should be starting from the first continuation"); 327 bool hadProperty; 328 nsPoint translation = 329 ComputePosition(aFrame) - aFrame->GetNormalPosition(&hadProperty); 330 if (NS_WARN_IF(!hadProperty)) { 331 // If the frame was never relatively positioned, don't move its position 332 // dynamically. There are a variety of frames for which `position` doesn't 333 // really apply like frames inside svg which would get here and be sticky 334 // only in one direction. 335 return; 336 } 337 338 // Move all continuation frames by the same amount. 339 for (nsIFrame* cont = aFrame; cont; 340 cont = nsLayoutUtils::GetNextContinuationOrIBSplitSibling(cont)) { 341 cont->SetPosition(cont->GetNormalPosition() + translation); 342 } 343 } 344 345 void StickyScrollContainer::UpdatePositions(nsPoint aScrollPosition, 346 nsIFrame* aSubtreeRoot) { 347 #ifdef DEBUG 348 { 349 nsIFrame* scrollFrameAsFrame = do_QueryFrame(mScrollContainerFrame); 350 NS_ASSERTION(!aSubtreeRoot || aSubtreeRoot == scrollFrameAsFrame, 351 "If reflowing, should be reflowing the scroll frame"); 352 } 353 #endif 354 mScrollPosition = aScrollPosition; 355 356 OverflowChangedTracker oct; 357 oct.SetSubtreeRoot(aSubtreeRoot); 358 // We need to position ancestors before children, so iter from shallowest. 359 // Collect a list of frames to be removed, so that we don't invalidate the 360 // iterator while we're using it. 361 AutoTArray<nsIFrame*, 8> framesToRemove; 362 for (nsIFrame* f : mFrames.IterFromShallowest()) { 363 if (!nsLayoutUtils::IsFirstContinuationOrIBSplitSibling(f)) { 364 // This frame was added in nsIFrame::DidSetComputedStyle before we knew it 365 // wasn't the first ib-split-sibling. 366 framesToRemove.AppendElement(f); 367 continue; 368 } 369 if (aSubtreeRoot) { 370 // Reflowing the scroll frame, so recompute offsets. 371 ComputeStickyOffsets(f); 372 } 373 // mFrames will only contain first continuations, because we filter in 374 // nsIFrame::DidSetComputedStyle. 375 PositionContinuations(f); 376 377 f = f->GetParent(); 378 if (f != aSubtreeRoot) { 379 for (nsIFrame* cont = f; cont; 380 cont = nsLayoutUtils::GetNextContinuationOrIBSplitSibling(cont)) { 381 oct.AddFrame(cont, OverflowChangedTracker::CHILDREN_CHANGED); 382 } 383 } 384 } 385 for (nsIFrame* f : framesToRemove) { 386 mFrames.Remove(f); 387 } 388 oct.Flush(); 389 } 390 391 void StickyScrollContainer::MarkFramesForReflow() { 392 PresShell* ps = mScrollContainerFrame->PresShell(); 393 for (nsIFrame* frame : mFrames.IterFromShallowest()) { 394 ps->FrameNeedsReflow(frame, IntrinsicDirty::None, NS_FRAME_IS_DIRTY); 395 } 396 } 397 } // namespace mozilla