AnchorPositioningUtils.cpp (42181B)
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 #include "AnchorPositioningUtils.h" 8 9 #include "DisplayPortUtils.h" 10 #include "ScrollContainerFrame.h" 11 #include "mozilla/Maybe.h" 12 #include "mozilla/PresShell.h" 13 #include "mozilla/StaticPrefs_apz.h" 14 #include "mozilla/dom/DOMIntersectionObserver.h" 15 #include "mozilla/dom/Document.h" 16 #include "mozilla/dom/Element.h" 17 #include "nsCanvasFrame.h" 18 #include "nsContainerFrame.h" 19 #include "nsDisplayList.h" 20 #include "nsIContent.h" 21 #include "nsIFrame.h" 22 #include "nsIFrameInlines.h" 23 #include "nsINode.h" 24 #include "nsLayoutUtils.h" 25 #include "nsPlaceholderFrame.h" 26 #include "nsStyleStruct.h" 27 #include "nsTArray.h" 28 29 namespace mozilla { 30 31 namespace { 32 33 bool IsScrolled(const nsIFrame* aFrame) { 34 switch (aFrame->Style()->GetPseudoType()) { 35 case PseudoStyleType::scrolledContent: 36 case PseudoStyleType::scrolledCanvas: 37 return true; 38 default: 39 return false; 40 } 41 } 42 43 bool DoTreeScopedPropertiesOfElementApplyToContent( 44 const nsINode* aStylePropertyElement, const nsINode* aStyledContent) { 45 // XXX: The proper implementation is deferred to bug 1988038 46 // concerning tree-scoped name resolution. For now, we just 47 // keep the shadow and light trees separate. 48 return aStylePropertyElement->GetContainingDocumentOrShadowRoot() == 49 aStyledContent->GetContainingDocumentOrShadowRoot(); 50 } 51 52 /** 53 * Checks for the implementation of `anchor-scope`: 54 * https://drafts.csswg.org/css-anchor-position-1/#anchor-scope 55 * 56 * TODO: Consider caching the ancestors, see bug 1986347 57 */ 58 bool IsAnchorInScopeForPositionedElement(const nsAtom* aName, 59 const nsIFrame* aPossibleAnchorFrame, 60 const nsIFrame* aPositionedFrame) { 61 // We don't need to look beyond positioned element's containing block. 62 const auto* positionedContainingBlockContent = 63 aPositionedFrame->GetParent()->GetContent(); 64 65 auto getAnchorPosNearestScope = 66 [&](const nsAtom* aName, const nsIFrame* aFrame) -> const nsIContent* { 67 // We need to traverse the DOM, not the frame tree, since `anchor-scope` 68 // may be present on elements with `display: contents` (in which case its 69 // frame is in the `::before` list and won't be found by walking the frame 70 // tree parent chain). 71 for (nsIContent* cp = aFrame->GetContent(); 72 cp && cp != positionedContainingBlockContent; 73 cp = cp->GetFlattenedTreeParentElementForStyle()) { 74 const auto* anchorScope = [&]() -> const StyleAnchorScope* { 75 const nsIFrame* f = nsLayoutUtils::GetStyleFrame(cp); 76 if (MOZ_LIKELY(f)) { 77 return &f->StyleDisplay()->mAnchorScope; 78 } 79 if (cp->AsElement()->IsDisplayContents()) { 80 const auto* style = 81 Servo_Element_GetMaybeOutOfDateStyle(cp->AsElement()); 82 MOZ_ASSERT(style); 83 return &style->StyleDisplay()->mAnchorScope; 84 } 85 return nullptr; 86 }(); 87 88 if (!anchorScope || anchorScope->IsNone()) { 89 continue; 90 } 91 92 if (anchorScope->IsAll()) { 93 return cp; 94 } 95 96 MOZ_ASSERT(anchorScope->IsIdents()); 97 for (const StyleAtom& ident : anchorScope->AsIdents().AsSpan()) { 98 if (aName == ident.AsAtom()) { 99 return cp; 100 } 101 } 102 } 103 return nullptr; 104 }; 105 106 const nsIContent* nearestScopeForAnchor = 107 getAnchorPosNearestScope(aName, aPossibleAnchorFrame); 108 const nsIContent* nearestScopeForPositioned = 109 getAnchorPosNearestScope(aName, aPositionedFrame); 110 if (!nearestScopeForAnchor) { 111 // Anchor is not scoped and positioned element also should 112 // not be gated by a scope. 113 return !nearestScopeForPositioned || 114 aPossibleAnchorFrame->GetContent() == nearestScopeForPositioned; 115 } 116 117 // There may not be any other scopes between the positioned element 118 // and the nearest scope of the anchor. 119 return nearestScopeForAnchor == nearestScopeForPositioned; 120 }; 121 122 bool IsFullyStyleableTreeAbidingOrNotPseudoElement(const nsIFrame* aFrame) { 123 if (!aFrame->Style()->IsPseudoElement()) { 124 return true; 125 } 126 127 const PseudoStyleType pseudoElementType = aFrame->Style()->GetPseudoType(); 128 129 // See https://www.w3.org/TR/css-pseudo-4/#treelike 130 return pseudoElementType == PseudoStyleType::before || 131 pseudoElementType == PseudoStyleType::after || 132 pseudoElementType == PseudoStyleType::marker; 133 } 134 135 size_t GetTopLayerIndex(const nsIFrame* aFrame) { 136 MOZ_ASSERT(aFrame); 137 138 const nsIContent* frameContent = aFrame->GetContent(); 139 140 if (!frameContent) { 141 return 0; 142 } 143 144 // Within the array returned by Document::GetTopLayer, 145 // a higher index means the layer sits higher in the stack, 146 // matching Document::GetTopLayerTop()’s top-to-bottom logic. 147 // See https://drafts.csswg.org/css-position-4/#in-a-higher-top-layer 148 const nsTArray<dom::Element*>& topLayers = 149 frameContent->OwnerDoc()->GetTopLayer(); 150 151 for (size_t index = 0; index < topLayers.Length(); ++index) { 152 const auto& topLayer = topLayers.ElementAt(index); 153 if (nsContentUtils::ContentIsFlattenedTreeDescendantOfForStyle( 154 /* aPossibleDescendant */ frameContent, 155 /* aPossibleAncestor */ topLayer)) { 156 return 1 + index; 157 } 158 } 159 160 return 0; 161 } 162 163 bool IsInitialContainingBlock(const nsIFrame* aContainingBlock) { 164 // Initial containing block: The containing block of the root element. 165 // https://drafts.csswg.org/css-display-4/#initial-containing-block 166 return aContainingBlock == aContainingBlock->PresShell() 167 ->FrameConstructor() 168 ->GetDocElementContainingBlock(); 169 } 170 171 bool IsContainingBlockGeneratedByElement(const nsIFrame* aContainingBlock) { 172 // 2.1. Containing Blocks of Positioned Boxes 173 // https://www.w3.org/TR/css-position-3/#def-cb 174 return !(!aContainingBlock || aContainingBlock->IsViewportFrame() || 175 IsInitialContainingBlock(aContainingBlock)); 176 } 177 178 bool IsAnchorLaidOutStrictlyBeforeElement( 179 const nsIFrame* aPossibleAnchorFrame, const nsIFrame* aPositionedFrame, 180 const nsTArray<const nsIFrame*>& aPositionedFrameAncestors) { 181 // 1. positioned el is in a higher top layer than possible anchor, 182 // see https://drafts.csswg.org/css-position-4/#in-a-higher-top-layer 183 const size_t positionedTopLayerIndex = GetTopLayerIndex(aPositionedFrame); 184 const size_t anchorTopLayerIndex = GetTopLayerIndex(aPossibleAnchorFrame); 185 186 if (anchorTopLayerIndex != positionedTopLayerIndex) { 187 return anchorTopLayerIndex < positionedTopLayerIndex; 188 } 189 190 // Note: The containing block of an absolutely positioned element 191 // is just the parent frame. 192 const nsIFrame* positionedContainingBlock = aPositionedFrame->GetParent(); 193 // Note(dshin, bug 1985654): Spec strictly uses the term "containing block," 194 // corresponding to `GetContainingBlock()`. However, this leads to cases 195 // where an anchor's non-inline containing block prevents it from being a 196 // valid anchor for a absolutely positioned element (Which can explicitly 197 // have inline elements as a containing block). Some WPT rely on inline 198 // containing blocks as well. 199 // See also: https://github.com/w3c/csswg-drafts/issues/12674 200 const nsIFrame* anchorContainingBlock = aPossibleAnchorFrame->GetParent(); 201 202 // 2. Both elements are in the same top layer but have different 203 // containing blocks and positioned el's containing block is an 204 // ancestor of possible anchor's containing block in the containing 205 // block chain, aka one of the following: 206 if (anchorContainingBlock != positionedContainingBlock) { 207 // 2.1 positioned el's containing block is the viewport, and 208 // possible anchor's containing block isn't. 209 if (positionedContainingBlock->IsViewportFrame() && 210 !anchorContainingBlock->IsViewportFrame()) { 211 return !nsLayoutUtils::IsProperAncestorFrame(aPositionedFrame, 212 aPossibleAnchorFrame); 213 } 214 215 auto isLastContainingBlockOrderable = 216 [&aPositionedFrame, &aPositionedFrameAncestors, &anchorContainingBlock, 217 &positionedContainingBlock]() -> bool { 218 const nsIFrame* it = anchorContainingBlock; 219 while (it) { 220 const nsIFrame* parentContainingBlock = it->GetParent(); 221 if (!parentContainingBlock) { 222 return false; 223 } 224 225 if (parentContainingBlock == positionedContainingBlock) { 226 return !it->IsAbsolutelyPositioned() || 227 nsLayoutUtils::CompareTreePosition(it, aPositionedFrame, 228 aPositionedFrameAncestors, 229 nullptr) < 0; 230 } 231 232 it = parentContainingBlock; 233 } 234 235 return false; 236 }; 237 238 // 2.2 positioned el's containing block is the initial containing 239 // block, and possible anchor's containing block is generated by an 240 // element, and the last containing block in possible anchor's containing 241 // block chain before reaching positioned el's containing block is either 242 // not absolutely positioned or precedes positioned el in the tree order, 243 const bool isAnchorContainingBlockGenerated = 244 IsContainingBlockGeneratedByElement(anchorContainingBlock); 245 if (isAnchorContainingBlockGenerated && 246 IsInitialContainingBlock(positionedContainingBlock)) { 247 return isLastContainingBlockOrderable(); 248 } 249 250 // 2.3 both elements' containing blocks are generated by elements, 251 // and positioned el's containing block is an ancestor in the flat 252 // tree to that of possible anchor's containing block, and the last 253 // containing block in possible anchor’s containing block chain before 254 // reaching positioned el’s containing block is either not absolutely 255 // positioned or precedes positioned el in the tree order. 256 if (isAnchorContainingBlockGenerated && 257 IsContainingBlockGeneratedByElement(positionedContainingBlock)) { 258 return isLastContainingBlockOrderable(); 259 } 260 261 return false; 262 } 263 264 // 3. Both elements are in the same top layer and have the same 265 // containing block, and are both absolutely positioned, and possible 266 // anchor is earlier in flat tree order than positioned el. 267 const bool isAnchorAbsolutelyPositioned = 268 aPossibleAnchorFrame->IsAbsolutelyPositioned(); 269 if (isAnchorAbsolutelyPositioned) { 270 // We must have checked that the positioned element is absolutely 271 // positioned by now. 272 return nsLayoutUtils::CompareTreePosition( 273 aPossibleAnchorFrame, aPositionedFrame, 274 aPositionedFrameAncestors, nullptr) < 0; 275 } 276 277 // 4. Both elements are in the same top layer and have the same 278 // containing block, but possible anchor isn't absolutely positioned. 279 return !isAnchorAbsolutelyPositioned; 280 } 281 282 /** 283 * https://drafts.csswg.org/css-contain-2/#skips-its-contents 284 */ 285 bool IsPositionedElementAlsoSkippedWhenAnchorIsSkipped( 286 const nsIFrame* aPossibleAnchorFrame, const nsIFrame* aPositionedFrame) { 287 // If potential anchor is skipped and a root of a visibility subtree, 288 // it can never be acceptable. 289 if (aPossibleAnchorFrame->HidesContentForLayout()) { 290 return false; 291 } 292 293 // If possible anchor is in the skipped contents of another element, 294 // then positioned el shall be in the skipped contents of that same element. 295 const nsIFrame* visibilityAncestor = aPossibleAnchorFrame->GetParent(); 296 while (visibilityAncestor) { 297 // If anchor is skipped via auto or hidden, it cannot be acceptable, 298 // be it a root or a non-root of a visibility subtree. 299 if (visibilityAncestor->HidesContentForLayout()) { 300 break; 301 } 302 303 visibilityAncestor = visibilityAncestor->GetParent(); 304 } 305 306 // If positioned el is skipped and a root of a visibility subtree, 307 // an anchor can never be acceptable. 308 if (aPositionedFrame->HidesContentForLayout()) { 309 return false; 310 } 311 312 const nsIFrame* ancestor = aPositionedFrame; 313 while (ancestor) { 314 if (ancestor->HidesContentForLayout()) { 315 return ancestor == visibilityAncestor; 316 } 317 318 ancestor = ancestor->GetParent(); 319 } 320 321 return true; 322 } 323 324 class LazyAncestorHolder { 325 const nsIFrame* mFrame; 326 AutoTArray<const nsIFrame*, 8> mAncestors; 327 bool mFilled = false; 328 329 public: 330 const nsTArray<const nsIFrame*>& GetAncestors() { 331 if (!mFilled) { 332 nsLayoutUtils::FillAncestors(mFrame, nullptr, &mAncestors); 333 mFilled = true; 334 } 335 return mAncestors; 336 } 337 338 explicit LazyAncestorHolder(const nsIFrame* aFrame) : mFrame(aFrame) {} 339 }; 340 341 bool IsAcceptableAnchorElement( 342 const nsIFrame* aPossibleAnchorFrame, const nsAtom* aName, 343 const nsIFrame* aPositionedFrame, 344 LazyAncestorHolder& aPositionedFrameAncestorHolder) { 345 MOZ_ASSERT(aPossibleAnchorFrame); 346 MOZ_ASSERT(aPositionedFrame); 347 348 // An element possible anchor is an acceptable anchor element for an 349 // absolutely positioned element positioned el if all of the following are 350 // true: 351 // - possible anchor is either an element or a fully styleable 352 // tree-abiding pseudo-element. 353 // - possible anchor is in scope for positioned el, per the effects of 354 // anchor-scope on positioned el or its ancestors. 355 // - possible anchor is laid out strictly before positioned el 356 // 357 // Note: Frames having an anchor name contain elements. 358 // The phrase "element or a fully styleable tree-abiding pseudo-element" 359 // used by the spec is taken to mean 360 // "either not a pseudo-element or a pseudo-element of a specific kind". 361 return (IsFullyStyleableTreeAbidingOrNotPseudoElement(aPossibleAnchorFrame) && 362 IsAnchorLaidOutStrictlyBeforeElement( 363 aPossibleAnchorFrame, aPositionedFrame, 364 aPositionedFrameAncestorHolder.GetAncestors()) && 365 IsAnchorInScopeForPositionedElement(aName, aPossibleAnchorFrame, 366 aPositionedFrame) && 367 IsPositionedElementAlsoSkippedWhenAnchorIsSkipped( 368 aPossibleAnchorFrame, aPositionedFrame)); 369 } 370 371 } // namespace 372 373 AnchorPosReferenceData::Result AnchorPosReferenceData::InsertOrModify( 374 const nsAtom* aAnchorName, bool aNeedOffset) { 375 bool exists = true; 376 auto* result = &mMap.LookupOrInsertWith(aAnchorName, [&exists]() { 377 exists = false; 378 return Nothing{}; 379 }); 380 381 if (!exists) { 382 return {false, result}; 383 } 384 385 // We tried to resolve before. 386 if (result->isNothing()) { 387 // We know this reference is invalid. 388 return {true, result}; 389 } 390 // Previous resolution found a valid anchor. 391 if (!aNeedOffset) { 392 // Size is guaranteed to be populated on resolution. 393 return {true, result}; 394 } 395 396 // Previous resolution may have been for size only, in which case another 397 // anchor resolution is still required. 398 return {result->ref().mOffsetData.isSome(), result}; 399 } 400 401 const AnchorPosReferenceData::Value* AnchorPosReferenceData::Lookup( 402 const nsAtom* aAnchorName) const { 403 return mMap.Lookup(aAnchorName).DataPtrOrNull(); 404 } 405 406 AnchorPosDefaultAnchorCache::AnchorPosDefaultAnchorCache( 407 const nsIFrame* aAnchor, const nsIFrame* aScrollContainer) 408 : mAnchor{aAnchor}, mScrollContainer{aScrollContainer} { 409 MOZ_ASSERT_IF( 410 aAnchor, 411 nsLayoutUtils::GetNearestScrollContainerFrame( 412 const_cast<nsContainerFrame*>(aAnchor->GetParent()), 413 nsLayoutUtils::SCROLLABLE_SAME_DOC | 414 nsLayoutUtils::SCROLLABLE_INCLUDE_HIDDEN) == mScrollContainer); 415 } 416 417 nsIFrame* AnchorPositioningUtils::FindFirstAcceptableAnchor( 418 const nsAtom* aName, const nsIFrame* aPositionedFrame, 419 const nsTArray<nsIFrame*>& aPossibleAnchorFrames) { 420 LazyAncestorHolder positionedFrameAncestorHolder(aPositionedFrame); 421 const auto* positionedContent = aPositionedFrame->GetContent(); 422 423 for (auto it = aPossibleAnchorFrames.rbegin(); 424 it != aPossibleAnchorFrames.rend(); ++it) { 425 const nsIFrame* possibleAnchorFrame = *it; 426 if (!DoTreeScopedPropertiesOfElementApplyToContent( 427 possibleAnchorFrame->GetContent(), positionedContent)) { 428 // Skip anchors in different shadow trees. 429 continue; 430 } 431 432 // Check if the possible anchor is an acceptable anchor element. 433 if (IsAcceptableAnchorElement(*it, aName, aPositionedFrame, 434 positionedFrameAncestorHolder)) { 435 return *it; 436 } 437 } 438 439 // If we reach here, we didn't find any acceptable anchor. 440 return nullptr; 441 } 442 443 // Find the aContainer's child that is the ancestor of aDescendant. 444 static const nsIFrame* TraverseUpToContainerChild(const nsIFrame* aContainer, 445 const nsIFrame* aDescendant) { 446 const auto* current = aDescendant; 447 while (true) { 448 const auto* parent = current->GetParent(); 449 if (!parent) { 450 return nullptr; 451 } 452 if (parent == aContainer) { 453 return current; 454 } 455 current = parent; 456 } 457 } 458 459 static const nsIFrame* GetAnchorOf(const nsIFrame* aPositioned, 460 const nsAtom* aAnchorName) { 461 const auto* presShell = aPositioned->PresShell(); 462 MOZ_ASSERT(presShell, "No PresShell for frame?"); 463 return presShell->GetAnchorPosAnchor(aAnchorName, aPositioned); 464 } 465 466 Maybe<nsRect> AnchorPositioningUtils::GetAnchorPosRect( 467 const nsIFrame* aAbsoluteContainingBlock, const nsIFrame* aAnchor, 468 bool aCBRectIsvalid) { 469 auto rect = [&]() -> Maybe<nsRect> { 470 if (aCBRectIsvalid) { 471 const nsRect result = 472 nsLayoutUtils::GetCombinedFragmentRects(aAnchor, true); 473 const auto offset = 474 aAnchor->GetOffsetToIgnoringScrolling(aAbsoluteContainingBlock); 475 // Easy, just use the existing function. 476 return Some(result + offset); 477 } 478 479 // Ok, containing block doesn't have its rect fully resolved. Figure out 480 // rect relative to the child of containing block that is also the ancestor 481 // of the anchor, and manually compute the offset. 482 // TODO(dshin): This wouldn't handle anchor in a previous top layer. 483 const auto* containerChild = 484 TraverseUpToContainerChild(aAbsoluteContainingBlock, aAnchor); 485 if (!containerChild) { 486 return Nothing{}; 487 } 488 489 if (aAnchor == containerChild) { 490 // Anchor is the direct child of anchor's CBWM. 491 return Some(nsLayoutUtils::GetCombinedFragmentRects(aAnchor, false)); 492 } 493 494 // TODO(dshin): Already traversed up to find `containerChild`, and we're 495 // going to do it again here, which feels a little wasteful. 496 const nsRect rectToContainerChild = 497 nsLayoutUtils::GetCombinedFragmentRects(aAnchor, true); 498 const auto offset = aAnchor->GetOffsetToIgnoringScrolling(containerChild); 499 return Some(rectToContainerChild + offset + containerChild->GetPosition()); 500 }(); 501 return rect.map([&](const nsRect& aRect) { 502 // We need to position the border box of the anchor within the abspos 503 // containing block's size - So the rectangle's size (i.e. Anchor size) 504 // stays the same, while "the outer rectangle" (i.e. The abspos cb size) 505 // "shrinks" by shifting the position. 506 const auto border = aAbsoluteContainingBlock->GetUsedBorder(); 507 const nsPoint borderTopLeft{border.left, border.top}; 508 const auto rect = aRect - borderTopLeft; 509 return rect; 510 }); 511 } 512 513 Maybe<AnchorPosInfo> AnchorPositioningUtils::ResolveAnchorPosRect( 514 const nsIFrame* aPositioned, const nsIFrame* aAbsoluteContainingBlock, 515 const nsAtom* aAnchorName, bool aCBRectIsvalid, 516 AnchorPosResolutionCache* aResolutionCache) { 517 if (!aPositioned) { 518 return Nothing{}; 519 } 520 521 if (!aPositioned->HasAnyStateBits(NS_FRAME_OUT_OF_FLOW)) { 522 return Nothing{}; 523 } 524 525 MOZ_ASSERT(aPositioned->GetParent() == aAbsoluteContainingBlock); 526 527 const auto* anchorName = GetUsedAnchorName(aPositioned, aAnchorName); 528 if (!anchorName) { 529 return Nothing{}; 530 } 531 532 Maybe<AnchorPosResolutionData>* entry = nullptr; 533 if (aResolutionCache) { 534 const auto result = 535 aResolutionCache->mReferenceData->InsertOrModify(anchorName, true); 536 if (result.mAlreadyResolved) { 537 MOZ_ASSERT(result.mEntry, "Entry exists but null?"); 538 return result.mEntry->map([&](const AnchorPosResolutionData& aData) { 539 MOZ_ASSERT(aData.mOffsetData, "Missing anchor offset resolution."); 540 const auto& offsetData = aData.mOffsetData.ref(); 541 return AnchorPosInfo{nsRect{offsetData.mOrigin, aData.mSize}, 542 offsetData.mCompensatesForScroll}; 543 }); 544 } 545 entry = result.mEntry; 546 } 547 548 const auto* anchor = GetAnchorOf(aPositioned, anchorName); 549 if (!anchor) { 550 // If we have a cached entry, just check that it resolved to nothing last 551 // time as well. 552 MOZ_ASSERT_IF(entry, entry->isNothing()); 553 return Nothing{}; 554 } 555 556 const auto result = 557 GetAnchorPosRect(aAbsoluteContainingBlock, anchor, aCBRectIsvalid); 558 return result.map([&](const nsRect& aRect) { 559 bool compensatesForScroll = false; 560 DistanceToNearestScrollContainer distanceToNearestScrollContainer; 561 if (aResolutionCache) { 562 MOZ_ASSERT(entry); 563 // Update the cache. 564 compensatesForScroll = [&]() { 565 auto& defaultAnchorCache = aResolutionCache->mDefaultAnchorCache; 566 if (!aAnchorName) { 567 // Explicitly resolved default anchor for the first time - populate 568 // the cache. 569 defaultAnchorCache.mAnchor = anchor; 570 const auto [scrollContainer, distance] = 571 AnchorPositioningUtils::GetNearestScrollFrame(anchor); 572 distanceToNearestScrollContainer = distance; 573 defaultAnchorCache.mScrollContainer = scrollContainer; 574 aResolutionCache->mReferenceData->mDistanceToDefaultScrollContainer = 575 distance; 576 aResolutionCache->mReferenceData->mDefaultAnchorName = anchorName; 577 // This is the default anchor, so scroll compensated by definition. 578 return true; 579 } 580 if (defaultAnchorCache.mAnchor == anchor) { 581 // This is referring to the default anchor, so scroll compensated by 582 // definition. 583 return true; 584 } 585 const auto [scrollContainer, distance] = 586 AnchorPositioningUtils::GetNearestScrollFrame(anchor); 587 distanceToNearestScrollContainer = distance; 588 return scrollContainer == 589 aResolutionCache->mDefaultAnchorCache.mScrollContainer; 590 }(); 591 // If a partially resolved entry exists, make sure that it matches what we 592 // have now. 593 MOZ_ASSERT_IF(*entry, entry->ref().mSize == aRect.Size()); 594 *entry = Some(AnchorPosResolutionData{ 595 aRect.Size(), 596 Some(AnchorPosOffsetData{aRect.TopLeft(), compensatesForScroll, 597 distanceToNearestScrollContainer}), 598 }); 599 } 600 return AnchorPosInfo{aRect, compensatesForScroll}; 601 }); 602 } 603 604 Maybe<nsSize> AnchorPositioningUtils::ResolveAnchorPosSize( 605 const nsIFrame* aPositioned, const nsAtom* aAnchorName, 606 AnchorPosResolutionCache* aResolutionCache) { 607 const auto* anchorName = GetUsedAnchorName(aPositioned, aAnchorName); 608 if (!anchorName) { 609 return Nothing{}; 610 } 611 Maybe<AnchorPosResolutionData>* entry = nullptr; 612 auto* referencedAnchors = 613 aResolutionCache ? aResolutionCache->mReferenceData : nullptr; 614 if (referencedAnchors) { 615 const auto result = referencedAnchors->InsertOrModify(anchorName, false); 616 if (result.mAlreadyResolved) { 617 MOZ_ASSERT(result.mEntry, "Entry exists but null?"); 618 return result.mEntry->map( 619 [](const AnchorPosResolutionData& aData) { return aData.mSize; }); 620 } 621 entry = result.mEntry; 622 } 623 const auto* anchor = GetAnchorOf(aPositioned, anchorName); 624 if (!anchor) { 625 return Nothing{}; 626 } 627 const auto size = nsLayoutUtils::GetCombinedFragmentRects(anchor).Size(); 628 if (entry) { 629 *entry = Some(AnchorPosResolutionData{size, Nothing{}}); 630 } 631 return Some(size); 632 } 633 634 /** 635 * Returns an equivalent StylePositionArea that contains: 636 * [ 637 * [ left | center | right | span-left | span-right | span-all] 638 * [ top | center | bottom | span-top | span-bottom | span-all] 639 * ] 640 */ 641 static StylePositionArea ToPhysicalPositionArea(StylePositionArea aPosArea, 642 WritingMode aCbWM, 643 WritingMode aPosWM) { 644 StyleWritingMode cbwm{aCbWM.GetBits()}; 645 StyleWritingMode wm{aPosWM.GetBits()}; 646 Servo_PhysicalizePositionArea(&aPosArea, &cbwm, &wm); 647 return aPosArea; 648 } 649 650 nsRect AnchorPositioningUtils::AdjustAbsoluteContainingBlockRectForPositionArea( 651 const nsRect& aAnchorRect, const nsRect& aCBRect, WritingMode aPositionedWM, 652 WritingMode aCBWM, const StylePositionArea& aPosArea, 653 StylePositionArea* aOutResolvedArea) { 654 // Get the boundaries of 3x3 grid in CB's frame space. The edges of the 655 // default anchor box are clamped to the bounds of the CB, even if that 656 // results in zero width/height cells. 657 // 658 // ltrEdges[0] ltrEdges[1] ltrEdges[2] ltrEdges[3] 659 // | | | | 660 // ttbEdges[0] +------------+------------+------------+ 661 // | | | | 662 // ttbEdges[1] +------------+------------+------------+ 663 // | | | | 664 // ttbEdges[2] +------------+------------+------------+ 665 // | | | | 666 // ttbEdges[3] +------------+------------+------------+ 667 668 const nsRect gridRect = aCBRect.Union(aAnchorRect); 669 nscoord ltrEdges[4] = {gridRect.x, aAnchorRect.x, 670 aAnchorRect.x + aAnchorRect.width, 671 gridRect.x + gridRect.width}; 672 nscoord ttbEdges[4] = {gridRect.y, aAnchorRect.y, 673 aAnchorRect.y + aAnchorRect.height, 674 gridRect.y + gridRect.height}; 675 ltrEdges[1] = std::clamp(ltrEdges[1], ltrEdges[0], ltrEdges[3]); 676 ltrEdges[2] = std::clamp(ltrEdges[2], ltrEdges[0], ltrEdges[3]); 677 ttbEdges[1] = std::clamp(ttbEdges[1], ttbEdges[0], ttbEdges[3]); 678 ttbEdges[2] = std::clamp(ttbEdges[2], ttbEdges[0], ttbEdges[3]); 679 680 nsRect res = gridRect; 681 682 // PositionArea, resolved to only contain Left/Right/Top/Bottom values. 683 StylePositionArea posArea = 684 ToPhysicalPositionArea(aPosArea, aCBWM, aPositionedWM); 685 *aOutResolvedArea = posArea; 686 687 nscoord right = ltrEdges[3]; 688 if (posArea.first == StylePositionAreaKeyword::Left) { 689 right = ltrEdges[1]; 690 } else if (posArea.first == StylePositionAreaKeyword::SpanLeft) { 691 right = ltrEdges[2]; 692 } else if (posArea.first == StylePositionAreaKeyword::Center) { 693 res.x = ltrEdges[1]; 694 right = ltrEdges[2]; 695 } else if (posArea.first == StylePositionAreaKeyword::SpanRight) { 696 res.x = ltrEdges[1]; 697 } else if (posArea.first == StylePositionAreaKeyword::Right) { 698 res.x = ltrEdges[2]; 699 } else if (posArea.first == StylePositionAreaKeyword::SpanAll) { 700 // no adjustment 701 } else { 702 MOZ_ASSERT_UNREACHABLE("Bad value from ToPhysicalPositionArea"); 703 } 704 res.width = right - res.x; 705 706 nscoord bottom = ttbEdges[3]; 707 if (posArea.second == StylePositionAreaKeyword::Top) { 708 bottom = ttbEdges[1]; 709 } else if (posArea.second == StylePositionAreaKeyword::SpanTop) { 710 bottom = ttbEdges[2]; 711 } else if (posArea.second == StylePositionAreaKeyword::Center) { 712 res.y = ttbEdges[1]; 713 bottom = ttbEdges[2]; 714 } else if (posArea.second == StylePositionAreaKeyword::SpanBottom) { 715 res.y = ttbEdges[1]; 716 } else if (posArea.second == StylePositionAreaKeyword::Bottom) { 717 res.y = ttbEdges[2]; 718 } else if (posArea.second == StylePositionAreaKeyword::SpanAll) { 719 // no adjustment 720 } else { 721 MOZ_ASSERT_UNREACHABLE("Bad value from ToPhysicalPositionArea"); 722 } 723 res.height = bottom - res.y; 724 725 return res; 726 } 727 728 AnchorPositioningUtils::NearestScrollFrameInfo 729 AnchorPositioningUtils::GetNearestScrollFrame(const nsIFrame* aFrame) { 730 if (!aFrame) { 731 return {nullptr, {}}; 732 } 733 uint32_t distance = 1; 734 // `GetNearestScrollContainerFrame` will return the incoming frame if it's a 735 // scroll frame, so nudge to parent. 736 for (const nsIFrame* f = aFrame->GetParent(); f; f = f->GetParent()) { 737 if (f->IsScrollContainerOrSubclass()) { 738 return {f, DistanceToNearestScrollContainer{distance}}; 739 } 740 distance++; 741 } 742 return {nullptr, {}}; 743 } 744 745 nsPoint AnchorPositioningUtils::GetScrollOffsetFor( 746 PhysicalAxes aAxes, const nsIFrame* aPositioned, 747 const AnchorPosDefaultAnchorCache& aDefaultAnchorCache) { 748 MOZ_ASSERT(aPositioned); 749 if (!aDefaultAnchorCache.mAnchor || aAxes.isEmpty()) { 750 return nsPoint{}; 751 } 752 nsPoint offset; 753 const bool trackHorizontal = aAxes.contains(PhysicalAxis::Horizontal); 754 const bool trackVertical = aAxes.contains(PhysicalAxis::Vertical); 755 // TODO(dshin, bug 1991489): Traverse properly, in case anchor and positioned 756 // elements are in different continuation frames of the absolute containing 757 // block. 758 const auto* absoluteContainingBlock = aPositioned->GetParent(); 759 if (GetNearestScrollFrame(aPositioned).mScrollContainer == 760 aDefaultAnchorCache.mScrollContainer) { 761 // Would scroll together anyway, skip. 762 return nsPoint{}; 763 } 764 // Grab the accumulated offset up to, but not including, the abspos 765 // container. 766 for (const auto* f = aDefaultAnchorCache.mScrollContainer; 767 f && f != absoluteContainingBlock; f = f->GetParent()) { 768 if (const ScrollContainerFrame* scrollFrame = do_QueryFrame(f)) { 769 const auto o = scrollFrame->GetScrollPosition(); 770 if (trackHorizontal) { 771 offset.x += o.x; 772 } 773 if (trackVertical) { 774 offset.y += o.y; 775 } 776 } 777 } 778 return offset; 779 } 780 781 // Out of line to avoid having to include AnchorPosReferenceData from nsIFrame.h 782 void DeleteAnchorPosReferenceData(AnchorPosReferenceData* aData) { 783 delete aData; 784 } 785 786 void DeleteLastSuccessfulPositionData(LastSuccessfulPositionData* aData) { 787 delete aData; 788 } 789 790 const nsAtom* AnchorPositioningUtils::GetUsedAnchorName( 791 const nsIFrame* aPositioned, const nsAtom* aAnchorName) { 792 if (aAnchorName && !aAnchorName->IsEmpty()) { 793 return aAnchorName; 794 } 795 796 const auto& defaultAnchor = aPositioned->StylePosition()->mPositionAnchor; 797 if (defaultAnchor.IsNone()) { 798 return nullptr; 799 } 800 801 if (defaultAnchor.IsIdent()) { 802 return defaultAnchor.AsIdent().AsAtom(); 803 } 804 805 if (aPositioned->Style()->IsPseudoElement()) { 806 return nsGkAtoms::AnchorPosImplicitAnchor; 807 } 808 809 if (const nsIContent* content = aPositioned->GetContent()) { 810 if (const auto* element = content->AsElement()) { 811 if (element->GetPopoverData()) { 812 return nsGkAtoms::AnchorPosImplicitAnchor; 813 } 814 } 815 } 816 817 return nullptr; 818 } 819 820 nsIFrame* AnchorPositioningUtils::GetAnchorPosImplicitAnchor( 821 const nsIFrame* aFrame) { 822 const auto* frameContent = aFrame->GetContent(); 823 const bool hasElement = frameContent && frameContent->IsElement(); 824 if (!aFrame->Style()->IsPseudoElement() && !hasElement) { 825 return nullptr; 826 } 827 828 if (MOZ_LIKELY(hasElement)) { 829 const auto* element = frameContent->AsElement(); 830 MOZ_ASSERT(element); 831 const dom::PopoverData* popoverData = element->GetPopoverData(); 832 if (MOZ_UNLIKELY(popoverData)) { 833 if (const RefPtr<dom::Element>& invoker = popoverData->GetInvoker()) { 834 return invoker->GetPrimaryFrame(); 835 } 836 } 837 } 838 839 const auto* pseudoRoot = aFrame->GetClosestNativeAnonymousSubtreeRoot(); 840 if (!pseudoRoot) { 841 return nullptr; 842 } 843 844 auto* pseudoRootFrame = pseudoRoot->GetPrimaryFrame(); 845 if (!pseudoRootFrame) { 846 return nullptr; 847 } 848 849 return pseudoRootFrame->HasAnyStateBits(NS_FRAME_OUT_OF_FLOW) 850 ? pseudoRootFrame->GetPlaceholderFrame()->GetParent() 851 : pseudoRootFrame->GetParent(); 852 } 853 854 AnchorPositioningUtils::ContainingBlockInfo 855 AnchorPositioningUtils::ContainingBlockInfo::ExplicitCBFrameSize( 856 const nsRect& aContainingBlockRect) { 857 // TODO(dshin, bug 1989292): Ideally, this takes both local containing rect + 858 // scrollable containing rect, and one is picked here. 859 return ContainingBlockInfo{aContainingBlockRect}; 860 } 861 862 AnchorPositioningUtils::ContainingBlockInfo 863 AnchorPositioningUtils::ContainingBlockInfo::UseCBFrameSize( 864 const nsIFrame* aPositioned) { 865 // TODO(dshin, bug 1989292): This just gets local containing block. 866 const auto* cb = aPositioned->GetParent(); 867 MOZ_ASSERT(cb); 868 if (IsScrolled(cb)) { 869 cb = aPositioned->GetParent(); 870 } 871 return ContainingBlockInfo{cb->GetPaddingRectRelativeToSelf()}; 872 } 873 874 bool AnchorPositioningUtils::FitsInContainingBlock( 875 const nsIFrame* aPositioned, const AnchorPosReferenceData& aReferenceData) { 876 MOZ_ASSERT(aPositioned->GetProperty(nsIFrame::AnchorPosReferences()) == 877 &aReferenceData); 878 879 const auto& scrollShift = aReferenceData.mDefaultScrollShift; 880 const auto scrollCompensatedSides = aReferenceData.mScrollCompensatedSides; 881 nsSize checkSize = [&]() { 882 const auto& adjustedCB = aReferenceData.mAdjustedContainingBlock; 883 if (scrollShift == nsPoint{} || scrollCompensatedSides == SideBits::eNone) { 884 return adjustedCB.Size(); 885 } 886 887 // We now know that this frame's anchor has moved in relation to 888 // the original containing block, and that at least one side of our 889 // IMCB is attached to it. 890 891 // Scroll shift the adjusted containing block. 892 const auto shifted = aReferenceData.mAdjustedContainingBlock - scrollShift; 893 const auto& originalCB = aReferenceData.mOriginalContainingBlockRect; 894 895 // Now, move edges that are not attached to the anchors and pin it 896 // to the original containing block. 897 const nsPoint pt{ 898 scrollCompensatedSides & SideBits::eLeft ? shifted.X() : originalCB.X(), 899 scrollCompensatedSides & SideBits::eTop ? shifted.Y() : originalCB.Y()}; 900 const nsPoint ptMost{ 901 scrollCompensatedSides & SideBits::eRight ? shifted.XMost() 902 : originalCB.XMost(), 903 scrollCompensatedSides & SideBits::eBottom ? shifted.YMost() 904 : originalCB.YMost()}; 905 906 return nsSize{ptMost.x - pt.x, ptMost.y - pt.y}; 907 }(); 908 909 // Finally, reduce by inset. 910 checkSize -= nsSize{aReferenceData.mInsets.LeftRight(), 911 aReferenceData.mInsets.TopBottom()}; 912 913 return aPositioned->GetMarginRectRelativeToSelf().Size() <= checkSize; 914 } 915 916 nsIFrame* AnchorPositioningUtils::GetAnchorThatFrameScrollsWith( 917 nsIFrame* aFrame, nsDisplayListBuilder* aBuilder, 918 bool aSkipAsserts /* = false */) { 919 #ifdef DEBUG 920 if (!aSkipAsserts) { 921 MOZ_ASSERT(!aBuilder || aBuilder->IsPaintingToWindow()); 922 MOZ_ASSERT_IF(!aBuilder, aFrame->PresContext()->LayoutPhaseCount( 923 nsLayoutPhase::DisplayListBuilding) == 0); 924 } 925 #endif 926 927 if (!StaticPrefs::apz_async_scroll_css_anchor_pos_AtStartup()) { 928 return nullptr; 929 } 930 PhysicalAxes axes = aFrame->GetAnchorPosCompensatingForScroll(); 931 if (axes.isEmpty()) { 932 return nullptr; 933 } 934 935 const auto* pos = aFrame->StylePosition(); 936 if (!pos->mPositionAnchor.IsIdent()) { 937 return nullptr; 938 } 939 940 const nsAtom* defaultAnchorName = pos->mPositionAnchor.AsIdent().AsAtom(); 941 nsIFrame* anchor = const_cast<nsIFrame*>( 942 aFrame->PresShell()->GetAnchorPosAnchor(defaultAnchorName, aFrame)); 943 // TODO Bug 1997026 We need to update the anchor finding code so this can't 944 // happen. For now we just detect it and reject it. 945 if (anchor && !nsLayoutUtils::IsProperAncestorFrameConsideringContinuations( 946 aFrame->GetParent(), anchor)) { 947 return nullptr; 948 } 949 if (!aBuilder) { 950 return anchor; 951 } 952 // TODO for now ShouldAsyncScrollWithAnchor will return false if we are 953 // compensating in only one axis and there is a scroll frame between the 954 // anchor and the positioned's containing block that can scroll in the "wrong" 955 // axis so that we don't async scroll in the wrong axis because ASRs/APZ only 956 // support scrolling in both axes. This is not fully spec compliant, bug 957 // 1988034 tracks this. 958 return DisplayPortUtils::ShouldAsyncScrollWithAnchor(aFrame, anchor, aBuilder, 959 axes) 960 ? anchor 961 : nullptr; 962 } 963 964 static bool TriggerFallbackReflow(PresShell* aPresShell, nsIFrame* aPositioned, 965 AnchorPosReferenceData& aReferencedAnchors, 966 bool aEvaluateAllFallbacksIfNeeded) { 967 auto totalFallbacks = 968 aPositioned->StylePosition()->mPositionTryFallbacks._0.Length(); 969 if (!totalFallbacks) { 970 // No fallbacks specified. 971 return false; 972 } 973 974 const bool positionedFitsInCB = AnchorPositioningUtils::FitsInContainingBlock( 975 aPositioned, aReferencedAnchors); 976 if (positionedFitsInCB) { 977 return false; 978 } 979 980 // TODO(bug 1987964): Try to only do this when the scroll offset changes? 981 auto* lastSuccessfulPosition = 982 aPositioned->GetProperty(nsIFrame::LastSuccessfulPositionFallback()); 983 const bool needsRetry = 984 aEvaluateAllFallbacksIfNeeded || 985 (lastSuccessfulPosition && !lastSuccessfulPosition->mTriedAllFallbacks); 986 if (!needsRetry) { 987 return false; 988 } 989 aPresShell->MarkPositionedFrameForReflow(aPositioned); 990 return true; 991 } 992 993 static bool AnchorIsEffectivelyHidden(nsIFrame* aAnchor) { 994 if (!aAnchor->StyleVisibility()->IsVisible()) { 995 return true; 996 } 997 for (auto* anchor = aAnchor; anchor; anchor = anchor->GetParent()) { 998 if (anchor->HasAnyStateBits(NS_FRAME_POSITION_VISIBILITY_HIDDEN)) { 999 return true; 1000 } 1001 } 1002 return false; 1003 } 1004 1005 static bool ComputePositionVisibility( 1006 PresShell* aPresShell, nsIFrame* aPositioned, 1007 AnchorPosReferenceData& aReferencedAnchors) { 1008 auto vis = aPositioned->StylePosition()->mPositionVisibility; 1009 if (vis & StylePositionVisibility::ALWAYS) { 1010 MOZ_ASSERT(vis == StylePositionVisibility::ALWAYS, 1011 "always can't be combined"); 1012 return true; 1013 } 1014 if (vis & StylePositionVisibility::ANCHORS_VALID) { 1015 for (const auto& ref : aReferencedAnchors) { 1016 if (ref.GetData().isNothing()) { 1017 return false; 1018 } 1019 } 1020 } 1021 if (vis & StylePositionVisibility::NO_OVERFLOW) { 1022 const bool positionedFitsInCB = 1023 AnchorPositioningUtils::FitsInContainingBlock(aPositioned, 1024 aReferencedAnchors); 1025 if (!positionedFitsInCB) { 1026 return false; 1027 } 1028 } 1029 if (vis & StylePositionVisibility::ANCHORS_VISIBLE) { 1030 const auto* defaultAnchorName = aReferencedAnchors.mDefaultAnchorName.get(); 1031 if (defaultAnchorName) { 1032 auto* defaultAnchor = 1033 aPresShell->GetAnchorPosAnchor(defaultAnchorName, aPositioned); 1034 if (defaultAnchor && AnchorIsEffectivelyHidden(defaultAnchor)) { 1035 return false; 1036 } 1037 // If both are in the same cb the expectation is that this doesn't apply 1038 // because there are no intervening clips. I think that's broken, see 1039 // https://github.com/w3c/csswg-drafts/issues/13176 1040 if (defaultAnchor && 1041 defaultAnchor->GetParent() != aPositioned->GetParent()) { 1042 auto* intersectionRoot = aPositioned->GetParent(); 1043 nsRect rootRect = intersectionRoot->InkOverflowRectRelativeToSelf(); 1044 if (IsScrolled(intersectionRoot)) { 1045 intersectionRoot = intersectionRoot->GetParent(); 1046 ScrollContainerFrame* sc = do_QueryFrame(intersectionRoot); 1047 rootRect = sc->GetScrollPortRectAccountingForDynamicToolbar(); 1048 } 1049 const auto* doc = aPositioned->PresContext()->Document(); 1050 const nsINode* root = 1051 intersectionRoot->GetContent() 1052 ? static_cast<nsINode*>(intersectionRoot->GetContent()) 1053 : doc; 1054 rootRect = nsLayoutUtils::TransformFrameRectToAncestor( 1055 intersectionRoot, rootRect, 1056 nsLayoutUtils::GetContainingBlockForClientRect(intersectionRoot)); 1057 const auto input = dom::IntersectionInput{ 1058 .mIsImplicitRoot = false, 1059 .mRootNode = root, 1060 .mRootFrame = intersectionRoot, 1061 .mRootRect = rootRect, 1062 .mRootMargin = {}, 1063 .mScrollMargin = {}, 1064 .mRemoteDocumentVisibleRect = {}, 1065 }; 1066 const auto output = 1067 dom::DOMIntersectionObserver::Intersect(input, defaultAnchor); 1068 // NOTE(emilio): It is a bit weird to also check that mIntersectionRect 1069 // is non-empty, see https://github.com/w3c/csswg-drafts/issues/13176. 1070 if (!output.Intersects() || (output.mIntersectionRect->IsEmpty() && 1071 !defaultAnchor->GetRect().IsEmpty())) { 1072 return false; 1073 } 1074 } 1075 } 1076 } 1077 return true; 1078 } 1079 1080 bool AnchorPositioningUtils::TriggerLayoutOnOverflow( 1081 PresShell* aPresShell, bool aEvaluateAllFallbacksIfNeeded) { 1082 bool didLayoutPositionedItems = false; 1083 1084 for (auto* positioned : aPresShell->GetAnchorPosPositioned()) { 1085 AnchorPosReferenceData* referencedAnchors = 1086 positioned->GetProperty(nsIFrame::AnchorPosReferences()); 1087 if (NS_WARN_IF(!referencedAnchors)) { 1088 continue; 1089 } 1090 1091 if (TriggerFallbackReflow(aPresShell, positioned, *referencedAnchors, 1092 aEvaluateAllFallbacksIfNeeded)) { 1093 didLayoutPositionedItems = true; 1094 } 1095 1096 if (didLayoutPositionedItems) { 1097 // We'll come back to evaluate position-visibility later. 1098 continue; 1099 } 1100 const bool shouldBeVisible = 1101 ComputePositionVisibility(aPresShell, positioned, *referencedAnchors); 1102 const bool isVisible = 1103 !positioned->HasAnyStateBits(NS_FRAME_POSITION_VISIBILITY_HIDDEN); 1104 if (shouldBeVisible != isVisible) { 1105 positioned->AddOrRemoveStateBits(NS_FRAME_POSITION_VISIBILITY_HIDDEN, 1106 !shouldBeVisible); 1107 positioned->InvalidateFrameSubtree(); 1108 } 1109 } 1110 return didLayoutPositionedItems; 1111 } 1112 1113 } // namespace mozilla