mirror of
https://github.com/mozilla-firefox/firefox
synced 2026-08-10 11:48:50 +00:00
This will make the main patch simpler. However, some of them do not support `TreeKind::ShadowIncludingDOM` because it's unclear what should return as a parent node of a child of `ShadowRoot`, what should return as a child node of a shadow host. To avoid the developers to use them on wrong assumptions, this patch makes the users use `DOM`, `Flat` or `FlatForSelection` explicitly. Differential Revision: https://phabricator.services.mozilla.com/D281305
1442 lines
55 KiB
C++
1442 lines
55 KiB
C++
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||
|
||
#include "AnchorPositioningUtils.h"
|
||
|
||
#include "DisplayPortUtils.h"
|
||
#include "ScrollContainerFrame.h"
|
||
#include "mozilla/Maybe.h"
|
||
#include "mozilla/OverflowChangedTracker.h"
|
||
#include "mozilla/PresShell.h"
|
||
#include "mozilla/StaticPrefs_apz.h"
|
||
#include "mozilla/dom/DOMIntersectionObserver.h"
|
||
#include "mozilla/dom/Document.h"
|
||
#include "mozilla/dom/Element.h"
|
||
#include "nsCanvasFrame.h"
|
||
#include "nsContainerFrame.h"
|
||
#include "nsDisplayList.h"
|
||
#include "nsIContent.h"
|
||
#include "nsIContentInlines.h"
|
||
#include "nsIFrame.h"
|
||
#include "nsIFrameInlines.h"
|
||
#include "nsINode.h"
|
||
#include "nsLayoutUtils.h"
|
||
#include "nsPlaceholderFrame.h"
|
||
#include "nsStyleStruct.h"
|
||
#include "nsTArray.h"
|
||
|
||
namespace mozilla {
|
||
|
||
namespace {
|
||
|
||
bool IsScrolled(const nsIFrame* aFrame) {
|
||
return aFrame->Style()->GetPseudoType() ==
|
||
PseudoStyleType::MozScrolledContent;
|
||
}
|
||
|
||
bool DoTreeScopedPropertiesOfElementApplyToContent(
|
||
const ScopedNameRef& aAnchorName, const nsIFrame* aReferencingFrame,
|
||
const nsIFrame* aMaybeReferencedFrame) {
|
||
const auto* referencingElement = aReferencingFrame->GetContent()->AsElement();
|
||
|
||
const auto& referencingTreeScope =
|
||
aReferencingFrame->StyleDisplay()->mAnchorName.scope;
|
||
|
||
const auto* referencingShadowRoot =
|
||
AnchorPositioningUtils::GetShadowRootForTreeScope(*referencingElement,
|
||
referencingTreeScope);
|
||
|
||
const auto* maybeReferencedElement =
|
||
aMaybeReferencedFrame->GetContent()->AsElement();
|
||
const auto& maybeReferencedScope = aAnchorName.mTreeScope;
|
||
|
||
const auto* maybeReferencedShadowRoot =
|
||
AnchorPositioningUtils::GetShadowRootForTreeScope(*maybeReferencedElement,
|
||
maybeReferencedScope);
|
||
const auto* currentShadowRoot = maybeReferencedShadowRoot;
|
||
while (currentShadowRoot) {
|
||
if (referencingShadowRoot == currentShadowRoot) {
|
||
return true;
|
||
}
|
||
|
||
const auto* containingHost = currentShadowRoot->GetContainingShadowHost();
|
||
if (!containingHost) {
|
||
break;
|
||
}
|
||
currentShadowRoot = containingHost->GetContainingShadow();
|
||
}
|
||
|
||
// Original maybeReferencedShadowRoot, currentShadowRoot becomes eventually
|
||
// null
|
||
return !referencingShadowRoot && !maybeReferencedShadowRoot;
|
||
}
|
||
|
||
/**
|
||
* Checks for the implementation of `anchor-scope`:
|
||
* https://drafts.csswg.org/css-anchor-position-1/#anchor-scope
|
||
*
|
||
* TODO: Consider caching the ancestors, see bug 1986347
|
||
*/
|
||
bool IsAnchorInScopeForPositionedElement(const ScopedNameRef& aName,
|
||
const nsIFrame* aPossibleAnchorFrame,
|
||
const nsIFrame* aPositionedFrame) {
|
||
// We don't need to look beyond positioned element's containing block.
|
||
const auto* positionedContainingBlockContent =
|
||
aPositionedFrame->GetParent()->GetContent();
|
||
|
||
const auto* positionedElement = aPositionedFrame->GetContent()->AsElement();
|
||
|
||
const auto& positionAnchorScope = aName.mTreeScope;
|
||
|
||
const dom::ShadowRoot* positionAnchorShadowRoot =
|
||
AnchorPositioningUtils::GetShadowRootForTreeScope(*positionedElement,
|
||
positionAnchorScope);
|
||
|
||
auto getAnchorPosNearestScope =
|
||
[&](const nsAtom* aName, const nsIFrame* aFrame,
|
||
const dom::ShadowRoot* aShadowRoot) -> const nsIContent* {
|
||
// We need to traverse the DOM, not the frame tree, since `anchor-scope`
|
||
// may be present on elements with `display: contents` (in which case its
|
||
// frame is in the `::before` list and won't be found by walking the frame
|
||
// tree parent chain).
|
||
for (nsIContent* cp = aFrame->GetContent();
|
||
cp && cp != positionedContainingBlockContent;
|
||
cp = cp->GetFlattenedTreeParentElementForStyle()) {
|
||
const auto* anchorScope = [&]() -> const StyleScopedName* {
|
||
const nsIFrame* f = nsLayoutUtils::GetStyleFrame(cp);
|
||
if (MOZ_LIKELY(f)) {
|
||
return &f->StyleDisplay()->mAnchorScope;
|
||
}
|
||
if (cp->AsElement()->IsDisplayContents()) {
|
||
const auto* style =
|
||
Servo_Element_GetMaybeOutOfDateStyle(cp->AsElement());
|
||
MOZ_ASSERT(style);
|
||
return &style->StyleDisplay()->mAnchorScope;
|
||
}
|
||
return nullptr;
|
||
}();
|
||
|
||
if (!anchorScope || anchorScope->value.IsEmpty()) {
|
||
continue;
|
||
}
|
||
|
||
for (const StyleAtom& ident : anchorScope->value.AsSpan()) {
|
||
if (aName == ident.AsAtom() || ident.AsAtom() == nsGkAtoms::all) {
|
||
const dom::ShadowRoot* shadowRoot =
|
||
Servo_GetShadowRootForScoped(cp->AsElement(), anchorScope->scope);
|
||
if (shadowRoot == aShadowRoot) {
|
||
return cp;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return nullptr;
|
||
};
|
||
|
||
const auto& possibleAnchorName =
|
||
aPossibleAnchorFrame->StyleDisplay()->mAnchorName;
|
||
const dom::ShadowRoot* possibleAnchorShadowRoot =
|
||
AnchorPositioningUtils::GetShadowRootForTreeScope(
|
||
*aPossibleAnchorFrame->GetContent()->AsElement(),
|
||
possibleAnchorName.scope);
|
||
const auto* nearestScopeForAnchor = getAnchorPosNearestScope(
|
||
aName.mName, aPossibleAnchorFrame, possibleAnchorShadowRoot);
|
||
|
||
const auto* nearestScopeForPositioned = getAnchorPosNearestScope(
|
||
aName.mName, aPositionedFrame, positionAnchorShadowRoot);
|
||
if (!nearestScopeForAnchor) {
|
||
// Anchor is not scoped and positioned element also should
|
||
// not be gated by a scope.
|
||
return !nearestScopeForPositioned ||
|
||
aPossibleAnchorFrame->GetContent() == nearestScopeForPositioned;
|
||
}
|
||
|
||
// There may not be any other scopes between the positioned element
|
||
// and the nearest scope of the anchor.
|
||
return nearestScopeForAnchor == nearestScopeForPositioned;
|
||
};
|
||
|
||
bool IsFullyStyleableTreeAbidingOrNotPseudoElement(const nsIFrame* aFrame) {
|
||
if (!aFrame->Style()->IsPseudoElement()) {
|
||
return true;
|
||
}
|
||
|
||
const PseudoStyleType pseudoElementType = aFrame->Style()->GetPseudoType();
|
||
|
||
// See https://www.w3.org/TR/css-pseudo-4/#treelike
|
||
return pseudoElementType == PseudoStyleType::Before ||
|
||
pseudoElementType == PseudoStyleType::After ||
|
||
pseudoElementType == PseudoStyleType::Marker;
|
||
}
|
||
|
||
size_t GetTopLayerIndex(const nsIFrame* aFrame) {
|
||
MOZ_ASSERT(aFrame);
|
||
|
||
const nsIContent* frameContent = aFrame->GetContent();
|
||
|
||
if (!frameContent) {
|
||
return 0;
|
||
}
|
||
|
||
// Within the array returned by Document::GetTopLayer,
|
||
// a higher index means the layer sits higher in the stack,
|
||
// matching Document::GetTopLayerTop()’s top-to-bottom logic.
|
||
// See https://drafts.csswg.org/css-position-4/#in-a-higher-top-layer
|
||
const nsTArray<dom::Element*>& topLayers =
|
||
frameContent->OwnerDoc()->GetTopLayer();
|
||
|
||
for (size_t index = 0; index < topLayers.Length(); ++index) {
|
||
const auto& topLayer = topLayers.ElementAt(index);
|
||
if (nsContentUtils::ContentIsFlattenedTreeDescendantOfForStyle(
|
||
/* aPossibleDescendant */ frameContent,
|
||
/* aPossibleAncestor */ topLayer)) {
|
||
return 1 + index;
|
||
}
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
bool IsInitialContainingBlock(const nsIFrame* aContainingBlock) {
|
||
// Initial containing block: The containing block of the root element.
|
||
// https://drafts.csswg.org/css-display-4/#initial-containing-block
|
||
return aContainingBlock == aContainingBlock->PresShell()
|
||
->FrameConstructor()
|
||
->GetDocElementContainingBlock();
|
||
}
|
||
|
||
bool IsContainingBlockGeneratedByElement(const nsIFrame* aContainingBlock) {
|
||
// 2.1. Containing Blocks of Positioned Boxes
|
||
// https://www.w3.org/TR/css-position-3/#def-cb
|
||
return !(!aContainingBlock || aContainingBlock->IsViewportFrame() ||
|
||
IsInitialContainingBlock(aContainingBlock));
|
||
}
|
||
|
||
bool IsAnchorLaidOutStrictlyBeforeElement(
|
||
const nsIFrame* aPossibleAnchorFrame, const nsIFrame* aPositionedFrame,
|
||
const nsTArray<const nsIFrame*>& aPositionedFrameAncestors) {
|
||
// 1. positioned el is in a higher top layer than possible anchor,
|
||
// see https://drafts.csswg.org/css-position-4/#in-a-higher-top-layer
|
||
const size_t positionedTopLayerIndex = GetTopLayerIndex(aPositionedFrame);
|
||
const size_t anchorTopLayerIndex = GetTopLayerIndex(aPossibleAnchorFrame);
|
||
|
||
if (anchorTopLayerIndex != positionedTopLayerIndex) {
|
||
return anchorTopLayerIndex < positionedTopLayerIndex;
|
||
}
|
||
|
||
// Note: The containing block of an absolutely positioned element
|
||
// is just the parent frame.
|
||
const nsIFrame* positionedContainingBlock = aPositionedFrame->GetParent();
|
||
// Note(dshin, bug 1985654): Spec strictly uses the term "containing block,"
|
||
// corresponding to `GetContainingBlock()`. However, this leads to cases
|
||
// where an anchor's non-inline containing block prevents it from being a
|
||
// valid anchor for a absolutely positioned element (Which can explicitly
|
||
// have inline elements as a containing block). Some WPT rely on inline
|
||
// containing blocks as well.
|
||
// See also: https://github.com/w3c/csswg-drafts/issues/12674
|
||
const nsIFrame* anchorContainingBlock = aPossibleAnchorFrame->GetParent();
|
||
|
||
// 2. Both elements are in the same top layer but have different
|
||
// containing blocks and positioned el's containing block is an
|
||
// ancestor of possible anchor's containing block in the containing
|
||
// block chain, aka one of the following:
|
||
if (nsLayoutUtils::FirstContinuationOrIBSplitSibling(anchorContainingBlock) !=
|
||
nsLayoutUtils::FirstContinuationOrIBSplitSibling(
|
||
positionedContainingBlock)) {
|
||
// 2.1 positioned el's containing block is the viewport, and
|
||
// possible anchor's containing block isn't.
|
||
if (positionedContainingBlock->IsViewportFrame() &&
|
||
!anchorContainingBlock->IsViewportFrame()) {
|
||
return !nsLayoutUtils::IsProperAncestorFrame(aPositionedFrame,
|
||
aPossibleAnchorFrame);
|
||
}
|
||
|
||
auto isLastContainingBlockOrderable =
|
||
[&aPositionedFrame, &aPositionedFrameAncestors, &anchorContainingBlock,
|
||
&positionedContainingBlock]() -> bool {
|
||
const nsIFrame* it = anchorContainingBlock;
|
||
while (it) {
|
||
const nsIFrame* parentContainingBlock = it->GetParent();
|
||
if (!parentContainingBlock) {
|
||
return false;
|
||
}
|
||
|
||
if (nsLayoutUtils::FirstContinuationOrIBSplitSibling(
|
||
parentContainingBlock) ==
|
||
nsLayoutUtils::FirstContinuationOrIBSplitSibling(
|
||
positionedContainingBlock)) {
|
||
return !it->IsAbsolutelyPositioned() ||
|
||
nsLayoutUtils::CompareTreePosition(it, aPositionedFrame,
|
||
aPositionedFrameAncestors,
|
||
nullptr) < 0;
|
||
}
|
||
|
||
it = parentContainingBlock;
|
||
}
|
||
|
||
return false;
|
||
};
|
||
|
||
// 2.2 positioned el's containing block is the initial containing
|
||
// block, and possible anchor's containing block is generated by an
|
||
// element, and the last containing block in possible anchor's containing
|
||
// block chain before reaching positioned el's containing block is either
|
||
// not absolutely positioned or precedes positioned el in the tree order,
|
||
const bool isAnchorContainingBlockGenerated =
|
||
IsContainingBlockGeneratedByElement(anchorContainingBlock);
|
||
if (isAnchorContainingBlockGenerated &&
|
||
IsInitialContainingBlock(positionedContainingBlock)) {
|
||
return isLastContainingBlockOrderable();
|
||
}
|
||
|
||
// 2.3 both elements' containing blocks are generated by elements,
|
||
// and positioned el's containing block is an ancestor in the flat
|
||
// tree to that of possible anchor's containing block, and the last
|
||
// containing block in possible anchor’s containing block chain before
|
||
// reaching positioned el’s containing block is either not absolutely
|
||
// positioned or precedes positioned el in the tree order.
|
||
if (isAnchorContainingBlockGenerated &&
|
||
IsContainingBlockGeneratedByElement(positionedContainingBlock)) {
|
||
return isLastContainingBlockOrderable();
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
// 3. Both elements are in the same top layer and have the same
|
||
// containing block, and are both absolutely positioned, and possible
|
||
// anchor is earlier in flat tree order than positioned el.
|
||
const bool isAnchorAbsolutelyPositioned =
|
||
aPossibleAnchorFrame->IsAbsolutelyPositioned();
|
||
if (isAnchorAbsolutelyPositioned) {
|
||
// We must have checked that the positioned element is absolutely
|
||
// positioned by now.
|
||
return nsLayoutUtils::CompareTreePosition(
|
||
aPossibleAnchorFrame, aPositionedFrame,
|
||
aPositionedFrameAncestors, nullptr) < 0;
|
||
}
|
||
|
||
// 4. Both elements are in the same top layer and have the same
|
||
// containing block, but possible anchor isn't absolutely positioned.
|
||
return !isAnchorAbsolutelyPositioned;
|
||
}
|
||
|
||
/**
|
||
* https://drafts.csswg.org/css-contain-2/#skips-its-contents
|
||
*/
|
||
bool IsPositionedElementAlsoSkippedWhenAnchorIsSkipped(
|
||
const nsIFrame* aPossibleAnchorFrame, const nsIFrame* aPositionedFrame) {
|
||
// If potential anchor is skipped and a root of a visibility subtree,
|
||
// it can never be acceptable.
|
||
if (aPossibleAnchorFrame->HidesContentForLayout()) {
|
||
return false;
|
||
}
|
||
|
||
// If possible anchor is in the skipped contents of another element,
|
||
// then positioned el shall be in the skipped contents of that same element.
|
||
const nsIFrame* visibilityAncestor = aPossibleAnchorFrame->GetParent();
|
||
while (visibilityAncestor) {
|
||
// If anchor is skipped via auto or hidden, it cannot be acceptable,
|
||
// be it a root or a non-root of a visibility subtree.
|
||
if (visibilityAncestor->HidesContentForLayout()) {
|
||
break;
|
||
}
|
||
|
||
visibilityAncestor = visibilityAncestor->GetParent();
|
||
}
|
||
|
||
// If positioned el is skipped and a root of a visibility subtree,
|
||
// an anchor can never be acceptable.
|
||
if (aPositionedFrame->HidesContentForLayout()) {
|
||
return false;
|
||
}
|
||
|
||
const nsIFrame* ancestor = aPositionedFrame;
|
||
while (ancestor) {
|
||
if (ancestor->HidesContentForLayout()) {
|
||
return ancestor == visibilityAncestor;
|
||
}
|
||
|
||
ancestor = ancestor->GetParent();
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
class LazyAncestorHolder {
|
||
const nsIFrame* mFrame;
|
||
AutoTArray<const nsIFrame*, 8> mAncestors;
|
||
bool mFilled = false;
|
||
|
||
public:
|
||
const nsTArray<const nsIFrame*>& GetAncestors() {
|
||
if (!mFilled) {
|
||
nsLayoutUtils::FillAncestors(mFrame, nullptr, &mAncestors);
|
||
mFilled = true;
|
||
}
|
||
return mAncestors;
|
||
}
|
||
|
||
explicit LazyAncestorHolder(const nsIFrame* aFrame) : mFrame(aFrame) {}
|
||
};
|
||
|
||
bool IsAcceptableAnchorElement(
|
||
const nsIFrame* aPossibleAnchorFrame, const ScopedNameRef* aName,
|
||
const nsIFrame* aPositionedFrame,
|
||
LazyAncestorHolder& aPositionedFrameAncestorHolder) {
|
||
MOZ_ASSERT(aPossibleAnchorFrame);
|
||
MOZ_ASSERT(aPositionedFrame);
|
||
|
||
// An element possible anchor is an acceptable anchor element for an
|
||
// absolutely positioned element positioned el if all of the following are
|
||
// true:
|
||
// - possible anchor is either an element or a fully styleable
|
||
// tree-abiding pseudo-element.
|
||
// - possible anchor is in scope for positioned el, per the effects of
|
||
// anchor-scope on positioned el or its ancestors.
|
||
// - possible anchor is laid out strictly before positioned el
|
||
//
|
||
// Note: Frames having an anchor name contain elements.
|
||
// The phrase "element or a fully styleable tree-abiding pseudo-element"
|
||
// used by the spec is taken to mean
|
||
// "either not a pseudo-element or a pseudo-element of a specific kind".
|
||
if (!IsFullyStyleableTreeAbidingOrNotPseudoElement(aPossibleAnchorFrame)) {
|
||
return false;
|
||
}
|
||
if (!IsAnchorLaidOutStrictlyBeforeElement(
|
||
aPossibleAnchorFrame, aPositionedFrame,
|
||
aPositionedFrameAncestorHolder.GetAncestors())) {
|
||
return false;
|
||
}
|
||
if (aName && !IsAnchorInScopeForPositionedElement(
|
||
*aName, aPossibleAnchorFrame, aPositionedFrame)) {
|
||
return false;
|
||
}
|
||
if (!IsPositionedElementAlsoSkippedWhenAnchorIsSkipped(aPossibleAnchorFrame,
|
||
aPositionedFrame)) {
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
} // namespace
|
||
|
||
AnchorPosReferenceData::Result AnchorPosReferenceData::InsertOrModify(
|
||
const ScopedNameRef& aKey, const bool aNeedOffset) {
|
||
MOZ_ASSERT(aKey.mName);
|
||
bool exists = true;
|
||
auto* result = &mMap.LookupOrInsertWith(aKey, [&exists]() {
|
||
exists = false;
|
||
return Nothing{};
|
||
});
|
||
|
||
if (!exists) {
|
||
return {false, result};
|
||
}
|
||
|
||
// We tried to resolve before.
|
||
if (result->isNothing()) {
|
||
// We know this reference is invalid.
|
||
return {true, result};
|
||
}
|
||
// Previous resolution found a valid anchor.
|
||
if (!aNeedOffset) {
|
||
// Size is guaranteed to be populated on resolution.
|
||
return {true, result};
|
||
}
|
||
|
||
// Previous resolution may have been for size only, in which case another
|
||
// anchor resolution is still required.
|
||
return {result->ref().mOffsetData.isSome(), result};
|
||
}
|
||
|
||
const AnchorPosReferenceData::Value* AnchorPosReferenceData::Lookup(
|
||
const ScopedNameRef& aKey) const {
|
||
return mMap.Lookup(aKey).DataPtrOrNull();
|
||
}
|
||
|
||
AnchorPosDefaultAnchorCache::AnchorPosDefaultAnchorCache(
|
||
const nsIFrame* aAnchor, const nsIFrame* aScrollContainer)
|
||
: mAnchor{aAnchor}, mScrollContainer{aScrollContainer} {
|
||
MOZ_ASSERT_IF(
|
||
aAnchor,
|
||
nsLayoutUtils::GetNearestScrollContainerFrame(
|
||
const_cast<nsContainerFrame*>(aAnchor->GetParent()),
|
||
nsLayoutUtils::SCROLLABLE_SAME_DOC |
|
||
nsLayoutUtils::SCROLLABLE_INCLUDE_HIDDEN) == mScrollContainer);
|
||
}
|
||
|
||
nsIFrame* AnchorPositioningUtils::FindFirstAcceptableAnchor(
|
||
const ScopedNameRef& aName, const nsIFrame* aPositionedFrame,
|
||
const nsTArray<nsIFrame*>& aPossibleAnchorFrames) {
|
||
LazyAncestorHolder positionedFrameAncestorHolder(aPositionedFrame);
|
||
|
||
for (auto it = aPossibleAnchorFrames.rbegin();
|
||
it != aPossibleAnchorFrames.rend(); ++it) {
|
||
const nsIFrame* possibleAnchorFrame = *it;
|
||
if (!DoTreeScopedPropertiesOfElementApplyToContent(
|
||
aName, possibleAnchorFrame, aPositionedFrame)) {
|
||
// Skip anchors in different shadow trees.
|
||
continue;
|
||
}
|
||
|
||
// Check if the possible anchor is an acceptable anchor element.
|
||
if (IsAcceptableAnchorElement(*it, &aName, aPositionedFrame,
|
||
positionedFrameAncestorHolder)) {
|
||
return *it;
|
||
}
|
||
}
|
||
|
||
// If we reach here, we didn't find any acceptable anchor.
|
||
return nullptr;
|
||
}
|
||
|
||
static const nsIFrame* GetAnchorOf(const nsIFrame* aPositioned,
|
||
const ScopedNameRef& aAnchorName) {
|
||
const auto* presShell = aPositioned->PresShell();
|
||
MOZ_ASSERT(presShell, "No PresShell for frame?");
|
||
return presShell->GetAnchorPosAnchor(aAnchorName, aPositioned);
|
||
}
|
||
|
||
Maybe<nsRect> AnchorPositioningUtils::GetAnchorPosRect(
|
||
const nsIFrame* aAbsoluteContainingBlock, const nsIFrame* aAnchor,
|
||
bool aCBRectIsValid) {
|
||
auto rect = [&]() -> Maybe<nsRect> {
|
||
if (aCBRectIsValid) {
|
||
return Some(ReassembleAnchorRect(aAnchor, aAbsoluteContainingBlock));
|
||
}
|
||
|
||
// Ok, containing block doesn't have its rect fully resolved. Compute the
|
||
// anchor rect in aAbsoluteContainingBlock's coordinate space.
|
||
// TODO(dshin): This wouldn't handle anchor in a previous top layer.
|
||
if (!nsLayoutUtils::IsProperAncestorFrameConsideringContinuations(
|
||
aAbsoluteContainingBlock, aAnchor)) {
|
||
return Nothing{};
|
||
}
|
||
return Some(
|
||
nsLayoutUtils::GetCombinedFragmentRects(aAnchor).mRect +
|
||
aAnchor->GetOffsetToIgnoringScrolling(aAbsoluteContainingBlock));
|
||
}();
|
||
return rect.map([&](const nsRect& aRect) {
|
||
// We need to position the border box of the anchor within the abspos
|
||
// containing block's size - So the rectangle's size (i.e. Anchor size)
|
||
// stays the same, while "the outer rectangle" (i.e. The abspos cb size)
|
||
// "shrinks" by shifting the position.
|
||
const auto border = aAbsoluteContainingBlock->GetUsedBorder();
|
||
const nsPoint borderTopLeft{border.left, border.top};
|
||
const auto rect = aRect - borderTopLeft;
|
||
return rect;
|
||
});
|
||
}
|
||
|
||
Maybe<AnchorPosInfo> AnchorPositioningUtils::ResolveAnchorPosRect(
|
||
const nsIFrame* aPositioned, const nsIFrame* aAbsoluteContainingBlock,
|
||
const ScopedNameRef& aAnchorName, bool aCBRectIsValid,
|
||
AnchorPosResolutionCache* aResolutionCache) {
|
||
if (!aPositioned) {
|
||
return Nothing{};
|
||
}
|
||
|
||
if (!aPositioned->HasAnyStateBits(NS_FRAME_OUT_OF_FLOW)) {
|
||
return Nothing{};
|
||
}
|
||
|
||
MOZ_ASSERT(aPositioned->GetParent() == aAbsoluteContainingBlock);
|
||
|
||
const auto anchorName = GetUsedAnchorName(aPositioned, aAnchorName);
|
||
if (!anchorName) {
|
||
return Nothing{};
|
||
}
|
||
|
||
Maybe<AnchorPosResolutionData>* entry = nullptr;
|
||
if (aResolutionCache) {
|
||
const auto result =
|
||
aResolutionCache->mReferenceData->InsertOrModify(*anchorName, true);
|
||
if (result.mAlreadyResolved) {
|
||
MOZ_ASSERT(result.mEntry, "Entry exists but null?");
|
||
return result.mEntry->map([&](const AnchorPosResolutionData& aData) {
|
||
MOZ_ASSERT(aData.mOffsetData, "Missing anchor offset resolution.");
|
||
const auto& offsetData = aData.mOffsetData.ref();
|
||
return AnchorPosInfo{nsRect{offsetData.mOrigin, aData.mSize},
|
||
offsetData.mCompensatesForScroll};
|
||
});
|
||
}
|
||
entry = result.mEntry;
|
||
}
|
||
|
||
const auto* anchor = GetAnchorOf(aPositioned, *anchorName);
|
||
if (!anchor) {
|
||
// If we have a cached entry, just check that it resolved to nothing last
|
||
// time as well.
|
||
MOZ_ASSERT_IF(entry, entry->isNothing());
|
||
return Nothing{};
|
||
}
|
||
|
||
const auto result =
|
||
GetAnchorPosRect(aAbsoluteContainingBlock, anchor, aCBRectIsValid);
|
||
return result.map([&](const nsRect& aRect) {
|
||
bool compensatesForScroll = false;
|
||
DistanceToNearestScrollContainer distanceToNearestScrollContainer;
|
||
if (aResolutionCache) {
|
||
MOZ_ASSERT(entry);
|
||
// Update the cache.
|
||
compensatesForScroll = [&]() {
|
||
auto& defaultAnchorCache = aResolutionCache->mDefaultAnchorCache;
|
||
if (!aAnchorName.mName) {
|
||
// Explicitly resolved default anchor for the first time - populate
|
||
// the cache.
|
||
defaultAnchorCache.mAnchor = anchor;
|
||
const auto [scrollContainer, distance] =
|
||
AnchorPositioningUtils::GetNearestScrollFrame(anchor);
|
||
distanceToNearestScrollContainer = distance;
|
||
defaultAnchorCache.mScrollContainer = scrollContainer;
|
||
aResolutionCache->mReferenceData->mDistanceToDefaultScrollContainer =
|
||
distance;
|
||
aResolutionCache->mReferenceData->mDefaultAnchorName =
|
||
anchorName->mName;
|
||
aResolutionCache->mReferenceData->mAnchorTreeScope =
|
||
anchorName->mTreeScope;
|
||
// This is the default anchor, so scroll compensated by definition.
|
||
return true;
|
||
}
|
||
if (defaultAnchorCache.mAnchor == anchor) {
|
||
// This is referring to the default anchor, so scroll compensated by
|
||
// definition.
|
||
return true;
|
||
}
|
||
const auto [scrollContainer, distance] =
|
||
AnchorPositioningUtils::GetNearestScrollFrame(anchor);
|
||
distanceToNearestScrollContainer = distance;
|
||
return scrollContainer ==
|
||
aResolutionCache->mDefaultAnchorCache.mScrollContainer;
|
||
}();
|
||
// If a partially resolved entry exists, make sure that it matches what we
|
||
// have now.
|
||
MOZ_ASSERT_IF(*entry, entry->ref().mSize == aRect.Size());
|
||
*entry = Some(AnchorPosResolutionData{
|
||
aRect.Size(),
|
||
Some(AnchorPosOffsetData{aRect.TopLeft(), compensatesForScroll,
|
||
distanceToNearestScrollContainer}),
|
||
aAnchorName.mTreeScope});
|
||
}
|
||
return AnchorPosInfo{aRect, compensatesForScroll};
|
||
});
|
||
}
|
||
|
||
Maybe<nsSize> AnchorPositioningUtils::ResolveAnchorPosSize(
|
||
const nsIFrame* aPositioned, const ScopedNameRef& aAnchorName,
|
||
AnchorPosResolutionCache* aResolutionCache) {
|
||
auto anchorName = GetUsedAnchorName(aPositioned, aAnchorName);
|
||
if (!anchorName) {
|
||
return Nothing{};
|
||
}
|
||
Maybe<AnchorPosResolutionData>* entry = nullptr;
|
||
auto* referencedAnchors =
|
||
aResolutionCache ? aResolutionCache->mReferenceData : nullptr;
|
||
if (referencedAnchors) {
|
||
const auto result = referencedAnchors->InsertOrModify(*anchorName, false);
|
||
if (result.mAlreadyResolved) {
|
||
MOZ_ASSERT(result.mEntry, "Entry exists but null?");
|
||
return result.mEntry->map(
|
||
[](const AnchorPosResolutionData& aData) { return aData.mSize; });
|
||
}
|
||
entry = result.mEntry;
|
||
}
|
||
const auto* anchor = GetAnchorOf(aPositioned, *anchorName);
|
||
if (!anchor) {
|
||
return Nothing{};
|
||
}
|
||
const auto size =
|
||
nsLayoutUtils::GetCombinedFragmentRects(anchor).mRect.Size();
|
||
if (entry) {
|
||
*entry =
|
||
Some(AnchorPosResolutionData{size, Nothing{}, aAnchorName.mTreeScope});
|
||
}
|
||
return Some(size);
|
||
}
|
||
|
||
/**
|
||
* Returns an equivalent StylePositionArea that contains:
|
||
* [
|
||
* [ left | center | right | span-left | span-right | span-all]
|
||
* [ top | center | bottom | span-top | span-bottom | span-all]
|
||
* ]
|
||
*/
|
||
static StylePositionArea ToPhysicalPositionArea(StylePositionArea aPosArea,
|
||
WritingMode aCbWM,
|
||
WritingMode aPosWM) {
|
||
StyleWritingMode cbwm{aCbWM.GetBits()};
|
||
StyleWritingMode wm{aPosWM.GetBits()};
|
||
Servo_PhysicalizePositionArea(&aPosArea, &cbwm, &wm);
|
||
return aPosArea;
|
||
}
|
||
|
||
StylePositionArea AnchorPositioningUtils::PhysicalizePositionArea(
|
||
StylePositionArea aPosArea, const nsIFrame* aPositioned) {
|
||
return ToPhysicalPositionArea(aPosArea,
|
||
aPositioned->GetParent()->GetWritingMode(),
|
||
aPositioned->GetWritingMode());
|
||
}
|
||
|
||
nsRect AnchorPositioningUtils::AdjustAbsoluteContainingBlockRectForPositionArea(
|
||
const nsRect& aAnchorRect, const nsRect& aCBRect, WritingMode aPositionedWM,
|
||
WritingMode aCBWM, const StylePositionArea& aPosArea,
|
||
StylePositionArea* aOutResolvedArea) {
|
||
// Get the boundaries of 3x3 grid in CB's frame space. The edges of the
|
||
// default anchor box are clamped to the bounds of the CB, even if that
|
||
// results in zero width/height cells.
|
||
//
|
||
// ltrEdges[0] ltrEdges[1] ltrEdges[2] ltrEdges[3]
|
||
// | | | |
|
||
// ttbEdges[0] +------------+------------+------------+
|
||
// | | | |
|
||
// ttbEdges[1] +------------+------------+------------+
|
||
// | | | |
|
||
// ttbEdges[2] +------------+------------+------------+
|
||
// | | | |
|
||
// ttbEdges[3] +------------+------------+------------+
|
||
|
||
const nsRect gridRect = aCBRect.Union(aAnchorRect);
|
||
nscoord ltrEdges[4] = {gridRect.x, aAnchorRect.x,
|
||
aAnchorRect.x + aAnchorRect.width,
|
||
gridRect.x + gridRect.width};
|
||
nscoord ttbEdges[4] = {gridRect.y, aAnchorRect.y,
|
||
aAnchorRect.y + aAnchorRect.height,
|
||
gridRect.y + gridRect.height};
|
||
ltrEdges[1] = std::clamp(ltrEdges[1], ltrEdges[0], ltrEdges[3]);
|
||
ltrEdges[2] = std::clamp(ltrEdges[2], ltrEdges[0], ltrEdges[3]);
|
||
ttbEdges[1] = std::clamp(ttbEdges[1], ttbEdges[0], ttbEdges[3]);
|
||
ttbEdges[2] = std::clamp(ttbEdges[2], ttbEdges[0], ttbEdges[3]);
|
||
|
||
nsRect res = gridRect;
|
||
|
||
// PositionArea, resolved to only contain Left/Right/Top/Bottom values.
|
||
StylePositionArea posArea =
|
||
ToPhysicalPositionArea(aPosArea, aCBWM, aPositionedWM);
|
||
*aOutResolvedArea = posArea;
|
||
|
||
nscoord right = ltrEdges[3];
|
||
if (posArea.first == StylePositionAreaKeyword::Left) {
|
||
right = ltrEdges[1];
|
||
} else if (posArea.first == StylePositionAreaKeyword::SpanLeft) {
|
||
right = ltrEdges[2];
|
||
} else if (posArea.first == StylePositionAreaKeyword::Center) {
|
||
res.x = ltrEdges[1];
|
||
right = ltrEdges[2];
|
||
} else if (posArea.first == StylePositionAreaKeyword::SpanRight) {
|
||
res.x = ltrEdges[1];
|
||
} else if (posArea.first == StylePositionAreaKeyword::Right) {
|
||
res.x = ltrEdges[2];
|
||
} else if (posArea.first == StylePositionAreaKeyword::SpanAll) {
|
||
// no adjustment
|
||
} else {
|
||
MOZ_ASSERT_UNREACHABLE("Bad value from ToPhysicalPositionArea");
|
||
}
|
||
res.width = right - res.x;
|
||
|
||
nscoord bottom = ttbEdges[3];
|
||
if (posArea.second == StylePositionAreaKeyword::Top) {
|
||
bottom = ttbEdges[1];
|
||
} else if (posArea.second == StylePositionAreaKeyword::SpanTop) {
|
||
bottom = ttbEdges[2];
|
||
} else if (posArea.second == StylePositionAreaKeyword::Center) {
|
||
res.y = ttbEdges[1];
|
||
bottom = ttbEdges[2];
|
||
} else if (posArea.second == StylePositionAreaKeyword::SpanBottom) {
|
||
res.y = ttbEdges[1];
|
||
} else if (posArea.second == StylePositionAreaKeyword::Bottom) {
|
||
res.y = ttbEdges[2];
|
||
} else if (posArea.second == StylePositionAreaKeyword::SpanAll) {
|
||
// no adjustment
|
||
} else {
|
||
MOZ_ASSERT_UNREACHABLE("Bad value from ToPhysicalPositionArea");
|
||
}
|
||
res.height = bottom - res.y;
|
||
|
||
return res;
|
||
}
|
||
|
||
AnchorPositioningUtils::NearestScrollFrameInfo
|
||
AnchorPositioningUtils::GetNearestScrollFrame(const nsIFrame* aFrame) {
|
||
if (!aFrame) {
|
||
return {nullptr, {}};
|
||
}
|
||
uint32_t distance = 1;
|
||
// `GetNearestScrollContainerFrame` will return the incoming frame if it's a
|
||
// scroll frame, so nudge to parent.
|
||
for (const nsIFrame* f = aFrame->GetParent(); f; f = f->GetParent()) {
|
||
if (f->IsScrollContainerOrSubclass()) {
|
||
return {f, DistanceToNearestScrollContainer{distance}};
|
||
}
|
||
distance++;
|
||
}
|
||
return {nullptr, {}};
|
||
}
|
||
|
||
nsPoint AnchorPositioningUtils::GetScrollOffsetFor(
|
||
PhysicalAxes aAxes, const nsIFrame* aPositioned,
|
||
const AnchorPosDefaultAnchorCache& aDefaultAnchorCache) {
|
||
MOZ_ASSERT(aPositioned);
|
||
if (!aDefaultAnchorCache.mAnchor || aAxes.isEmpty()) {
|
||
return nsPoint{};
|
||
}
|
||
nsPoint offset;
|
||
const bool trackHorizontal = aAxes.contains(PhysicalAxis::Horizontal);
|
||
const bool trackVertical = aAxes.contains(PhysicalAxis::Vertical);
|
||
// TODO(dshin, bug 1991489): Traverse properly, in case anchor and positioned
|
||
// elements are in different continuation frames of the absolute containing
|
||
// block.
|
||
const auto* absoluteContainingBlock = aPositioned->GetParent();
|
||
if (GetNearestScrollFrame(aPositioned).mScrollContainer ==
|
||
aDefaultAnchorCache.mScrollContainer) {
|
||
// Would scroll together anyway, skip.
|
||
return nsPoint{};
|
||
}
|
||
// Grab the accumulated offset up to, but not including, the abspos
|
||
// container.
|
||
for (const auto* f = aDefaultAnchorCache.mScrollContainer;
|
||
f && f != absoluteContainingBlock; f = f->GetParent()) {
|
||
if (const ScrollContainerFrame* scrollFrame = do_QueryFrame(f)) {
|
||
const auto o = scrollFrame->GetScrollPosition();
|
||
if (trackHorizontal) {
|
||
offset.x += o.x;
|
||
}
|
||
if (trackVertical) {
|
||
offset.y += o.y;
|
||
}
|
||
}
|
||
}
|
||
return offset;
|
||
}
|
||
|
||
// Out of line to avoid having to include AnchorPosReferenceData from nsIFrame.h
|
||
void DeleteAnchorPosReferenceData(AnchorPosReferenceData* aData) {
|
||
delete aData;
|
||
}
|
||
|
||
void DeleteLastSuccessfulPositionData(LastSuccessfulPositionData* aData) {
|
||
delete aData;
|
||
}
|
||
|
||
Maybe<ScopedNameRef> AnchorPositioningUtils::GetUsedAnchorName(
|
||
const nsIFrame* aPositioned, const ScopedNameRef& aAnchorName) {
|
||
if (aAnchorName.mName && !aAnchorName.mName->IsEmpty()) {
|
||
return Some(aAnchorName);
|
||
}
|
||
|
||
const auto* stylePosition = aPositioned->StylePosition();
|
||
if (!stylePosition->CanHaveDefaultAnchor()) {
|
||
return Nothing{};
|
||
}
|
||
|
||
const auto& defaultAnchor = stylePosition->mPositionAnchor;
|
||
if (defaultAnchor.value.IsIdent()) {
|
||
return Some(ScopedNameRef(defaultAnchor.value.AsIdent().AsAtom(),
|
||
defaultAnchor.scope));
|
||
}
|
||
|
||
MOZ_ASSERT(defaultAnchor.value.IsNormal() || defaultAnchor.value.IsAuto());
|
||
|
||
if (aPositioned->Style()->IsPseudoElement()) {
|
||
return Some(ScopedNameRef(nsGkAtoms::AnchorPosImplicitAnchor,
|
||
StyleCascadeLevel::Default()));
|
||
}
|
||
|
||
if (const nsIContent* content = aPositioned->GetContent()) {
|
||
if (const auto* element = nsGenericHTMLElement::FromNode(content)) {
|
||
if (element->GetPopoverAttributeState() !=
|
||
dom::PopoverAttributeState::None) {
|
||
return Some(ScopedNameRef(nsGkAtoms::AnchorPosImplicitAnchor,
|
||
StyleCascadeLevel::Default()));
|
||
}
|
||
}
|
||
}
|
||
|
||
return Nothing{};
|
||
}
|
||
|
||
static std::pair<nsIContent*, AnchorPositioningUtils::ImplicitAnchorKind>
|
||
GetImplicitAnchorContent(const nsIFrame* aFrame) {
|
||
const auto* element = dom::Element::FromNodeOrNull(aFrame->GetContent());
|
||
if (!element) [[unlikely]] {
|
||
return {};
|
||
}
|
||
if (const auto* popoverData = element->GetPopoverData()) [[unlikely]] {
|
||
if (RefPtr invoker = popoverData->GetInvoker()) {
|
||
return {invoker.get(),
|
||
AnchorPositioningUtils::ImplicitAnchorKind::Popover};
|
||
}
|
||
}
|
||
if (!aFrame->Style()->IsPseudoElement()) {
|
||
return {};
|
||
}
|
||
return {element->GetClosestNativeAnonymousSubtreeRootParentOrHost(),
|
||
AnchorPositioningUtils::ImplicitAnchorKind::PseudoElement};
|
||
}
|
||
|
||
auto AnchorPositioningUtils::GetAnchorPosImplicitAnchor(const nsIFrame* aFrame)
|
||
-> ImplicitAnchorResult {
|
||
auto [implicitAnchor, kind] = GetImplicitAnchorContent(aFrame);
|
||
if (!implicitAnchor) {
|
||
return {};
|
||
}
|
||
auto* anchorFrame = implicitAnchor->GetPrimaryFrame();
|
||
if (!anchorFrame) {
|
||
return {};
|
||
}
|
||
LazyAncestorHolder ancestorHolder(aFrame);
|
||
if (!IsAcceptableAnchorElement(anchorFrame, /* aName = */ nullptr, aFrame,
|
||
ancestorHolder)) {
|
||
return {};
|
||
}
|
||
return {anchorFrame, kind};
|
||
}
|
||
|
||
AnchorPositioningUtils::ContainingBlockInfo
|
||
AnchorPositioningUtils::ContainingBlockInfo::ExplicitCBFrameSize(
|
||
const nsRect& aContainingBlockRect) {
|
||
// TODO(dshin, bug 1989292): Ideally, this takes both local containing rect +
|
||
// scrollable containing rect, and one is picked here.
|
||
return ContainingBlockInfo{aContainingBlockRect};
|
||
}
|
||
|
||
AnchorPositioningUtils::ContainingBlockInfo
|
||
AnchorPositioningUtils::ContainingBlockInfo::UseCBFrameSize(
|
||
const nsIFrame* aPositioned) {
|
||
// TODO(dshin, bug 1989292): This just gets local containing block.
|
||
const auto* cb = aPositioned->GetParent();
|
||
MOZ_ASSERT(cb);
|
||
if (IsScrolled(cb)) {
|
||
cb = aPositioned->GetParent();
|
||
}
|
||
return ContainingBlockInfo{cb->GetPaddingRectRelativeToSelf()};
|
||
}
|
||
|
||
bool AnchorPositioningUtils::FitsInContainingBlock(
|
||
const nsIFrame* aPositioned, const AnchorPosReferenceData& aReferenceData) {
|
||
MOZ_ASSERT(aPositioned->FirstInFlow()->GetProperty(
|
||
nsIFrame::AnchorPosReferences()) == &aReferenceData);
|
||
|
||
const auto& scrollShift = aReferenceData.mDefaultScrollShift;
|
||
const auto scrollCompensatedSides = aReferenceData.mScrollCompensatedSides;
|
||
nsSize checkSize = [&]() {
|
||
const auto& adjustedCB = aReferenceData.mAdjustedContainingBlock;
|
||
if (scrollShift == nsPoint{} || scrollCompensatedSides == SideBits::eNone) {
|
||
return adjustedCB.Size();
|
||
}
|
||
|
||
// We now know that this frame's anchor has moved in relation to
|
||
// the original containing block, and that at least one side of our
|
||
// IMCB is attached to it.
|
||
|
||
// Scroll shift the adjusted containing block.
|
||
const auto shifted = aReferenceData.mAdjustedContainingBlock - scrollShift;
|
||
const auto& originalCB = aReferenceData.mOriginalContainingBlockRect;
|
||
|
||
// Now, move edges that are not attached to the anchors and pin it
|
||
// to the original containing block.
|
||
const nsPoint pt{
|
||
scrollCompensatedSides & SideBits::eLeft ? shifted.X() : originalCB.X(),
|
||
scrollCompensatedSides & SideBits::eTop ? shifted.Y() : originalCB.Y()};
|
||
const nsPoint ptMost{
|
||
scrollCompensatedSides & SideBits::eRight ? shifted.XMost()
|
||
: originalCB.XMost(),
|
||
scrollCompensatedSides & SideBits::eBottom ? shifted.YMost()
|
||
: originalCB.YMost()};
|
||
|
||
return nsSize{ptMost.x - pt.x, ptMost.y - pt.y};
|
||
}();
|
||
|
||
// Finally, reduce by inset.
|
||
checkSize -= nsSize{aReferenceData.mInsets.LeftRight(),
|
||
aReferenceData.mInsets.TopBottom()};
|
||
|
||
return aPositioned->GetMarginRectRelativeToSelf().Size() <= checkSize;
|
||
}
|
||
|
||
nsIFrame* AnchorPositioningUtils::GetAnchorThatFrameScrollsWith(
|
||
nsIFrame* aFrame, nsDisplayListBuilder* aBuilder,
|
||
bool aSkipAsserts /* = false */) {
|
||
#ifdef DEBUG
|
||
if (!aSkipAsserts) {
|
||
MOZ_ASSERT(!aBuilder || aBuilder->IsPaintingToWindow());
|
||
MOZ_ASSERT_IF(!aBuilder, aFrame->PresContext()->LayoutPhaseCount(
|
||
nsLayoutPhase::DisplayListBuilding) == 0);
|
||
}
|
||
#endif
|
||
|
||
if (!StaticPrefs::apz_async_scroll_css_anchor_pos_AtStartup()) {
|
||
return nullptr;
|
||
}
|
||
PhysicalAxes axes = aFrame->GetAnchorPosCompensatingForScroll();
|
||
if (axes.isEmpty()) {
|
||
return nullptr;
|
||
}
|
||
|
||
const auto* pos = aFrame->StylePosition();
|
||
if (!pos->mPositionAnchor.value.IsIdent()) {
|
||
return nullptr;
|
||
}
|
||
|
||
const nsAtom* defaultAnchorName =
|
||
pos->mPositionAnchor.value.AsIdent().AsAtom();
|
||
StyleCascadeLevel anchorTreeScope = pos->mPositionAnchor.scope;
|
||
nsIFrame* anchor =
|
||
const_cast<nsIFrame*>(aFrame->PresShell()->GetAnchorPosAnchor(
|
||
{defaultAnchorName, anchorTreeScope}, aFrame));
|
||
// TODO Bug 1997026 We need to update the anchor finding code so this can't
|
||
// happen. For now we just detect it and reject it.
|
||
if (anchor && !nsLayoutUtils::IsProperAncestorFrameConsideringContinuations(
|
||
aFrame->GetParent(), anchor)) {
|
||
return nullptr;
|
||
}
|
||
if (!aBuilder) {
|
||
return anchor;
|
||
}
|
||
// TODO for now ShouldAsyncScrollWithAnchor will return false if we are
|
||
// compensating in only one axis and there is a scroll frame between the
|
||
// anchor and the positioned's containing block that can scroll in the "wrong"
|
||
// axis so that we don't async scroll in the wrong axis because ASRs/APZ only
|
||
// support scrolling in both axes. This is not fully spec compliant, bug
|
||
// 1988034 tracks this.
|
||
return DisplayPortUtils::ShouldAsyncScrollWithAnchor(aFrame, anchor, aBuilder,
|
||
axes)
|
||
? anchor
|
||
: nullptr;
|
||
}
|
||
|
||
using AffectedAnchor = AnchorPosDefaultAnchorCache;
|
||
using AppliedShifts = nsTHashMap<nsIFrame*, nsPoint>;
|
||
struct ScrollShifts {
|
||
nsPoint mScrollCompensatedDelta;
|
||
nsPoint mChainedDelta;
|
||
|
||
nsPoint Sum() const { return mChainedDelta + mScrollCompensatedDelta; }
|
||
};
|
||
static ScrollShifts FindScrollCompensatedAnchorShift(
|
||
const PresShell* aPresShell, const nsIFrame* aPositioned,
|
||
const AnchorPosReferenceData& aReferenceData,
|
||
const AppliedShifts& aAppliedShifts) {
|
||
MOZ_ASSERT(aPositioned->IsAbsolutelyPositioned(),
|
||
"Anchor positioned frame is not absolutely positioned?");
|
||
const auto* defaultAnchorName = aReferenceData.mDefaultAnchorName.get();
|
||
if (!defaultAnchorName) {
|
||
return {};
|
||
}
|
||
const StyleCascadeLevel& anchorTreeScope = aReferenceData.mAnchorTreeScope;
|
||
auto* defaultAnchor = aPresShell->GetAnchorPosAnchor(
|
||
{defaultAnchorName, anchorTreeScope}, aPositioned);
|
||
if (!defaultAnchor) {
|
||
return {};
|
||
}
|
||
const auto compensatingForScroll = aReferenceData.CompensatingForScrollAxes();
|
||
// HACK(dshin, Bug 1999954): This is a workaround. While we try to lay out
|
||
// against the scroll-ignored position of an anchor, chain anchored frames
|
||
// end up containing scroll offset in their position. For now, walk the chain
|
||
// to account for those deltas too.
|
||
const nsPoint chainedDelta = [&]() -> nsPoint {
|
||
if (!defaultAnchor->StylePosition()->CanHaveDefaultAnchor()) {
|
||
return {};
|
||
}
|
||
const auto* referenceData =
|
||
defaultAnchor->GetProperty(nsIFrame::AnchorPosReferences());
|
||
if (!referenceData) {
|
||
return {};
|
||
}
|
||
if (auto delta = aAppliedShifts.Lookup(defaultAnchor)) {
|
||
// If we've gone through this anchor already, grab the delta we've
|
||
// applied already (if any), since otherwise
|
||
// FindScrollCompensatedAnchorShift will end up being zero anyways.
|
||
return *delta;
|
||
}
|
||
return FindScrollCompensatedAnchorShift(aPresShell, defaultAnchor,
|
||
*referenceData, aAppliedShifts)
|
||
.Sum();
|
||
}();
|
||
|
||
const nsPoint scrollCompensatedDelta = [&]() -> nsPoint {
|
||
if (compensatingForScroll.isEmpty()) {
|
||
return {};
|
||
}
|
||
const auto* scrollContainer =
|
||
AnchorPositioningUtils::GetNearestScrollFrame(defaultAnchor)
|
||
.mScrollContainer;
|
||
if (!scrollContainer) {
|
||
return nsPoint();
|
||
}
|
||
const auto offset = AnchorPositioningUtils::GetScrollOffsetFor(
|
||
compensatingForScroll, aPositioned,
|
||
AffectedAnchor{defaultAnchor, scrollContainer});
|
||
return offset - aReferenceData.mDefaultScrollShift;
|
||
}();
|
||
return {scrollCompensatedDelta, chainedDelta};
|
||
}
|
||
|
||
// https://drafts.csswg.org/css-anchor-position-1/#default-scroll-shift
|
||
static void UpdateScrollShift(PresShell* aPresShell, nsIFrame* aPositioned,
|
||
AnchorPosReferenceData& aReferenceData,
|
||
OverflowChangedTracker& aOct,
|
||
AppliedShifts& aAppliedShifts) {
|
||
const auto scrollShifts = FindScrollCompensatedAnchorShift(
|
||
aPresShell, aPositioned, aReferenceData, aAppliedShifts);
|
||
auto delta = scrollShifts.Sum();
|
||
if (delta == nsPoint()) {
|
||
return;
|
||
}
|
||
aAppliedShifts.InsertOrUpdate(aPositioned, delta);
|
||
// APZ-handled scrolling may skip scheduling of paint for the relevant
|
||
// scroll container - We need to ensure that we schedule a paint for this
|
||
// positioned frame. Could theoretically do this when deciding to skip
|
||
// painting in `ScrollContainerFrame::ScrollToImpl`, that'd be conditional
|
||
// on finding a dependent anchor anyway, we should be as specific as
|
||
// possible as to what gets scheduled to paint.
|
||
aPositioned->SchedulePaint();
|
||
if (!aReferenceData.CompensatingForScrollAxes().isEmpty()) {
|
||
aReferenceData.mDefaultScrollShift += scrollShifts.mScrollCompensatedDelta;
|
||
}
|
||
#ifdef ACCESSIBILITY
|
||
if (nsAccessibilityService* accService = GetAccService()) {
|
||
accService->NotifyAnchorPositionedScrollUpdate(aPresShell, aPositioned);
|
||
}
|
||
#endif
|
||
// NOTE(emilio): It might be tempting to call MarkPositionedFrameForReflow(),
|
||
// but we don't want to trigger a full reflow as a response to scrolling, and
|
||
// it seems to match other browsers and test expectations, see bug 1950251.
|
||
aPositioned->SetPosition(aPositioned->GetPosition() - delta);
|
||
aPositioned->UpdateOverflow();
|
||
// Ensure that we propagate the overflow change up
|
||
// the ancestor chain.
|
||
// TODO: I think we can just use aPositioned, TRANSFORM_CHANGED and remove the
|
||
// explicit UpdateOverflow() call above.
|
||
aOct.AddFrame(aPositioned->GetParent(),
|
||
OverflowChangedTracker::CHILDREN_CHANGED);
|
||
}
|
||
|
||
static bool TriggerFallbackReflow(PresShell* aPresShell, nsIFrame* aPositioned,
|
||
AnchorPosReferenceData& aReferencedAnchors,
|
||
bool aEvaluateAllFallbacksIfNeeded) {
|
||
auto totalFallbacks =
|
||
aPositioned->StylePosition()->mPositionTryFallbacks.value._0.Length();
|
||
if (!totalFallbacks) {
|
||
// No fallbacks specified.
|
||
return false;
|
||
}
|
||
|
||
const bool positionedFitsInCB = AnchorPositioningUtils::FitsInContainingBlock(
|
||
aPositioned, aReferencedAnchors);
|
||
auto* lastSuccessfulPosition =
|
||
aPositioned->GetProperty(nsIFrame::LastSuccessfulPositionFallback());
|
||
|
||
const bool needsRetry = [&] {
|
||
if (positionedFitsInCB) {
|
||
return false;
|
||
}
|
||
// TODO(bug 1987964): Try to only do this when the scroll offset changes?
|
||
if (aEvaluateAllFallbacksIfNeeded) {
|
||
return true;
|
||
}
|
||
return lastSuccessfulPosition && lastSuccessfulPosition->mLastIndex &&
|
||
!lastSuccessfulPosition->mTriedAllFallbacks;
|
||
}();
|
||
|
||
if (!needsRetry) {
|
||
// Record our last successful fallback.
|
||
if (lastSuccessfulPosition) {
|
||
if (lastSuccessfulPosition->mLastIndex) {
|
||
lastSuccessfulPosition->mRecordedIndex =
|
||
lastSuccessfulPosition->mLastIndex;
|
||
} else {
|
||
aPositioned->RemoveProperty(nsIFrame::LastSuccessfulPositionFallback());
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
// We'll be back, no need to record the last fallback.
|
||
aPresShell->MarkPositionedFrameForReflow(aPositioned);
|
||
return true;
|
||
}
|
||
|
||
static bool AnchorIsEffectivelyHidden(nsIFrame* aAnchor) {
|
||
if (!aAnchor->StyleVisibility()->IsVisible()) {
|
||
return true;
|
||
}
|
||
for (auto* anchor = aAnchor; anchor; anchor = anchor->GetParent()) {
|
||
if (anchor->HasAnyStateBits(NS_FRAME_POSITION_VISIBILITY_HIDDEN)) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
static bool ComputePositionVisibility(
|
||
PresShell* aPresShell, nsIFrame* aPositioned,
|
||
AnchorPosReferenceData& aReferencedAnchors) {
|
||
auto vis = aPositioned->StylePosition()->mPositionVisibility;
|
||
if (vis & StylePositionVisibility::ALWAYS) {
|
||
MOZ_ASSERT(vis == StylePositionVisibility::ALWAYS,
|
||
"always can't be combined");
|
||
return true;
|
||
}
|
||
if (vis & StylePositionVisibility::ANCHORS_VALID) {
|
||
for (const auto& ref : aReferencedAnchors) {
|
||
if (ref.GetData().isNothing()) {
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
if (vis & StylePositionVisibility::NO_OVERFLOW) {
|
||
const bool positionedFitsInCB =
|
||
AnchorPositioningUtils::FitsInContainingBlock(aPositioned,
|
||
aReferencedAnchors);
|
||
if (!positionedFitsInCB) {
|
||
return false;
|
||
}
|
||
}
|
||
if (vis & StylePositionVisibility::ANCHORS_VISIBLE) {
|
||
const auto* defaultAnchorName = aReferencedAnchors.mDefaultAnchorName.get();
|
||
auto anchorTreeScope = aReferencedAnchors.mAnchorTreeScope;
|
||
if (defaultAnchorName) {
|
||
auto* defaultAnchor = aPresShell->GetAnchorPosAnchor(
|
||
{defaultAnchorName, anchorTreeScope}, aPositioned);
|
||
if (defaultAnchor && AnchorIsEffectivelyHidden(defaultAnchor)) {
|
||
return false;
|
||
}
|
||
auto* containingBlock = aPositioned->GetParent()->FirstInFlow();
|
||
// If both are in the same cb the expectation is that this doesn't apply
|
||
// because there are no intervening clips. I think that's broken, see
|
||
// https://github.com/w3c/csswg-drafts/issues/13176
|
||
if (defaultAnchor &&
|
||
defaultAnchor->GetParent()->FirstInFlow() != containingBlock) {
|
||
auto* intersectionRoot = containingBlock;
|
||
nsRect rootRect = nsLayoutUtils::GetAllInFlowRectsUnion(
|
||
intersectionRoot, containingBlock,
|
||
nsLayoutUtils::GetAllInFlowRectsFlag::UseInkOverflowAsBox);
|
||
if (IsScrolled(intersectionRoot)) {
|
||
intersectionRoot = intersectionRoot->GetParent();
|
||
ScrollContainerFrame* sc = do_QueryFrame(intersectionRoot);
|
||
rootRect = sc->GetScrollPortRectAccountingForDynamicToolbar();
|
||
}
|
||
const auto* doc = aPositioned->PresContext()->Document();
|
||
const nsINode* root =
|
||
intersectionRoot->GetContent()
|
||
? static_cast<nsINode*>(intersectionRoot->GetContent())
|
||
: doc;
|
||
rootRect = nsLayoutUtils::TransformFrameRectToAncestor(
|
||
intersectionRoot, rootRect,
|
||
nsLayoutUtils::GetContainingBlockForClientRect(intersectionRoot));
|
||
const auto input = dom::IntersectionInput{
|
||
.mIsImplicitRoot = false,
|
||
.mRootNode = root,
|
||
.mRootFrame = intersectionRoot,
|
||
.mRootRect = rootRect,
|
||
.mRootMargin = {},
|
||
.mScrollMargin = {},
|
||
.mRemoteDocumentVisibleRect = {},
|
||
};
|
||
const auto output =
|
||
dom::DOMIntersectionObserver::Intersect(input, defaultAnchor);
|
||
// NOTE(emilio): It is a bit weird to also check that mIntersectionRect
|
||
// is non-empty, see https://github.com/w3c/csswg-drafts/issues/13176.
|
||
if (!output.Intersects() || (output.mIntersectionRect->IsEmpty() &&
|
||
!defaultAnchor->GetRect().IsEmpty())) {
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
|
||
bool AnchorPositioningUtils::TriggerLayoutOnOverflow(PresShell* aPresShell,
|
||
bool aFirstIteration) {
|
||
bool didLayoutPositionedItems = false;
|
||
|
||
OverflowChangedTracker oct;
|
||
AppliedShifts appliedShifts;
|
||
for (auto* positioned : aPresShell->GetAnchorPosPositioned()) {
|
||
AnchorPosReferenceData* referencedAnchors =
|
||
positioned->GetProperty(nsIFrame::AnchorPosReferences());
|
||
if (NS_WARN_IF(!referencedAnchors)) {
|
||
continue;
|
||
}
|
||
|
||
if (aFirstIteration) {
|
||
UpdateScrollShift(aPresShell, positioned, *referencedAnchors, oct,
|
||
appliedShifts);
|
||
}
|
||
|
||
if (TriggerFallbackReflow(aPresShell, positioned, *referencedAnchors,
|
||
aFirstIteration)) {
|
||
didLayoutPositionedItems = true;
|
||
}
|
||
|
||
if (didLayoutPositionedItems) {
|
||
// We'll come back to evaluate position-visibility later.
|
||
continue;
|
||
}
|
||
const bool shouldBeVisible =
|
||
ComputePositionVisibility(aPresShell, positioned, *referencedAnchors);
|
||
const bool isVisible =
|
||
!positioned->HasAnyStateBits(NS_FRAME_POSITION_VISIBILITY_HIDDEN);
|
||
if (shouldBeVisible != isVisible) {
|
||
positioned->AddOrRemoveStateBits(NS_FRAME_POSITION_VISIBILITY_HIDDEN,
|
||
!shouldBeVisible);
|
||
positioned->InvalidateFrameSubtree();
|
||
}
|
||
}
|
||
oct.Flush();
|
||
return didLayoutPositionedItems;
|
||
}
|
||
|
||
static const nsIFrame* GetMatchingContainingBlock(
|
||
const nsIFrame* aAnchor, const nsIFrame* aContainingBlock) {
|
||
MOZ_ASSERT(nsLayoutUtils::IsProperAncestorFrameConsideringContinuations(
|
||
aContainingBlock, aAnchor));
|
||
|
||
const auto* firstCont =
|
||
nsLayoutUtils::FirstContinuationOrIBSplitSibling(aContainingBlock);
|
||
const auto* lastCont =
|
||
nsLayoutUtils::LastContinuationOrIBSplitSibling(aContainingBlock);
|
||
if (firstCont == lastCont ||
|
||
nsLayoutUtils::IsProperAncestorFrame(aContainingBlock, aAnchor)) {
|
||
// aContainingBlock has no continuations nor IB-split siblings, or
|
||
// aContainingBlock itself is already a proper ancestor.
|
||
return aContainingBlock;
|
||
}
|
||
|
||
for (const auto* f = firstCont; f;
|
||
f = nsLayoutUtils::GetNextContinuationOrIBSplitSibling(f)) {
|
||
if (nsLayoutUtils::IsProperAncestorFrame(f, aAnchor)) {
|
||
return f;
|
||
}
|
||
}
|
||
return nullptr;
|
||
}
|
||
|
||
static nsSize InkOverflowSize(const nsIFrame* aFrame) {
|
||
return aFrame->InkOverflowRectRelativeToSelf().Size();
|
||
}
|
||
|
||
static nscoord BSizeFromPhysicalSize(const nsSize& aSize,
|
||
WritingMode aWritingMode) {
|
||
return LogicalSize{aWritingMode, aSize}.BSize(aWritingMode);
|
||
}
|
||
|
||
nsRect AnchorPositioningUtils::ReassembleAnchorRect(
|
||
const nsIFrame* aAnchor, const nsIFrame* aContainingBlock) {
|
||
const nsIFrame* matchingCB =
|
||
GetMatchingContainingBlock(aAnchor, aContainingBlock);
|
||
if (!matchingCB) {
|
||
MOZ_ASSERT_UNREACHABLE("No matching containing block?");
|
||
return nsRect{};
|
||
}
|
||
// Union fragments of the anchor within this containing block.
|
||
const auto fragRect =
|
||
nsLayoutUtils::GetCombinedFragmentRects(aAnchor, matchingCB);
|
||
// This anchor is contained within this CB fragment, or the containing block
|
||
// is inline.
|
||
// TODO(dshin, bug 2014554): Handle inline containing blocks properly. Inline
|
||
// CBs may continue over multiple lines, e.g. when an inline frame has a
|
||
// `<br>`. In this case, stacking of containing blocks should take line height
|
||
// into account.
|
||
if ((!fragRect.mSkippedPrevContinuation &&
|
||
!fragRect.mSkippedNextContinuation) ||
|
||
matchingCB->IsInlineOutside()) {
|
||
// fragRect.mRect is in matching containing block's coordinate space.
|
||
// Translate the rect back to aContainingBlock's coordinate space.
|
||
return fragRect.mRect +
|
||
matchingCB->GetOffsetToIgnoringScrolling(aContainingBlock);
|
||
}
|
||
// Ok, we need to reassemble the unfragmented size and position of the anchor,
|
||
// by stacking up the containing block in block direction.
|
||
const auto cbwm = matchingCB->GetWritingMode();
|
||
// Note the use of ink overflow, since the anchor may overflow it.
|
||
const auto cbSize = InkOverflowSize(matchingCB);
|
||
LogicalRect unfragmentedAnchorRect{cbwm, fragRect.mRect, cbSize};
|
||
LogicalSize relevantCbSize{cbwm, cbSize};
|
||
|
||
const auto* prev = fragRect.mSkippedPrevContinuation;
|
||
const auto* prevCb = matchingCB->GetPrevContinuation();
|
||
while (prev) {
|
||
MOZ_ASSERT(unfragmentedAnchorRect.BStart(cbwm) == 0,
|
||
"Prev continuation exists but this continuation didn't hit "
|
||
"block-start?");
|
||
MOZ_ASSERT(nsLayoutUtils::IsProperAncestorFrame(prevCb, prev));
|
||
|
||
const auto r = nsLayoutUtils::GetCombinedFragmentRects(prev, prevCb);
|
||
const auto inkOverflowSize = InkOverflowSize(prevCb);
|
||
const auto prevCBBSize = BSizeFromPhysicalSize(inkOverflowSize, cbwm);
|
||
|
||
relevantCbSize.BSize(cbwm) += prevCBBSize;
|
||
LogicalRect rect{cbwm, r.mRect, inkOverflowSize};
|
||
MOZ_ASSERT(rect.BEnd(cbwm) == prevCBBSize,
|
||
"Prev contination doesn't end at block-end?");
|
||
|
||
// Use the previous continuation's rect as a base, using its origin, and
|
||
// extending its inline/block size
|
||
unfragmentedAnchorRect = LogicalRect{
|
||
cbwm, rect.Origin(cbwm),
|
||
LogicalSize{
|
||
cbwm,
|
||
std::max(unfragmentedAnchorRect.ISize(cbwm), rect.ISize(cbwm)),
|
||
unfragmentedAnchorRect.BSize(cbwm) + rect.BSize(cbwm)}};
|
||
|
||
prev = r.mSkippedPrevContinuation;
|
||
prevCb = prevCb->GetPrevContinuation();
|
||
}
|
||
|
||
// We need to get through the rest of previous continuations here, since we
|
||
// need block-start offset of the anchor.
|
||
while (prevCb) {
|
||
const auto prevCbBOffset =
|
||
BSizeFromPhysicalSize(InkOverflowSize(prevCb), cbwm);
|
||
relevantCbSize.BSize(cbwm) += prevCbBOffset;
|
||
unfragmentedAnchorRect.MoveBy(cbwm, LogicalPoint{cbwm, 0, prevCbBOffset});
|
||
|
||
prevCb = prevCb->GetPrevContinuation();
|
||
}
|
||
|
||
// Assemble fragments in the next block flow fragment.
|
||
const auto* next = fragRect.mSkippedNextContinuation;
|
||
const auto* nextCb = matchingCB->GetNextContinuation();
|
||
while (next) {
|
||
MOZ_ASSERT(
|
||
unfragmentedAnchorRect.BEnd(cbwm) == relevantCbSize.BSize(cbwm),
|
||
"Next continuation exists this continuation didn't hit block-end?");
|
||
MOZ_ASSERT(nsLayoutUtils::IsProperAncestorFrame(nextCb, next));
|
||
const auto r = nsLayoutUtils::GetCombinedFragmentRects(next, nextCb);
|
||
|
||
const auto inkOverflowSize = InkOverflowSize(nextCb);
|
||
relevantCbSize.BSize(cbwm) += BSizeFromPhysicalSize(inkOverflowSize, cbwm);
|
||
LogicalRect rect{cbwm, r.mRect, inkOverflowSize};
|
||
MOZ_ASSERT(rect.BStart(cbwm) == 0,
|
||
"Next continuation doesn't start at block-start?");
|
||
|
||
// Use the current combined anchor rect as a base, keeping its origin,
|
||
// extending its inline/block size.
|
||
unfragmentedAnchorRect = LogicalRect{
|
||
cbwm, unfragmentedAnchorRect.Origin(cbwm),
|
||
LogicalSize{
|
||
cbwm,
|
||
std::max(unfragmentedAnchorRect.ISize(cbwm), rect.ISize(cbwm)),
|
||
unfragmentedAnchorRect.BSize(cbwm) + rect.BSize(cbwm)}};
|
||
|
||
next = r.mSkippedNextContinuation;
|
||
nextCb = nextCb->GetNextContinuation();
|
||
}
|
||
|
||
// Don't need to run through `nextCb` since reassembled anchor rect is fully
|
||
// constrained by the start side.
|
||
|
||
return unfragmentedAnchorRect.GetPhysicalRect(
|
||
cbwm, relevantCbSize.GetPhysicalSize(cbwm));
|
||
}
|
||
|
||
const dom::ShadowRoot* AnchorPositioningUtils::GetShadowRootForTreeScope(
|
||
const dom::Element& aElement, const StyleCascadeLevel& aTreeScope) {
|
||
return Servo_GetShadowRootForScoped(&aElement, aTreeScope);
|
||
}
|
||
|
||
} // namespace mozilla
|