/* 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/. */ /* * code for managing absolutely positioned children of a rendering * object that is a containing block for them */ #include "mozilla/AbsoluteContainingBlock.h" #include "AnchorPositioningUtils.h" #include "mozilla/CSSAlignUtils.h" #include "mozilla/DebugOnly.h" #include "mozilla/PresShell.h" #include "mozilla/ReflowInput.h" #include "mozilla/ScrollContainerFrame.h" #include "mozilla/ServoStyleSet.h" #include "mozilla/ViewportFrame.h" #include "mozilla/dom/ViewTransition.h" #include "nsCSSFrameConstructor.h" #include "nsContainerFrame.h" #include "nsGridContainerFrame.h" #include "nsIFrameInlines.h" #include "nsPlaceholderFrame.h" #include "nsPresContext.h" #include "nsPresContextInlines.h" #ifdef DEBUG # include "fmt/format.h" # include "nsBlockFrame.h" #endif using namespace mozilla; void AbsoluteContainingBlock::SetInitialChildList(nsIFrame* aDelegatingFrame, FrameChildListID aListID, nsFrameList&& aChildList) { MOZ_ASSERT(aListID == FrameChildListID::Absolute, "unexpected child list"); #ifdef DEBUG nsIFrame::VerifyDirtyBitSet(aChildList); for (nsIFrame* f : aChildList) { MOZ_ASSERT(f->GetParent() == aDelegatingFrame, "Unexpected parent"); } #endif mAbsoluteFrames = std::move(aChildList); } void AbsoluteContainingBlock::AppendFrames(nsIFrame* aDelegatingFrame, FrameChildListID aListID, nsFrameList&& aFrameList) { MOZ_ASSERT(aListID == FrameChildListID::Absolute, "unexpected child list"); // Append the frames to our list of absolutely positioned frames #ifdef DEBUG nsIFrame::VerifyDirtyBitSet(aFrameList); #endif mAbsoluteFrames.AppendFrames(nullptr, std::move(aFrameList)); // no damage to intrinsic widths, since absolutely positioned frames can't // change them aDelegatingFrame->PresShell()->FrameNeedsReflow( aDelegatingFrame, IntrinsicDirty::None, NS_FRAME_HAS_DIRTY_CHILDREN); } void AbsoluteContainingBlock::InsertFrames(nsIFrame* aDelegatingFrame, FrameChildListID aListID, nsIFrame* aPrevFrame, nsFrameList&& aFrameList) { MOZ_ASSERT(aListID == FrameChildListID::Absolute, "unexpected child list"); NS_ASSERTION(!aPrevFrame || aPrevFrame->GetParent() == aDelegatingFrame, "inserting after sibling frame with different parent"); #ifdef DEBUG nsIFrame::VerifyDirtyBitSet(aFrameList); #endif mAbsoluteFrames.InsertFrames(nullptr, aPrevFrame, std::move(aFrameList)); // no damage to intrinsic widths, since absolutely positioned frames can't // change them aDelegatingFrame->PresShell()->FrameNeedsReflow( aDelegatingFrame, IntrinsicDirty::None, NS_FRAME_HAS_DIRTY_CHILDREN); } void AbsoluteContainingBlock::RemoveFrame(FrameDestroyContext& aContext, FrameChildListID aListID, nsIFrame* aOldFrame) { MOZ_ASSERT(aListID == FrameChildListID::Absolute, "unexpected child list"); AutoTArray delFrames; for (nsIFrame* f = aOldFrame; f; f = f->GetNextInFlow()) { delFrames.AppendElement(f); } for (nsIFrame* delFrame : Reversed(delFrames)) { delFrame->GetParent()->GetAbsoluteContainingBlock()->StealFrame(delFrame); delFrame->Destroy(aContext); } } // In a fragmented context, for an absolutely positioned frame, this property // stores the logical border-box position that the frame would have, if its // abspos containing block were not being fragmented. The value for this // property is determined by performing a special reflow on the abspos // containing block (or a larger subtree that includes it), with an // unconstrained available block-size. // // The position is relative to the absolute containing block's border-box, and // is stored in the containing block's writing mode. // // Note: caller should use GetUnfragmentedPosition() helper to get the property. NS_DECLARE_FRAME_PROPERTY_DELETABLE(UnfragmentedPositionProperty, LogicalPoint) // Corresponding property to above, for the size of an absolutely positioned // frame. However, there are important distinctions to note: // 1. Writing mode is that of the absolutely positioned frame's. // 2. Stores border-box size for box-sizing: border-box, or content box size for // box-sizing: content-box. NS_DECLARE_FRAME_PROPERTY_DELETABLE(UnfragmentedSizeProperty, LogicalSize) // In a fragmented context, for an absolute containing block, this property // stores the unfragmented containing block rects. This is used to allow // proper percentage-sizing of its children. NS_DECLARE_FRAME_PROPERTY_DELETABLE( UnfragmentedContainingBlockProperty, AbsoluteContainingBlock::ContainingBlockRects) static LogicalPoint* GetUnfragmentedPosition(const ReflowInput& aCBReflowInput, const nsIFrame* aFrame) { // If the absolute containing block is in a measuring reflow, then aFrame's // unfragmented position is going to be updated. Don't return the obsolete // value in the property. return aCBReflowInput.mFlags.mIsInFragmentainerMeasuringReflow ? nullptr : aFrame->GetProperty(UnfragmentedPositionProperty()); } static LogicalSize* GetUnfragmentedSize(const ReflowInput& aCBReflowInput, const nsIFrame* aFrame) { return aCBReflowInput.mFlags.mIsInFragmentainerMeasuringReflow ? nullptr // Later fragment frames need to know the size for resolving // automatic sizes. : aFrame->FirstInFlow()->GetProperty(UnfragmentedSizeProperty()); } nsFrameList AbsoluteContainingBlock::StealPushedChildList() { return std::move(mPushedAbsoluteFrames); } void AbsoluteContainingBlock::DrainPushedChildList( const nsIFrame* aDelegatingFrame) { MOZ_ASSERT(aDelegatingFrame->GetAbsoluteContainingBlock() == this, "aDelegatingFrame's absCB should be us!"); // Our pushed absolute child list might be non-empty if our next-in-flow // hasn't reflowed yet. Move any child in that list that is a first-in-flow, // or whose prev-in-flow is not in our absolute child list, into our absolute // child list. for (auto iter = mPushedAbsoluteFrames.begin(); iter != mPushedAbsoluteFrames.end();) { // Advance the iterator first, so it's safe to move |child|. nsIFrame* const child = *iter++; if (!child->GetPrevInFlow() || child->GetPrevInFlow()->GetParent() != aDelegatingFrame) { mPushedAbsoluteFrames.RemoveFrame(child); mAbsoluteFrames.AppendFrame(nullptr, child); if (!child->GetPrevInFlow()) { child->RemoveStateBits(NS_FRAME_IS_PUSHED_OUT_OF_FLOW); } } } } bool AbsoluteContainingBlock::PrepareAbsoluteFrames( nsContainerFrame* aDelegatingFrame) { if (const nsIFrame* prevInFlow = aDelegatingFrame->GetPrevInFlow()) { AbsoluteContainingBlock* prevAbsCB = prevInFlow->GetAbsoluteContainingBlock(); MOZ_ASSERT(prevAbsCB, "If this delegating frame has an absCB, its prev-in-flow must " "have one, too!"); // Prepend the pushed absolute frames from the previous absCB to our // absolute child list. nsFrameList pushedFrames = prevAbsCB->StealPushedChildList(); if (pushedFrames.NotEmpty()) { mAbsoluteFrames.InsertFrames(aDelegatingFrame, nullptr, std::move(pushedFrames)); // After stealing children from the previous absCB, traverse our children // and see if any child has a prev-in-flow that is also in our child list. // If so, we insert them at the front of our pushed child list. nsFrameList newPushedAbsoluteFrames; for (auto iter = mAbsoluteFrames.begin(); iter != mAbsoluteFrames.end();) { // Advance the iterator first, so it's safe to move |child|. nsIFrame* const child = *iter++; nsIFrame* const childPrevInFlow = child->GetPrevInFlow(); if (childPrevInFlow && childPrevInFlow->GetParent() == aDelegatingFrame) { mAbsoluteFrames.RemoveFrame(child); newPushedAbsoluteFrames.AppendFrame(nullptr, child); } } if (newPushedAbsoluteFrames.NotEmpty()) { // Prepend the new pushed frames to the front of mPushedAbsoluteFrames. mPushedAbsoluteFrames.InsertFrames(nullptr, nullptr, std::move(newPushedAbsoluteFrames)); } } } // Similarly, for any children in our pushed child list that don't have a // prev-in-flow in our regular child list, we move those children back into // our child list. DrainPushedChildList(aDelegatingFrame); // Steal absolute frame's first-in-flow from our next-in-flow's child lists. for (const nsIFrame* nextInFlow = aDelegatingFrame->GetNextInFlow(); nextInFlow; nextInFlow = nextInFlow->GetNextInFlow()) { AbsoluteContainingBlock* nextAbsCB = nextInFlow->GetAbsoluteContainingBlock(); MOZ_ASSERT(nextAbsCB, "If this delegating frame has an absCB, its next-in-flow must " "have one, too!"); nextAbsCB->DrainPushedChildList(nextInFlow); for (auto iter = nextAbsCB->GetChildList().begin(); iter != nextAbsCB->GetChildList().end();) { // Advance the iterator first, so it's safe to move |child|. nsIFrame* const child = *iter++; if (!child->GetPrevInFlow()) { nextAbsCB->StealFrame(child); mAbsoluteFrames.AppendFrame(aDelegatingFrame, child); child->RemoveStateBits(NS_FRAME_IS_PUSHED_OUT_OF_FLOW); } } } return HasAbsoluteFrames(); } void AbsoluteContainingBlock::StealFrame(nsIFrame* aFrame) { const DebugOnly frameRemoved = mAbsoluteFrames.StartRemoveFrame(aFrame) || mPushedAbsoluteFrames.ContinueRemoveFrame(aFrame); MOZ_ASSERT(frameRemoved, "Failed to find aFrame from our child lists!"); } #ifdef DEBUG void AbsoluteContainingBlock::SanityCheckChildListsBeforeReflow( const nsIFrame* aDelegatingFrame) const { // TODO(TYLin): This is potentially O(N^2), where N is the number of // continuations that an abspos frame gets. Consider putting this behind an // about:config pref if it turns out to slow down debug builds too much. for (const nsIFrame* child : mAbsoluteFrames) { for (nsIFrame* prev = child->GetPrevInFlow(); prev; prev = prev->GetPrevInFlow()) { MOZ_ASSERT(!GetChildList().ContainsFrame(prev), "It is wrong that both a child and its prev-in-flow are in " "our child list!"); } } { // Verify that continuations are ordered across both lists concatenated. // If a frame and its continuation are both present, the continuation must // appear later. nsTHashSet allFrames; for (const nsFrameList* list : {&mAbsoluteFrames, &mPushedAbsoluteFrames}) { for (const nsIFrame* child : *list) { allFrames.Insert(child); } } nsTHashSet seen; auto CheckOrder = [&](const nsIFrame* child) { seen.Insert(child); const nsIFrame* prev = child->GetPrevInFlow(); if (prev && allFrames.Contains(prev)) { MOZ_ASSERT(seen.Contains(prev), "A frame's continuation appears before the frame in " "mAbsoluteFrames + mPushedAbsoluteFrames!"); } }; for (const nsFrameList* list : {&mAbsoluteFrames, &mPushedAbsoluteFrames}) { for (const nsIFrame* child : *list) { CheckOrder(child); } } } for (const nsIFrame* next = aDelegatingFrame->GetNextInFlow(); next; next = next->GetNextInFlow()) { auto* nextAbsCB = next->GetAbsoluteContainingBlock(); MOZ_ASSERT(nextAbsCB, "Delegating frame's next-in-flow should have " "AbsoluteContainingBlock!"); for (nsIFrame* child : nextAbsCB->GetChildList()) { MOZ_ASSERT( child->GetPrevInFlow(), "We should've pulled all abspos first-in-flows to our child list!"); } } } #endif static void MaybeMarkAncestorsAsHavingDescendantDependentOnItsStaticPos( nsIFrame* aFrame, nsIFrame* aContainingBlockFrame) { MOZ_ASSERT(aFrame->HasAnyStateBits(NS_FRAME_OUT_OF_FLOW)); if (!aFrame->StylePosition()->NeedsHypotheticalPositionIfAbsPos()) { return; } // We should have set the bit when reflowing the previous continuations // already. if (aFrame->GetPrevContinuation()) { return; } auto* placeholder = aFrame->GetPlaceholderFrame(); MOZ_ASSERT(placeholder); // Only fixed-pos frames can escape their containing block. if (!placeholder->HasAnyStateBits(PLACEHOLDER_FOR_FIXEDPOS)) { return; } for (nsIFrame* ancestor = placeholder->GetParent(); ancestor; ancestor = ancestor->GetParent()) { // Walk towards the ancestor's first continuation. That's the only one that // really matters, since it's the only one restyling will look at. We also // flag the following continuations just so it's caught on the first // early-return ones just to avoid walking them over and over. do { if (ancestor->DescendantMayDependOnItsStaticPosition()) { return; } // Moving the containing block or anything above it would move our static // position as well, so no need to flag it or any of its ancestors. if (aFrame == aContainingBlockFrame) { return; } ancestor->SetDescendantMayDependOnItsStaticPosition(true); nsIFrame* prev = ancestor->GetPrevContinuation(); if (!prev) { break; } ancestor = prev; } while (true); } } static bool IsSnapshotContainingBlock(const nsIFrame* aFrame) { return aFrame->Style()->GetPseudoType() == PseudoStyleType::MozSnapshotContainingBlock; } static PhysicalAxes CheckEarlyCompensatingForScroll(const nsIFrame* aKidFrame) { // Three conditions to compensate for scroll, once a default anchor // exists: // * Used alignment property is `anchor-center`, // * `position-area` is not `none`, or // * `anchor()` function refers to default anchor, or an anchor that // shares the same scroller with it. // First two conditions are checkable right now, so do that. if (!aKidFrame->StylePosition()->mPositionArea.IsNone()) { return PhysicalAxes{PhysicalAxis::Horizontal, PhysicalAxis::Vertical}; } PhysicalAxes result; const auto cbwm = aKidFrame->GetParent()->GetWritingMode(); // We don't concern ourselves with align/justify-items here, because // they don't apply to absolute positioned boxes [1]. // [1]: https://drafts.csswg.org/css-align-3/#justify-self-property if (aKidFrame->StylePosition()->mAlignSelf._0 & StyleAlignFlags::ANCHOR_CENTER) { result += cbwm.IsVertical() ? PhysicalAxis::Horizontal : PhysicalAxis::Vertical; } if (aKidFrame->StylePosition()->mJustifySelf._0 & StyleAlignFlags::ANCHOR_CENTER) { result += cbwm.IsVertical() ? PhysicalAxis::Vertical : PhysicalAxis::Horizontal; } return result; } static AnchorPosResolutionCache PopulateAnchorResolutionCache( const nsIFrame* aKidFrame, AnchorPosReferenceData* aData, bool aReuseUnfragmentedAnchorPosReferences) { MOZ_ASSERT(aKidFrame->HasAnchorPosReference()); if (aReuseUnfragmentedAnchorPosReferences) [[unlikely]] { MOZ_ASSERT( aKidFrame->FirstInFlow()->HasProperty(UnfragmentedPositionProperty())); // We inherited reference data from unfragmented reflow, but still need to // repopulate the cache. AnchorPosDefaultAnchorCache cache; if (aData->mDefaultAnchorName) { const auto* presShell = aKidFrame->PresShell(); cache.mAnchor = presShell->GetAnchorPosAnchor( ScopedNameRef{aData->mDefaultAnchorName, aData->mAnchorTreeScope}, aKidFrame->FirstInFlow()); MOZ_ASSERT(cache.mAnchor); cache.mScrollContainer = AnchorPositioningUtils::GetNearestScrollFrame(cache.mAnchor) .mScrollContainer; } return {aData, cache}; } // If the default anchor exists, it will likely be referenced (Except when // authors then use `anchor()` without referring to anchors whose nearest // scroller that of the default anchor, but that seems // counter-productive). This is a prerequisite for scroll compensation. We // also need to check for `anchor()` resolutions, so cache information for // default anchor and its scrollers right now. AnchorPosResolutionCache result{aData, {}}; // Let this call populate the cache. const auto defaultAnchorInfo = AnchorPositioningUtils::ResolveAnchorPosRect( aKidFrame, aKidFrame->GetParent(), {nullptr, StyleCascadeLevel::Default()}, false, &result); if (defaultAnchorInfo) { aData->AdjustCompensatingForScroll( CheckEarlyCompensatingForScroll(aKidFrame)); } return result; } static nsRect ComputeScrollableContainingBlock( const nsContainerFrame* aDelegatingFrame, const nsRect& aContainingBlock, const OverflowAreas* aOverflowAreas) { if (aOverflowAreas && aDelegatingFrame->Style()->GetPseudoType() == PseudoStyleType::MozScrolledContent) { // FIXME(bug 2004432): This is close enough to what we want. In practice // we don't want to account for relative positioning and so on, but this // seems good enough for now. ScrollContainerFrame* sf = do_QueryFrame(aDelegatingFrame->GetParent()); // Clamp to the scrollable range. return sf->GetUnsnappedScrolledRectInternal( aOverflowAreas->ScrollableOverflow(), aContainingBlock.Size()); } return aContainingBlock; } static SideBits GetScrollCompensatedSidesFor( const StylePositionArea& aPositionArea) { SideBits sides{SideBits::eNone}; // The opposite side of the direction keyword is attached to the // position-anchor grid, which is then attached to the anchor, and so is // scroll compensated. `center` is constrained by the position-area grid // on both sides. `span-all` is unconstrained in that axis. if (aPositionArea.first == StylePositionAreaKeyword::Left || aPositionArea.first == StylePositionAreaKeyword::SpanLeft) { sides |= SideBits::eRight; } else if (aPositionArea.first == StylePositionAreaKeyword::Right || aPositionArea.first == StylePositionAreaKeyword::SpanRight) { sides |= SideBits::eLeft; } else if (aPositionArea.first == StylePositionAreaKeyword::Center) { sides |= SideBits::eLeftRight; } if (aPositionArea.second == StylePositionAreaKeyword::Top || aPositionArea.second == StylePositionAreaKeyword::SpanTop) { sides |= SideBits::eBottom; } else if (aPositionArea.second == StylePositionAreaKeyword::Bottom || aPositionArea.second == StylePositionAreaKeyword::SpanBottom) { sides |= SideBits::eTop; } else if (aPositionArea.first == StylePositionAreaKeyword::Center) { sides |= SideBits::eTopBottom; } return sides; } struct ModifiedContainingBlock { using AnchorOffsetInfo = AbsoluteContainingBlock::AnchorOffsetInfo; Maybe mAnchorOffsetInfo; // Unmodified scrollable or local containing block nsRect mMaybeScrollableRect; // Containing block after all its modifications e.g. By grid/position-area. nsRect mFinalRect; explicit ModifiedContainingBlock(const nsRect& aRect) : mMaybeScrollableRect{aRect}, mFinalRect{aRect} {} ModifiedContainingBlock(const nsRect& aMaybeScrollableRect, const nsRect& aFinalRect) : mMaybeScrollableRect{aMaybeScrollableRect}, mFinalRect{aFinalRect} {} ModifiedContainingBlock(const nsPoint& aOffset, const StylePositionArea& aResolvedArea, const nsRect& aMaybeScrollableRect, const nsRect& aFinalRect) : mAnchorOffsetInfo{Some(AnchorOffsetInfo{aOffset, aResolvedArea})}, mMaybeScrollableRect{aMaybeScrollableRect}, mFinalRect{aFinalRect} {} AnchorOffsetInfo GetAnchorOffsetInfo() const { return mAnchorOffsetInfo.valueOr(AnchorOffsetInfo{}); } StylePositionArea ResolvedPositionArea() const { return mAnchorOffsetInfo .map([](const AnchorOffsetInfo& aInfo) { return aInfo.mResolvedPositionArea; }) .valueOr(StylePositionArea{}); } }; static ModifiedContainingBlock ComputeContainingBlock( bool aIsGrid, const nsContainerFrame* aDelegatingFrame, const ReflowInput& aReflowInput, const AbsoluteContainingBlock::ContainingBlockRects& aContainingBlockRects, nsIFrame* aKidFrame, AnchorPosResolutionCache* aAnchorPosResolutionCache, bool aReuseUnfragmentedAnchorPosReferences) { if (aReuseUnfragmentedAnchorPosReferences) { MOZ_ASSERT(aAnchorPosResolutionCache); const auto* referenceData = aAnchorPosResolutionCache->mReferenceData; if (const auto positionArea = aKidFrame->StylePosition()->mPositionArea; !positionArea.IsNone()) { return ModifiedContainingBlock{ referenceData->mDefaultScrollShift, AnchorPositioningUtils::PhysicalizePositionArea(positionArea, aKidFrame), referenceData->mOriginalContainingBlockRect, referenceData->mAdjustedContainingBlock}; } return ModifiedContainingBlock{referenceData->mOriginalContainingBlockRect, referenceData->mAdjustedContainingBlock}; } // The current containing block, with ongoing modifications. // Starts as a local containing block. nsRect containingBlock = aContainingBlockRects.mLocal; nsRect scrollableContainingBlock = aContainingBlockRects.mScrollable; const auto defaultAnchorInfo = [&]() -> Maybe { if (!aAnchorPosResolutionCache) { return Nothing{}; } return AnchorPositioningUtils::ResolveAnchorPosRect( aKidFrame, aDelegatingFrame, {nullptr, StyleCascadeLevel::Default()}, false, aAnchorPosResolutionCache); }(); if (defaultAnchorInfo) { // Presence of a valid default anchor causes us to use the scrollable // containing block. // https://github.com/w3c/csswg-drafts/issues/12552#issuecomment-3210696721 containingBlock = aContainingBlockRects.mScrollable; } if (const ViewportFrame* viewport = do_QueryFrame(aDelegatingFrame)) { if (IsSnapshotContainingBlock(aKidFrame)) { return ModifiedContainingBlock{ dom::ViewTransition::SnapshotContainingBlockRect( viewport->PresContext())}; } MOZ_ASSERT(aContainingBlockRects.mScrollable == aContainingBlockRects.mLocal); containingBlock = scrollableContainingBlock = viewport->GetContainingBlockAdjustedForScrollbars(aReflowInput); } // https://drafts.csswg.org/css-position/#original-cb // Handle grid-based adjustment first... if (aIsGrid) { const auto border = aDelegatingFrame->GetUsedBorder(); const nsPoint borderShift{border.left, border.top}; const nsRect preGridCB = containingBlock; // Shift in by border of the overall grid container. containingBlock = nsGridContainerFrame::GridItemCB(aKidFrame) + borderShift; if (!defaultAnchorInfo) { return ModifiedContainingBlock{preGridCB, containingBlock}; } } // ... Then the position-area based adjustment. if (defaultAnchorInfo) { auto positionArea = aKidFrame->StylePosition()->mPositionArea; // Offset should be up to, but not including the containing block's // scroll offset. const auto offset = AnchorPositioningUtils::GetScrollOffsetFor( aAnchorPosResolutionCache->mReferenceData->CompensatingForScrollAxes(), aKidFrame, aAnchorPosResolutionCache->mDefaultAnchorCache); StylePositionArea resolvedPositionArea{}; if (!positionArea.IsNone()) { // Imagine an abspos container with a scroller in it, and then an // anchor in it, where the anchor is visually in the middle of the // scrollport. Then, when the scroller moves such that the anchor's // left edge is on that of the scrollports, w.r.t. containing block, // the anchor is zero left offset horizontally. The position-area // grid needs to account for this. const auto scrolledAnchorRect = defaultAnchorInfo->mRect - offset; const auto scrolledAnchorCb = AnchorPositioningUtils:: AdjustAbsoluteContainingBlockRectForPositionArea( scrolledAnchorRect + aContainingBlockRects.mLocal.TopLeft(), containingBlock, aKidFrame->GetWritingMode(), aDelegatingFrame->GetWritingMode(), positionArea, &resolvedPositionArea); // By definition, we're using the default anchor, and are scroll // compensated. aAnchorPosResolutionCache->mReferenceData->mScrollCompensatedSides = GetScrollCompensatedSidesFor(resolvedPositionArea); // Unscroll the CB by canceling out the previously applied // scroll offset (See above), the offset will be applied later. containingBlock = scrolledAnchorCb + offset; } return ModifiedContainingBlock{offset, resolvedPositionArea, scrollableContainingBlock, containingBlock}; } return ModifiedContainingBlock{containingBlock}; } void AbsoluteContainingBlock::Reflow(nsContainerFrame* aDelegatingFrame, nsPresContext* aPresContext, const ReflowInput& aReflowInput, nsReflowStatus& aReflowStatus, const nsRect& aContainingBlock, AbsPosReflowFlags aFlags, OverflowAreas* aOverflowAreas) { const auto scrollableContainingBlock = ComputeScrollableContainingBlock( aDelegatingFrame, aContainingBlock, aOverflowAreas); const ContainingBlockRects passedContainingBlock{aContainingBlock, scrollableContainingBlock}; const auto* unfragmentedContainingBlockRects = [&]() -> const ContainingBlockRects* { if (aReflowInput.mFlags.mIsInFragmentainerMeasuringReflow) { // Doing the measuring reflow, so set the unfragmented containing sizes // here. NS_WARNING_ASSERTION(aDelegatingFrame->FirstInFlow() == aDelegatingFrame, "Saving unfragmented CB into non-first-in-flow"); aDelegatingFrame->SetOrUpdateDeletableProperty( UnfragmentedContainingBlockProperty(), passedContainingBlock); // Just reuse what was passed in. return &passedContainingBlock; } if (const auto* unfragmented = aDelegatingFrame->FirstInFlow()->GetProperty( UnfragmentedContainingBlockProperty())) { return unfragmented; } return &passedContainingBlock; }(); const auto* fragmentedContainingBlockRects = unfragmentedContainingBlockRects != &passedContainingBlock ? &passedContainingBlock : nullptr; #ifdef DEBUG SanityCheckChildListsBeforeReflow(aDelegatingFrame); #endif if (nsIFrame* prevInFlow = aDelegatingFrame->GetPrevInFlow()) { const auto* prevAbsCB = prevInFlow->GetAbsoluteContainingBlock(); MOZ_ASSERT(prevAbsCB, "If this delegating frame has an absCB, its prev-in-flow must " "have one, too!"); mCumulativeContainingBlockBSize = prevAbsCB->mCumulativeContainingBlockBSize; } else { mCumulativeContainingBlockBSize = 0; } nsReflowStatus reflowStatus; // Assume all the kids may need a reflow when they are in a fragmented // context. We'll perform more targeted check below. For example, skip reflow // them when they are positioned in a later fragment. const bool reflowAll = aReflowInput.ShouldReflowAllKids() || aReflowInput.IsInFragmentedContext(); const bool cbWidthChanged = aFlags.contains(AbsPosReflowFlag::CBWidthChanged); const bool cbHeightChanged = aFlags.contains(AbsPosReflowFlag::CBHeightChanged); const nscoord availBSize = aReflowInput.AvailableBSize(); const WritingMode containerWM = aReflowInput.GetWritingMode(); nsFrameList newPushedAbsoluteFrames; for (auto iter = mAbsoluteFrames.begin(); iter != mAbsoluteFrames.end();) { // Advance the iterator first, so it's safe to move |kidFrame|. nsIFrame* const kidFrame = *iter++; bool reuseUnfragmentedAnchorPosReferences = false; Maybe anchorPosResolutionCache; if (kidFrame->HasAnchorPosReference()) { AnchorPosReferenceData* referenceData = nullptr; if (const auto* firstInFlow = kidFrame->FirstInFlow(); GetUnfragmentedPosition(aReflowInput, firstInFlow)) { // Ok, we've done a measuring reflow with no fragmentation, and so the // unfragmented position property is now set. Use the existing // references, which contains the anchor lookup data from the measuring // reflow. referenceData = firstInFlow->GetProperty(nsIFrame::AnchorPosReferences()); reuseUnfragmentedAnchorPosReferences = true; } if (!referenceData) { referenceData = kidFrame->SetOrUpdateDeletableProperty( nsIFrame::AnchorPosReferences()); } anchorPosResolutionCache = Some(PopulateAnchorResolutionCache( kidFrame, referenceData, reuseUnfragmentedAnchorPosReferences)); } else { kidFrame->RemoveProperty(nsIFrame::AnchorPosReferences()); } bool kidNeedsReflow = reflowAll || kidFrame->IsSubtreeDirty() || FrameDependsOnContainer(kidFrame, cbWidthChanged, cbHeightChanged, anchorPosResolutionCache.ptrOr(nullptr)); if (kidFrame->IsSubtreeDirty()) { MaybeMarkAncestorsAsHavingDescendantDependentOnItsStaticPos( kidFrame, aDelegatingFrame); } if (kidNeedsReflow && !aPresContext->HasPendingInterrupt()) { const LogicalSize cbSize(containerWM, unfragmentedContainingBlockRects->mLocal.Size()); const LogicalMargin border = aDelegatingFrame->GetLogicalUsedBorder(containerWM) .ApplySkipSides( aDelegatingFrame->PreReflowBlockLevelLogicalSkipSides()); const nsSize cbBorderBoxSize = (cbSize + border.Size(containerWM)).GetPhysicalSize(containerWM); bool kidFrameNeedsPush = false; if (const auto* unfragPos = GetUnfragmentedPosition(aReflowInput, kidFrame); unfragPos && availBSize != NS_UNCONSTRAINEDSIZE) { // If kidFrame's position in this fragment is beyond the end of this // fragmentainer, push it to the next fragmentainer. const nscoord kidBPosInThisFragment = unfragPos->B(containerWM) - mCumulativeContainingBlockBSize; if (kidBPosInThisFragment > availBSize) { kidFrameNeedsPush = true; } } OverflowAreas kidOverflowAreas; nsReflowStatus kidStatus; if (!kidFrameNeedsPush) { ReflowAbsoluteFrame(aDelegatingFrame, aPresContext, aReflowInput, *unfragmentedContainingBlockRects, aFlags, kidFrame, kidStatus, aOverflowAreas, fragmentedContainingBlockRects, anchorPosResolutionCache.ptrOr(nullptr), reuseUnfragmentedAnchorPosReferences); if (aReflowInput.mFlags.mIsInFragmentainerMeasuringReflow) { kidFrame->SetOrUpdateDeletableProperty( UnfragmentedPositionProperty(), kidFrame->GetLogicalPosition(containerWM, cbBorderBoxSize)); const LogicalSize kidSize = kidFrame->StylePosition()->mBoxSizing == StyleBoxSizing::BorderBox ? kidFrame->GetLogicalSize() : kidFrame->ContentSize(); kidFrame->SetOrUpdateDeletableProperty(UnfragmentedSizeProperty(), kidSize); // kidFrame must be a first-in-flow here. In a measuring reflow // starting in the first column, we only see first-in-flows (either // unsplit or pulled back from later continuations of this absolute // containing block). However, in an incremental measuring reflow, if // the first-in-flow is not fully-complete, it is possible that we // still reflow continuations here. NS_ASSERTION( !kidFrame->GetPrevInFlow(), "UnfragmentedPositionProperty and UnfragmentedSizeProperty " "should only be set on first-in-flow!"); } MOZ_ASSERT(!kidStatus.IsInlineBreakBefore(), "ShouldAvoidBreakInside should prevent this from happening"); } nsIFrame* nextFrame = kidFrame->GetNextInFlow(); if (kidFrameNeedsPush) { StealFrame(kidFrame); kidFrame->AddStateBits(NS_FRAME_IS_PUSHED_OUT_OF_FLOW); newPushedAbsoluteFrames.AppendFrame(nullptr, kidFrame); } else if (!kidStatus.IsFullyComplete()) { if (!nextFrame) { nextFrame = aPresContext->PresShell() ->FrameConstructor() ->CreateContinuingFrame(kidFrame, aDelegatingFrame); nextFrame->AddStateBits(NS_FRAME_IS_PUSHED_OUT_OF_FLOW); newPushedAbsoluteFrames.AppendFrame(nullptr, nextFrame); } else if (nextFrame->GetParent() != aDelegatingFrame->GetNextInFlow()) { nextFrame->GetParent()->GetAbsoluteContainingBlock()->StealFrame( nextFrame); // nextFrame is in a later absCB continuation. To keep the // continuations in order, append it to mPushedAbsoluteFrames. mPushedAbsoluteFrames.AppendFrame(aDelegatingFrame, nextFrame); } reflowStatus.MergeCompletionStatusFrom(kidStatus); } else if (nextFrame) { // kidFrame is fully-complete. Delete all its next-in-flows. FrameDestroyContext context(aPresContext->PresShell()); nextFrame->GetParent()->GetAbsoluteContainingBlock()->RemoveFrame( context, FrameChildListID::Absolute, nextFrame); } } else { if (aOverflowAreas) { aDelegatingFrame->ConsiderChildOverflow(*aOverflowAreas, kidFrame); } } // Make a CheckForInterrupt call, here, not just HasPendingInterrupt. That // will make sure that we end up reflowing aDelegatingFrame in cases when // one of our kids interrupted. Otherwise we'd set the dirty or // dirty-children bit on the kid in the condition below, and then when // reflow completes and we go to mark dirty bits on all ancestors of that // kid we'll immediately bail out, because the kid already has a dirty bit. // In particular, we won't set any dirty bits on aDelegatingFrame, so when // the following reflow happens we won't reflow the kid in question. This // might be slightly suboptimal in cases where |kidFrame| itself did not // interrupt, since we'll trigger a reflow of it too when it's not strictly // needed. But the logic to not do that is enough more complicated, and // the case enough of an edge case, that this is probably better. if (kidNeedsReflow && aPresContext->CheckForInterrupt(aDelegatingFrame)) { if (aDelegatingFrame->HasAnyStateBits(NS_FRAME_IS_DIRTY)) { kidFrame->MarkSubtreeDirty(); } else { kidFrame->AddStateBits(NS_FRAME_HAS_DIRTY_CHILDREN); } } } if (newPushedAbsoluteFrames.NotEmpty()) { // Prepend the new pushed frames to the front of mPushedAbsoluteFrames. mPushedAbsoluteFrames.InsertFrames(nullptr, nullptr, std::move(newPushedAbsoluteFrames)); } if (availBSize != NS_UNCONSTRAINEDSIZE) { mCumulativeContainingBlockBSize += availBSize; } // Abspos frames can't cause their parent to be incomplete, // only overflow incomplete. if (reflowStatus.IsIncomplete() || mPushedAbsoluteFrames.NotEmpty()) { reflowStatus.SetOverflowIncomplete(); reflowStatus.SetNextInFlowNeedsReflow(); } aReflowStatus.MergeCompletionStatusFrom(reflowStatus); } static inline bool IsFixedPaddingSize(const LengthPercentage& aCoord) { return aCoord.ConvertsToLength(); } static inline bool IsFixedMarginSize(const AnchorResolvedMargin& aCoord) { return aCoord->ConvertsToLength(); } static inline bool IsFixedOffset(const AnchorResolvedInset& aInset) { // For anchor positioning functions, even if the computed value may be a // fixed length, it depends on the absolute containing block's size. return aInset->ConvertsToLength(); } bool AbsoluteContainingBlock::FrameDependsOnContainer( nsIFrame* f, bool aCBWidthChanged, bool aCBHeightChanged, AnchorPosResolutionCache* aAnchorPosResolutionCache) { const nsStylePosition* pos = f->StylePosition(); // See if f's position might have changed because it depends on a // placeholder's position. if (pos->NeedsHypotheticalPositionIfAbsPos()) { return true; } if (!aCBWidthChanged && !aCBHeightChanged) { // skip getting style data return false; } const nsStylePadding* padding = f->StylePadding(); const nsStyleMargin* margin = f->StyleMargin(); WritingMode wm = f->GetWritingMode(); const auto anchorResolutionParams = AnchorPosResolutionParams::From(f, aAnchorPosResolutionCache); if (wm.IsVertical() ? aCBHeightChanged : aCBWidthChanged) { // See if f's inline-size might have changed. // If margin-inline-start/end, padding-inline-start/end, // inline-size, min/max-inline-size are all lengths, 'none', or enumerated, // then our frame isize does not depend on the parent isize. // Note that borders never depend on the parent isize. // XXX All of the enumerated values except -moz-available are ok too. if (nsStylePosition::ISizeDependsOnContainer( pos->ISize(wm, anchorResolutionParams)) || nsStylePosition::MinISizeDependsOnContainer( pos->MinISize(wm, anchorResolutionParams)) || nsStylePosition::MaxISizeDependsOnContainer( pos->MaxISize(wm, anchorResolutionParams)) || !IsFixedPaddingSize(padding->mPadding.GetIStart(wm)) || !IsFixedPaddingSize(padding->mPadding.GetIEnd(wm))) { return true; } // See if f's position might have changed. If we're RTL then the // rules are slightly different. We'll assume percentage or auto // margins will always induce a dependency on the size if (!IsFixedMarginSize(margin->GetMargin(LogicalSide::IStart, wm, anchorResolutionParams)) || !IsFixedMarginSize( margin->GetMargin(LogicalSide::IEnd, wm, anchorResolutionParams))) { return true; } } if (wm.IsVertical() ? aCBWidthChanged : aCBHeightChanged) { // See if f's block-size might have changed. // If margin-block-start/end, padding-block-start/end, // min-block-size, and max-block-size are all lengths or 'none', // and bsize is a length or bsize and bend are auto and bstart is not auto, // then our frame bsize does not depend on the parent bsize. // Note that borders never depend on the parent bsize. // // FIXME(emilio): Should the BSize(wm).IsAuto() check also for the extremum // lengths? const auto bSize = pos->BSize(wm, anchorResolutionParams); const auto anchorOffsetResolutionParams = AnchorPosOffsetResolutionParams::UseCBFrameSize(anchorResolutionParams); if ((nsStylePosition::BSizeDependsOnContainer(bSize) && !(bSize->IsAuto() && pos->GetAnchorResolvedInset(LogicalSide::BEnd, wm, anchorOffsetResolutionParams) ->IsAuto() && !pos->GetAnchorResolvedInset(LogicalSide::BStart, wm, anchorOffsetResolutionParams) ->IsAuto())) || nsStylePosition::MinBSizeDependsOnContainer( pos->MinBSize(wm, anchorResolutionParams)) || nsStylePosition::MaxBSizeDependsOnContainer( pos->MaxBSize(wm, anchorResolutionParams)) || !IsFixedPaddingSize(padding->mPadding.GetBStart(wm)) || !IsFixedPaddingSize(padding->mPadding.GetBEnd(wm))) { return true; } // See if f's position might have changed. if (!IsFixedMarginSize(margin->GetMargin(LogicalSide::BStart, wm, anchorResolutionParams)) || !IsFixedMarginSize( margin->GetMargin(LogicalSide::BEnd, wm, anchorResolutionParams))) { return true; } } // Since we store coordinates relative to top and left, the position // of a frame depends on that of its container if it is fixed relative // to the right or bottom, or if it is positioned using percentages // relative to the left or top. Because of the dependency on the // sides (left and top) that we use to store coordinates, these tests // are easier to do using physical coordinates rather than logical. if (aCBWidthChanged) { const auto anchorOffsetResolutionParams = AnchorPosOffsetResolutionParams::UseCBFrameSize(anchorResolutionParams); if (!IsFixedOffset(pos->GetAnchorResolvedInset( eSideLeft, anchorOffsetResolutionParams))) { return true; } // Note that even if 'left' is a length, our position can still // depend on the containing block width, because if our direction or // writing-mode moves from right to left (in either block or inline // progression) and 'right' is not 'auto', we will discard 'left' // and be positioned relative to the containing block right edge. // 'left' length and 'right' auto is the only combination we can be // sure of. if ((wm.GetInlineDir() == WritingMode::InlineDir::RTL || wm.GetBlockDir() == WritingMode::BlockDir::RL) && !pos->GetAnchorResolvedInset(eSideRight, anchorOffsetResolutionParams) ->IsAuto()) { return true; } } if (aCBHeightChanged) { const auto anchorOffsetResolutionParams = AnchorPosOffsetResolutionParams::UseCBFrameSize(anchorResolutionParams); if (!IsFixedOffset(pos->GetAnchorResolvedInset( eSideTop, anchorOffsetResolutionParams))) { return true; } // See comment above for width changes. if (wm.GetInlineDir() == WritingMode::InlineDir::BTT && !pos->GetAnchorResolvedInset(eSideBottom, anchorOffsetResolutionParams) ->IsAuto()) { return true; } } return false; } void AbsoluteContainingBlock::DestroyFrames(DestroyContext& aContext) { mAbsoluteFrames.DestroyFrames(aContext); mPushedAbsoluteFrames.DestroyFrames(aContext); } void AbsoluteContainingBlock::MarkSizeDependentFramesDirty() { DoMarkFramesDirty(false); } void AbsoluteContainingBlock::MarkAllFramesDirty() { DoMarkFramesDirty(true); } void AbsoluteContainingBlock::DoMarkFramesDirty(bool aMarkAllDirty) { for (nsIFrame* kidFrame : mAbsoluteFrames) { if (aMarkAllDirty) { kidFrame->MarkSubtreeDirty(); } else if (FrameDependsOnContainer(kidFrame, true, true)) { // Add the weakest flags that will make sure we reflow this frame later kidFrame->AddStateBits(NS_FRAME_HAS_DIRTY_CHILDREN); } } } // Given an out-of-flow frame, this method returns the parent frame of its // placeholder frame or null if it doesn't have a placeholder for some reason. static nsContainerFrame* GetPlaceholderContainer(nsIFrame* aPositionedFrame) { nsIFrame* placeholder = aPositionedFrame->GetPlaceholderFrame(); return placeholder ? placeholder->GetParent() : nullptr; } struct NonAutoAlignParams { nscoord mCurrentStartInset; nscoord mCurrentEndInset; NonAutoAlignParams(nscoord aStartInset, nscoord aEndInset) : mCurrentStartInset(aStartInset), mCurrentEndInset(aEndInset) {} }; /** * This function returns the offset of an abs/fixed-pos child's static * position, with respect to the "start" corner of its alignment container, * according to CSS Box Alignment. This function only operates in a single * axis at a time -- callers can choose which axis via the |aAbsPosCBAxis| * parameter. This is called under two scenarios: * 1. We're statically positioning this absolutely positioned box, meaning * that the offsets are auto and will change depending on the alignment * of the box. * 2. The offsets are non-auto, but the element may not fill the inset-reduced * containing block, so its margin box needs to be aligned in that axis. * This is the step 4 of [1]. Should also be noted that, unlike static * positioning, where we may confine the alignment area for flex/grid * parent containers, we explicitly align to the inset-reduced absolute * container size. * * [1]: https://drafts.csswg.org/css-position-3/#abspos-layout * * @param aKidReflowInput The ReflowInput for the to-be-aligned abspos child. * @param aKidSizeInAbsPosCBWM The child frame's size (after it's been given * the opportunity to reflow), in terms of * aAbsPosCBWM. * @param aAbsPosCBSize The abspos CB size, in terms of aAbsPosCBWM. * @param aPlaceholderContainer The parent of the child frame's corresponding * placeholder frame, cast to a nsContainerFrame. * (This will help us choose which alignment enum * we should use for the child.) * @param aAbsPosCBWM The child frame's containing block's WritingMode. * @param aAbsPosCBAxis The axis (of the containing block) that we should * be doing this computation for. * @param aNonAutoAlignParams Parameters, if specified, indicating that we're * handling scenario 2. */ static nscoord OffsetToAlignedStaticPos( const ReflowInput& aKidReflowInput, const LogicalSize& aKidSizeInAbsPosCBWM, const LogicalSize& aAbsPosCBSize, const nsContainerFrame* aPlaceholderContainer, WritingMode aAbsPosCBWM, LogicalAxis aAbsPosCBAxis, Maybe aNonAutoAlignParams, const AbsoluteContainingBlock::AnchorOffsetInfo& aAnchorOffsetInfo) { if (!aPlaceholderContainer) { // (The placeholder container should be the thing that kicks this whole // process off, by setting PLACEHOLDER_STATICPOS_NEEDS_CSSALIGN. So it // should exist... but bail gracefully if it doesn't.) NS_ERROR( "Missing placeholder-container when computing a " "CSS Box Alignment static position"); return 0; } // (Most of this function is simply preparing args that we'll pass to // AlignJustifySelf at the end.) // NOTE: Our alignment container is aPlaceholderContainer's content-box // (or an area within it, if aPlaceholderContainer is a grid). So, we'll // perform most of our arithmetic/alignment in aPlaceholderContainer's // WritingMode. For brevity, we use the abbreviation "pc" for "placeholder // container" in variables below. WritingMode pcWM = aPlaceholderContainer->GetWritingMode(); LogicalSize absPosCBSizeInPCWM = aAbsPosCBSize.ConvertTo(pcWM, aAbsPosCBWM); // Find what axis aAbsPosCBAxis corresponds to, in placeholder's parent's // writing-mode. const LogicalAxis pcAxis = aAbsPosCBWM.ConvertAxisTo(aAbsPosCBAxis, pcWM); const LogicalSize alignAreaSize = [&]() { if (!aNonAutoAlignParams) { const bool placeholderContainerIsContainingBlock = aPlaceholderContainer == aKidReflowInput.mCBReflowInput->mFrame; LayoutFrameType parentType = aPlaceholderContainer->Type(); LogicalSize alignAreaSize(pcWM); if (parentType == LayoutFrameType::FlexContainer) { // We store the frame rect in FinishAndStoreOverflow, which runs _after_ // reflowing the absolute frames, so handle the special case of the // frame being the actual containing block here, by getting the size // from aAbsPosCBSize. // // The alignment container is the flex container's content box. if (placeholderContainerIsContainingBlock) { alignAreaSize = aAbsPosCBSize.ConvertTo(pcWM, aAbsPosCBWM); // aAbsPosCBSize is the padding-box, so substract the padding to get // the content box. alignAreaSize -= aPlaceholderContainer->GetLogicalUsedPadding(pcWM).Size(pcWM); } else { alignAreaSize = aPlaceholderContainer->GetLogicalSize(pcWM); LogicalMargin pcBorderPadding = aPlaceholderContainer->GetLogicalUsedBorderAndPadding(pcWM); alignAreaSize -= pcBorderPadding.Size(pcWM); } return alignAreaSize; } if (parentType == LayoutFrameType::GridContainer) { // This abspos elem's parent is a grid container. Per CSS Grid 10.1 // & 10.2: // - If the grid container *also* generates the abspos containing block // (a // grid area) for this abspos child, we use that abspos containing block // as the alignment container, too. (And its size is aAbsPosCBSize.) // - Otherwise, we use the grid's padding box as the alignment // container. // https://drafts.csswg.org/css-grid/#static-position if (placeholderContainerIsContainingBlock) { // The alignment container is the grid area that we're using as the // absolute containing block. alignAreaSize = aAbsPosCBSize.ConvertTo(pcWM, aAbsPosCBWM); } else { // The alignment container is a the grid container's content box // (which we can get by subtracting away its border & padding from // frame's size): alignAreaSize = aPlaceholderContainer->GetLogicalSize(pcWM); LogicalMargin pcBorderPadding = aPlaceholderContainer->GetLogicalUsedBorderAndPadding(pcWM); alignAreaSize -= pcBorderPadding.Size(pcWM); } return alignAreaSize; } } // Either we're in scenario 1 but within a non-flex/grid parent, or in // scenario 2. return aAbsPosCBSize.ConvertTo(pcWM, aAbsPosCBWM); }(); const nscoord existingOffset = aNonAutoAlignParams ? aNonAutoAlignParams->mCurrentStartInset + aNonAutoAlignParams->mCurrentEndInset : 0; const nscoord alignAreaSizeInAxis = ((pcAxis == LogicalAxis::Inline) ? alignAreaSize.ISize(pcWM) : alignAreaSize.BSize(pcWM)) - existingOffset; using AlignJustifyFlag = CSSAlignUtils::AlignJustifyFlag; CSSAlignUtils::AlignJustifyFlags flags(AlignJustifyFlag::IgnoreAutoMargins); // Given that scenario 2 ignores the parent container type, special handling // of absolutely-positioned child is also ignored. StyleAlignFlags alignConst = aNonAutoAlignParams ? aPlaceholderContainer ->CSSAlignmentForAbsPosChildWithinContainingBlock( aKidReflowInput, pcAxis, aAnchorOffsetInfo.mResolvedPositionArea, absPosCBSizeInPCWM) : aPlaceholderContainer->CSSAlignmentForAbsPosChild(aKidReflowInput, pcAxis); // If the safe bit in alignConst is set, set the safe flag in |flags|. const auto safetyBits = alignConst & (StyleAlignFlags::SAFE | StyleAlignFlags::UNSAFE); alignConst &= ~StyleAlignFlags::FLAG_BITS; if (safetyBits & StyleAlignFlags::SAFE) { flags += AlignJustifyFlag::OverflowSafe; } // Find out if placeholder-container & the OOF child have the same start-sides // in the placeholder-container's pcAxis. WritingMode kidWM = aKidReflowInput.GetWritingMode(); if (pcWM.ParallelAxisStartsOnSameSide(pcAxis, kidWM)) { flags += AlignJustifyFlag::SameSide; } if (aNonAutoAlignParams) { flags += AlignJustifyFlag::AligningMarginBox; } // (baselineAdjust is unused. CSSAlignmentForAbsPosChild() should've // converted 'baseline'/'last baseline' enums to their fallback values.) const nscoord baselineAdjust = nscoord(0); // AlignJustifySelf operates in the kid's writing mode, so we need to // represent the child's size and the desired axis in that writing mode: LogicalSize kidSizeInOwnWM = aKidSizeInAbsPosCBWM.ConvertTo(kidWM, aAbsPosCBWM); const LogicalAxis kidAxis = aAbsPosCBWM.ConvertAxisTo(aAbsPosCBAxis, kidWM); // Build an Inset Modified anchor info from the anchor which can be used to // align to the anchor-center, if AlignJustifySelf is AnchorCenter. Maybe anchorAlignInfo; if (alignConst == StyleAlignFlags::ANCHOR_CENTER && aKidReflowInput.mAnchorPosResolutionCache) { AnchorPosReferenceData* referenceData = aKidReflowInput.mAnchorPosResolutionCache->mReferenceData; if (referenceData) { const auto* cachedData = referenceData->Lookup( {referenceData->mDefaultAnchorName, referenceData->mAnchorTreeScope}); if (cachedData && *cachedData) { referenceData->AdjustCompensatingForScroll( aAbsPosCBWM.PhysicalAxis(aAbsPosCBAxis)); const auto& data = cachedData->ref(); if (data.mOffsetData) { const nsSize containerSize = aAbsPosCBSize.GetPhysicalSize(aAbsPosCBWM); // Adjust for position-area, grid, etc. const auto cbOffset = referenceData->mAdjustedContainingBlock.TopLeft() - referenceData->mOriginalContainingBlockRect.TopLeft(); const nsRect anchorRect(data.mOffsetData->mOrigin - cbOffset, data.mSize); const LogicalRect logicalAnchorRect{aAbsPosCBWM, anchorRect, containerSize}; const auto axisInAbsPosCBWM = kidWM.ConvertAxisTo(kidAxis, aAbsPosCBWM); const auto anchorStart = logicalAnchorRect.Start(axisInAbsPosCBWM, aAbsPosCBWM); const auto anchorSize = logicalAnchorRect.Size(axisInAbsPosCBWM, aAbsPosCBWM); anchorAlignInfo = Some(CSSAlignUtils::AnchorAlignInfo{anchorStart, anchorSize}); if (aNonAutoAlignParams) { anchorAlignInfo->mAnchorStart -= aNonAutoAlignParams->mCurrentStartInset; } } } } } nscoord offset = CSSAlignUtils::AlignJustifySelf( alignConst, kidAxis, flags, baselineAdjust, alignAreaSizeInAxis, aKidReflowInput, kidSizeInOwnWM, anchorAlignInfo); // Safe alignment clamping for anchor-center. // When using anchor-center with the safe keyword, or when both insets are // auto (which defaults to safe behavior), clamp the element to stay within // the containing block. if ((!aNonAutoAlignParams || (safetyBits & StyleAlignFlags::SAFE)) && alignConst == StyleAlignFlags::ANCHOR_CENTER) { const auto cbSize = aAbsPosCBSize.Size(aAbsPosCBAxis, aAbsPosCBWM); const auto kidSize = aKidSizeInAbsPosCBWM.Size(aAbsPosCBAxis, aAbsPosCBWM); if (aNonAutoAlignParams) { const nscoord currentStartInset = aNonAutoAlignParams->mCurrentStartInset; const nscoord finalStart = currentStartInset + offset; const nscoord clampedStart = CSSMinMax(finalStart, nscoord(0), cbSize - kidSize); offset = clampedStart - currentStartInset; } else { offset = CSSMinMax(offset, nscoord(0), cbSize - kidSize); } } const auto rawAlignConst = (pcAxis == LogicalAxis::Inline) ? aKidReflowInput.mStylePosition->mJustifySelf._0 : aKidReflowInput.mStylePosition->mAlignSelf._0; if (aNonAutoAlignParams && !safetyBits && (rawAlignConst != StyleAlignFlags::AUTO || alignConst == StyleAlignFlags::ANCHOR_CENTER)) { // No `safe` or `unsafe` specified - "in-between" behaviour for relevant // alignment values: https://drafts.csswg.org/css-position-3/#abspos-layout // Skip if the raw self alignment for this element is `auto` to preserve // legacy behaviour, except in the case where the resolved value is // anchor-center (where "legacy behavior" is not a concern). // Follows https://drafts.csswg.org/css-align-3/#auto-safety-position const auto cbSize = aAbsPosCBSize.Size(aAbsPosCBAxis, aAbsPosCBWM); // IMCB stands for "Inset-Modified Containing Block." const auto imcbStart = aNonAutoAlignParams->mCurrentStartInset; const auto imcbEnd = cbSize - aNonAutoAlignParams->mCurrentEndInset; // Need to pull the offset into the "current view," unless it already did. const auto scrollOffset = aAnchorOffsetInfo.mResolvedPositionArea.IsNone() ? aAbsPosCBWM.PhysicalAxis(aAbsPosCBAxis) == PhysicalAxis::Horizontal ? aAnchorOffsetInfo.mScrollOffset.x : aAnchorOffsetInfo.mScrollOffset.y : 0; const auto kidSize = aKidSizeInAbsPosCBWM.Size(aAbsPosCBAxis, aAbsPosCBWM); const auto kidStart = aNonAutoAlignParams->mCurrentStartInset + offset - scrollOffset; const auto kidEnd = kidStart + kidSize; // "[...] the overflow limit rect is the bounding rectangle of the alignment // subject’s inset-modified containing block and its original containing // block." const auto overflowLimitRectStart = std::min(0, imcbStart); const auto overflowLimitRectEnd = std::max(cbSize, imcbEnd); if (kidStart >= imcbStart && kidEnd <= imcbEnd) { // 1. We fit inside the IMCB, no action needed. } else if (kidSize <= overflowLimitRectEnd - overflowLimitRectStart) { // 2. We overflowed IMCB, try to cover IMCB completely, if it's not. if (kidStart <= imcbStart && kidEnd >= imcbEnd) { // IMCB already covered, ensure that we aren't escaping the limit rect. if (kidStart < overflowLimitRectStart) { offset += overflowLimitRectStart - kidStart; } else if (kidEnd > overflowLimitRectEnd) { offset -= kidEnd - overflowLimitRectEnd; } } else if (kidEnd < imcbEnd && kidStart < imcbStart) { // Space to end, overflowing on start - nudge to end. offset += std::min(imcbStart - kidStart, imcbEnd - kidEnd); } else if (kidStart > imcbStart && kidEnd > imcbEnd) { // Space to start, overflowing on end - nudge to start. offset -= std::min(kidEnd - imcbEnd, kidStart - imcbStart); } } else { // 3. We'll overflow the limit rect. Start align the subject int overflow // limit rect. offset = -aNonAutoAlignParams->mCurrentStartInset + overflowLimitRectStart; } } // "offset" is in terms of the CSS Box Alignment container (i.e. it's in // terms of pcWM). But our return value needs to in terms of the containing // block's writing mode, which might have the opposite directionality in the // given axis. In that case, we just need to negate "offset" when returning, // to make it have the right effect as an offset for coordinates in the // containing block's writing mode. if (!pcWM.ParallelAxisStartsOnSameSide(pcAxis, aAbsPosCBWM)) { return -offset; } return offset; } void AbsoluteContainingBlock::ResolveSizeDependentOffsets( ReflowInput& aKidReflowInput, const LogicalSize& aCBSize, const LogicalSize& aKidSize, const LogicalMargin& aMargin, const AnchorOffsetInfo& aAnchorOffsetInfo, LogicalMargin& aOffsets) { WritingMode outerWM = aKidReflowInput.mParentReflowInput->GetWritingMode(); // Now that we know the child's size, we resolve any sentinel values in its // IStart/BStart offset coordinates that depend on that size. // * NS_AUTOOFFSET indicates that the child's position in the given axis // is determined by its end-wards offset property, combined with its size and // available space. e.g.: "top: auto; height: auto; bottom: 50px" // * m{I,B}OffsetsResolvedAfterSize indicate that the child is using its // static position in that axis, *and* its static position is determined by // the axis-appropriate css-align property (which may require the child's // size, e.g. to center it within the parent). if ((NS_AUTOOFFSET == aOffsets.IStart(outerWM)) || (NS_AUTOOFFSET == aOffsets.BStart(outerWM)) || aKidReflowInput.mFlags.mIOffsetsNeedCSSAlign || aKidReflowInput.mFlags.mBOffsetsNeedCSSAlign) { // placeholderContainer is used in each of the m{I,B}OffsetsNeedCSSAlign // clauses. We declare it at this scope so we can avoid having to look // it up twice (and only look it up if it's needed). nsContainerFrame* placeholderContainer = nullptr; if (NS_AUTOOFFSET == aOffsets.IStart(outerWM)) { NS_ASSERTION(NS_AUTOOFFSET != aOffsets.IEnd(outerWM), "Can't solve for both start and end"); aOffsets.IStart(outerWM) = aCBSize.ISize(outerWM) - aOffsets.IEnd(outerWM) - aMargin.IStartEnd(outerWM) - aKidSize.ISize(outerWM); } else if (aKidReflowInput.mFlags.mIOffsetsNeedCSSAlign) { placeholderContainer = GetPlaceholderContainer(aKidReflowInput.mFrame); nscoord offset = OffsetToAlignedStaticPos( aKidReflowInput, aKidSize, aCBSize, placeholderContainer, outerWM, LogicalAxis::Inline, Nothing{}, aAnchorOffsetInfo); // Shift IStart from its current position (at start corner of the // alignment container) by the returned offset. And set IEnd to the // distance between the kid's end edge to containing block's end edge. aOffsets.IStart(outerWM) += offset; aOffsets.IEnd(outerWM) = aCBSize.ISize(outerWM) - (aOffsets.IStart(outerWM) + aKidSize.ISize(outerWM)); } if (NS_AUTOOFFSET == aOffsets.BStart(outerWM)) { aOffsets.BStart(outerWM) = aCBSize.BSize(outerWM) - aOffsets.BEnd(outerWM) - aMargin.BStartEnd(outerWM) - aKidSize.BSize(outerWM); } else if (aKidReflowInput.mFlags.mBOffsetsNeedCSSAlign) { if (!placeholderContainer) { placeholderContainer = GetPlaceholderContainer(aKidReflowInput.mFrame); } nscoord offset = OffsetToAlignedStaticPos( aKidReflowInput, aKidSize, aCBSize, placeholderContainer, outerWM, LogicalAxis::Block, Nothing{}, aAnchorOffsetInfo); // Shift BStart from its current position (at start corner of the // alignment container) by the returned offset. And set BEnd to the // distance between the kid's end edge to containing block's end edge. aOffsets.BStart(outerWM) += offset; aOffsets.BEnd(outerWM) = aCBSize.BSize(outerWM) - (aOffsets.BStart(outerWM) + aKidSize.BSize(outerWM)); } aKidReflowInput.SetComputedLogicalOffsets(outerWM, aOffsets); } } void AbsoluteContainingBlock::ResolveAutoMarginsAfterLayout( ReflowInput& aKidReflowInput, const LogicalSize& aCBSize, const LogicalSize& aKidSize, LogicalMargin& aMargin, const LogicalMargin& aOffsets) { WritingMode outerWM = aKidReflowInput.mParentReflowInput->GetWritingMode(); const auto& styleMargin = aKidReflowInput.mStyleMargin; const auto anchorResolutionParams = AnchorPosResolutionParams::From(&aKidReflowInput); auto ResolveMarginsInAxis = [&](LogicalAxis aAxis) { const auto startSide = MakeLogicalSide(aAxis, LogicalEdge::Start); const auto endSide = MakeLogicalSide(aAxis, LogicalEdge::End); // No need to substract border sizes because aKidSize has it included // already. Also, if any offset is auto, the auto margin resolves to zero. // https://drafts.csswg.org/css-position-3/#abspos-margins const bool autoOffset = aOffsets.Side(startSide, outerWM) == NS_AUTOOFFSET || aOffsets.Side(endSide, outerWM) == NS_AUTOOFFSET; nscoord availMarginSpace; if (autoOffset) { availMarginSpace = 0; } else { const nscoord stretchFitSize = std::max( 0, aCBSize.Size(aAxis, outerWM) - aOffsets.StartEnd(aAxis, outerWM) - aMargin.StartEnd(aAxis, outerWM)); availMarginSpace = stretchFitSize - aKidSize.Size(aAxis, outerWM); } const bool startSideMarginIsAuto = styleMargin->GetMargin(startSide, outerWM, anchorResolutionParams) ->IsAuto(); const bool endSideMarginIsAuto = styleMargin->GetMargin(endSide, outerWM, anchorResolutionParams) ->IsAuto(); if (aAxis == LogicalAxis::Inline) { ReflowInput::ComputeAbsPosInlineAutoMargin(availMarginSpace, outerWM, startSideMarginIsAuto, endSideMarginIsAuto, aMargin); } else { ReflowInput::ComputeAbsPosBlockAutoMargin(availMarginSpace, outerWM, startSideMarginIsAuto, endSideMarginIsAuto, aMargin); } }; ResolveMarginsInAxis(LogicalAxis::Inline); ResolveMarginsInAxis(LogicalAxis::Block); aKidReflowInput.SetComputedLogicalMargin(outerWM, aMargin); nsMargin* propValue = aKidReflowInput.mFrame->GetProperty(nsIFrame::UsedMarginProperty()); // InitOffsets should've created a UsedMarginProperty for us, if any margin is // auto. MOZ_ASSERT_IF( styleMargin->HasInlineAxisAuto(outerWM, anchorResolutionParams) || styleMargin->HasBlockAxisAuto(outerWM, anchorResolutionParams), propValue); if (propValue) { *propValue = aMargin.GetPhysicalMargin(outerWM); } } struct None {}; using OldCacheState = Variant; struct MOZ_STACK_CLASS MOZ_RAII AutoFallbackStyleSetter { AutoFallbackStyleSetter(nsIFrame* aFrame, ComputedStyle* aFallbackStyle, AnchorPosResolutionCache* aCache, bool aIsFirstTry) : mFrame(aFrame), mCache{aCache}, mOldCacheState{None{}} { if (aFallbackStyle) { mOldStyle = aFrame->SetComputedStyleWithoutNotification(aFallbackStyle); } // We need to be able to "go back" to the old, first try (Which is not // necessarily base style) cache. if (!aIsFirstTry && aCache) { // New fallback could just be a flip keyword. if (mOldStyle && mOldStyle->StylePosition()->mPositionAnchor != aFrame->StylePosition()->mPositionAnchor) { mOldCacheState = OldCacheState{aCache->TryPositionWithDifferentDefaultAnchor()}; // TODO(dshin, bug 2014913): Fragmentation _can_ change the containing // block size from its unfragmented version, and that may cause us to // choose a different fallback, and hit this code path. *aCache = PopulateAnchorResolutionCache(aFrame, aCache->mReferenceData, false); } else { mOldCacheState = OldCacheState{aCache->TryPositionWithSameDefaultAnchor()}; if (aCache->mDefaultAnchorCache.mAnchor) { aCache->mReferenceData->AdjustCompensatingForScroll( CheckEarlyCompensatingForScroll(aFrame)); } } } } ~AutoFallbackStyleSetter() { if (mOldStyle) { mFrame->SetComputedStyleWithoutNotification(std::move(mOldStyle)); } std::move(mOldCacheState) .match( [](None&&) {}, [&](AnchorPosResolutionCache::PositionTryBackup&& aBackup) { mCache->UndoTryPositionWithSameDefaultAnchor(std::move(aBackup)); }, [&](AnchorPosResolutionCache::PositionTryFullBackup&& aBackup) { mCache->UndoTryPositionWithDifferentDefaultAnchor( std::move(aBackup)); }); } void CommitCurrentFallback() { mOldCacheState = OldCacheState{None{}}; // If we have a non-layout dependent margin / paddings, which are different // from our original style, we need to make sure to commit it into the frame // property so that it doesn't get lost after returning from reflow. nsMargin margin; if (mOldStyle && !mOldStyle->StyleMargin()->MarginEquals(*mFrame->StyleMargin()) && mFrame->StyleMargin()->GetMargin(margin)) { mFrame->SetOrUpdateDeletableProperty(nsIFrame::UsedMarginProperty(), margin); } } private: nsIFrame* const mFrame; RefPtr mOldStyle; AnchorPosResolutionCache* const mCache; OldCacheState mOldCacheState; }; // XXX Optimize the case where it's a resize reflow and the absolutely // positioned child has the exact same size and position and skip the // reflow... void AbsoluteContainingBlock::ReflowAbsoluteFrame( nsContainerFrame* aDelegatingFrame, nsPresContext* aPresContext, const ReflowInput& aReflowInput, const ContainingBlockRects& aContainingBlockRects, AbsPosReflowFlags aFlags, nsIFrame* aKidFrame, nsReflowStatus& aStatus, OverflowAreas* aOverflowAreas, const ContainingBlockRects* aFragmentedContainingBlockRects, AnchorPosResolutionCache* aAnchorPosResolutionCache, bool aReuseUnfragmentedAnchorPosReferences) { MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!"); #ifdef DEBUG if (nsBlockFrame::gNoisyReflow) { nsIFrame::IndentBy(stdout, nsBlockFrame::gNoiseIndent); fmt::println("abspos {}: begin reflow: availSize={}, orig cbRect={}", aKidFrame->ListTag(), ToString(aReflowInput.AvailableSize()), ToString(aContainingBlockRects.mLocal)); } AutoNoisyIndenter indent(nsBlockFrame::gNoisy); #endif // DEBUG const WritingMode outerWM = aReflowInput.GetWritingMode(); const WritingMode wm = aKidFrame->GetWritingMode(); const bool isGrid = aFlags.contains(AbsPosReflowFlag::IsGridContainerCB); const auto* kidStylePosition = aKidFrame->StylePosition(); auto fallbacks = kidStylePosition->mPositionTryFallbacks.value._0.AsSpan(); const auto fallbackScope = kidStylePosition->mPositionTryFallbacks.scope; Maybe currentFallbackIndex; const StylePositionTryFallbacksItem* currentFallback = nullptr; RefPtr currentFallbackStyle; RefPtr firstTryStyle; Maybe firstTryIndex; // If non-'normal' position-try-order is in effect, we keep track of the // index of the "best" option seen, and its size in the relevant axis, so // that once all fallbacks have been considered we can reset to the one // that provided the most space. Maybe bestIndex; nscoord bestSize = -1; // Flag to indicate that we've determined which fallback to use and should // exit the loop. bool finalizing = false; auto tryOrder = kidStylePosition->mPositionTryOrder; // If position-try-order is a logical value, resolve to physical using // the containing block's writing mode. switch (tryOrder) { case StylePositionTryOrder::MostInlineSize: tryOrder = outerWM.IsVertical() ? StylePositionTryOrder::MostHeight : StylePositionTryOrder::MostWidth; break; case StylePositionTryOrder::MostBlockSize: tryOrder = outerWM.IsVertical() ? StylePositionTryOrder::MostWidth : StylePositionTryOrder::MostHeight; break; default: break; } const auto* baseStyle = aKidFrame->Style(); // Set the current fallback to the given index, or reset to the base position // if Nothing() is passed. auto SeekFallbackTo = [&](Maybe aIndex) -> bool { if (!aIndex) { currentFallbackIndex = Nothing(); currentFallback = nullptr; currentFallbackStyle = nullptr; return true; } uint32_t index = *aIndex; if (index >= fallbacks.Length()) { return false; } const StylePositionTryFallbacksItem* nextFallback; RefPtr nextFallbackStyle; while (true) { nextFallback = &fallbacks[index]; nextFallbackStyle = aPresContext->StyleSet()->ResolvePositionTry( fallbackScope, *aKidFrame->GetContent()->AsElement(), *baseStyle, *nextFallback); if (nextFallbackStyle) { break; } // No @position-try rule for this name was found, per spec we should // skip it. index++; if (index >= fallbacks.Length()) { return false; } } currentFallbackIndex = Some(index); currentFallback = nextFallback; currentFallbackStyle = std::move(nextFallbackStyle); return true; }; // Advance to the next fallback to be tried. Normally this is simply the next // index in the position-try-fallbacks list, but we have some special cases: // - if we're currently at the last-successful fallback (recorded as // firstTryIndex), we "advance" to the base position // - we skip the last-successful fallback when we reach its position again auto TryAdvanceFallback = [&]() -> bool { if (fallbacks.IsEmpty()) { return false; } if (firstTryIndex && currentFallbackIndex == firstTryIndex) { return SeekFallbackTo(Nothing()); } uint32_t nextFallbackIndex = currentFallbackIndex ? *currentFallbackIndex + 1 : 0; if (firstTryIndex && nextFallbackIndex == *firstTryIndex) { ++nextFallbackIndex; } return SeekFallbackTo(Some(nextFallbackIndex)); }; Maybe firstTryRect; if (auto* lastSuccessfulPosition = aKidFrame->GetProperty(nsIFrame::LastSuccessfulPositionFallback()); lastSuccessfulPosition && lastSuccessfulPosition->mRecordedIndex && SeekFallbackTo(lastSuccessfulPosition->mRecordedIndex)) { // Remember which fallback we're trying first; also record its style, // in case we need to restore it later. firstTryIndex = lastSuccessfulPosition->mRecordedIndex; firstTryStyle = currentFallbackStyle; } // Assume we *are* overflowing the CB and if we find a fallback that doesn't // overflow, we set this to false and break the loop. bool isOverflowingCB = true; do { AutoFallbackStyleSetter fallback(aKidFrame, currentFallbackStyle, aAnchorPosResolutionCache, firstTryIndex == currentFallbackIndex); auto cb = ComputeContainingBlock(isGrid, aDelegatingFrame, aReflowInput, aContainingBlockRects, aKidFrame, aAnchorPosResolutionCache, aReuseUnfragmentedAnchorPosReferences); PhysicalAxes earlyScrollCompensation; if (aAnchorPosResolutionCache) { const auto& originalCb = cb.mMaybeScrollableRect; aAnchorPosResolutionCache->mReferenceData->mOriginalContainingBlockRect = originalCb; // Stash the adjusted containing block as well, since the insets need to // resolve against the adjusted CB, e.g. With `position-area: bottom // right;`, + `left: anchor(right);` // resolves to 0. aAnchorPosResolutionCache->mReferenceData->mAdjustedContainingBlock = cb.mFinalRect; // May need to recompute scroll compensation if e.g. anchor-center in one // axis, then `anchor(--default-anchor)` in another. earlyScrollCompensation = aAnchorPosResolutionCache->mReferenceData ->CompensatingForScrollAxes(); } const LogicalSize cbSize(outerWM, cb.mFinalRect.Size()); ReflowInput::InitFlags initFlags; const bool staticPosIsCBOrigin = [&] { if (aFlags.contains(AbsPosReflowFlag::IsGridContainerCB)) { // When a grid container generates the abs.pos. CB for a *child* then // the static position is determined via CSS Box Alignment within the // abs.pos. CB (a grid area, i.e. a piece of the grid). In this // scenario, due to the multiple coordinate spaces in play, we use a // convenience flag to simply have the child's ReflowInput give it a // static position at its abs.pos. CB origin, and then we'll align & // offset it from there. nsIFrame* placeholder = aKidFrame->GetPlaceholderFrame(); if (placeholder && placeholder->GetParent() == aDelegatingFrame) { return true; } } if (aKidFrame->IsMenuPopupFrame()) { // Popups never use their static pos. return true; } // TODO(emilio): Either reparent the top layer placeholder frames to the // viewport, or return true here for top layer frames more generally (not // only menupopups), see https://github.com/w3c/csswg-drafts/issues/8040. return false; }(); if (staticPosIsCBOrigin) { initFlags += ReflowInput::InitFlag::StaticPosIsCBOrigin; } const bool kidFrameMaySplit = aReflowInput.AvailableBSize() != NS_UNCONSTRAINEDSIZE && // Don't split if told not to (e.g. for fixed frames) aFlags.contains(AbsPosReflowFlag::AllowFragmentation) && // TODO(Bug 2038072): Support splitting abspos frames under inline // absolute containing blocks. !aDelegatingFrame->IsInlineFrame() && // Bug 1588623: Support splitting absolute positioned multicol // containers. !aKidFrame->IsColumnSetWrapperFrame(); // Get the border values const LogicalMargin border = aDelegatingFrame->GetLogicalUsedBorder(outerWM).ApplySkipSides( aDelegatingFrame->PreReflowBlockLevelLogicalSkipSides()); const nsIFrame* kidPrevInFlow = aKidFrame->GetPrevInFlow(); const LogicalPoint* const unfragmentedPosition = GetUnfragmentedPosition(aReflowInput, aKidFrame); nscoord availBSize; if (kidFrameMaySplit) { if (unfragmentedPosition) { // The unfragmented position is relative to the absolute containing // block's first fragment, so we subtract // mCumulativeContainingBlockBSize to get the position in this fragment. const nscoord kidBPosInThisFragment = unfragmentedPosition->B(outerWM) - mCumulativeContainingBlockBSize; availBSize = aReflowInput.AvailableBSize() - kidBPosInThisFragment; NS_ASSERTION(availBSize >= 0, "Why is available block-size < 0?"); } else if (!aDelegatingFrame->GetPrevInFlow()) { // aDelegatingFrame is a first-in-flow. We subtract our containing // block's border-block-start, to consider the available space as // starting at the containing block's padding-edge. availBSize = aReflowInput.AvailableBSize() - border.BStart(outerWM); } else { // aDelegatingFrame is *not* a first-in-flow. Then we don't need to // subtract the containing block's border. Instead, we consider this // whole fragment as our available space, i.e., we allow abspos // continuations to overlap any border that their containing block // parent might have (including borders generated by // 'box-decoration-break:clone'). availBSize = aReflowInput.AvailableBSize(); } } else { availBSize = NS_UNCONSTRAINEDSIZE; } StyleSizeOverrides sizeOverrides; Maybe unfragmentedBSizeAsMinBSize; if (const auto* unfragmentedSize = GetUnfragmentedSize(aReflowInput, aKidFrame)) { // ReflowInput for fragmented absolute frames will not compute absolute // constraints - it'd be redundant anyway, so just use the unfragmented // size and skip it. auto resolutionParams = AnchorPosResolutionParams::From(aKidFrame, aAnchorPosResolutionCache); const auto* stylePos = aKidFrame->StylePosition(); if (stylePos->ISize(wm, resolutionParams)->IsAuto()) { sizeOverrides.mStyleISize.emplace( StyleSize::FromAppUnits(unfragmentedSize->ISize(wm))); } if (stylePos->BSize(wm, resolutionParams)->IsAuto()) { unfragmentedBSizeAsMinBSize = Some(unfragmentedSize->BSize(wm)); } } const LogicalSize availSize(outerWM, cbSize.ISize(outerWM), availBSize); ReflowInput kidReflowInput(aPresContext, aReflowInput, aKidFrame, availSize.ConvertTo(wm, outerWM), Some(cbSize.ConvertTo(wm, outerWM)), initFlags, sizeOverrides, {}, aAnchorPosResolutionCache); if (unfragmentedBSizeAsMinBSize) { // The kid has 'auto' block-size. Instead of setting unfragmented // block-size to sizeOverrides above, use it as a min-block-size lower // bound to keep allowing fragmentation-imposed block-size growth. const nscoord contentBSize = *unfragmentedBSizeAsMinBSize - (kidReflowInput.mStylePosition->mBoxSizing == StyleBoxSizing::BorderBox ? kidReflowInput.ComputedLogicalBorderPadding(wm).BStartEnd(wm) : 0); kidReflowInput.SetComputedMinBSize(contentBSize); } if (unfragmentedPosition) { // Do nothing. If aKidFrame may split, we've adjusted availBSize before // creating kidReflowInput. } else if (!kidPrevInFlow) { // ReflowInput's constructor may change the available block-size to // unconstrained, e.g. in orthogonal reflow, so we retrieve it again and // account for kid's constraints in its own writing-mode if needed. nscoord kidAvailBSize = kidReflowInput.AvailableBSize(); if (kidAvailBSize != NS_UNCONSTRAINEDSIZE) { kidAvailBSize -= kidReflowInput.ComputedLogicalMargin(wm).BStart(wm); nscoord kidOffsetBStart = kidReflowInput.ComputedLogicalOffsets(wm).BStart(wm); if (kidOffsetBStart != NS_AUTOOFFSET) { kidOffsetBStart -= mCumulativeContainingBlockBSize; kidAvailBSize -= kidOffsetBStart; } kidReflowInput.SetAvailableBSize(kidAvailBSize); } } // Do the reflow ReflowOutput kidDesiredSize(kidReflowInput); aKidFrame->Reflow(aPresContext, kidDesiredSize, kidReflowInput, aStatus); nsMargin insets; if (aKidFrame->IsMenuPopupFrame()) { // Do nothing. Popup frame will handle its own positioning. } else if (unfragmentedPosition || kidPrevInFlow) { // We can have reflows in a spanner that is also a multicol. const auto maybeFragmentedCbSize = (aFragmentedContainingBlockRects ? *aFragmentedContainingBlockRects : aContainingBlockRects) .mLocal.Size(); // TODO(dshin): Fix this up for anchor positioning. Scroll containers are // monolithic and will not fragment, but an anchor-positioned frame's // percentage size still needs to resolve against the correct containing // block. const LogicalSize unmodifiedCBSize(outerWM, maybeFragmentedCbSize); const nsSize cbBorderBoxSize = (unmodifiedCBSize + border.Size(outerWM)).GetPhysicalSize(outerWM); LogicalPoint kidPos(outerWM); if (unfragmentedPosition) { MOZ_ASSERT(!kidPrevInFlow, "aKidFrame should be a first-in-flow!"); // aKidFrame is a first-in-flow. Place it at its unfragmented position // with the block-start position adjusted. kidPos = *unfragmentedPosition; kidPos.B(outerWM) -= mCumulativeContainingBlockBSize; } else { // aKidFrame is a next-in-flow. Place it at the block-edge start of its // containing block, with the same inline-position as its prev-in-flow. kidPos = LogicalPoint( outerWM, kidPrevInFlow->IStart(outerWM, cbBorderBoxSize), 0); } const LogicalSize kidSize = kidDesiredSize.Size(outerWM); const LogicalRect kidRect(outerWM, kidPos, kidSize); aKidFrame->SetRect(outerWM, kidRect, cbBorderBoxSize); } else { // Position the child relative to our padding edge. const LogicalSize kidSize = kidDesiredSize.Size(outerWM); LogicalMargin offsets = kidReflowInput.ComputedLogicalOffsets(outerWM); LogicalMargin margin = kidReflowInput.ComputedLogicalMargin(outerWM); // If we're doing CSS Box Alignment in either axis, that will apply the // margin for us in that axis (since the thing that's aligned is the // margin box). So, we clear out the margin here to avoid applying it // twice. if (kidReflowInput.mFlags.mIOffsetsNeedCSSAlign) { margin.IStart(outerWM) = margin.IEnd(outerWM) = 0; } if (kidReflowInput.mFlags.mBOffsetsNeedCSSAlign) { margin.BStart(outerWM) = margin.BEnd(outerWM) = 0; } // If we're solving for start in either inline or block direction, // then compute it now that we know the dimensions. ResolveSizeDependentOffsets(kidReflowInput, cbSize, kidSize, margin, cb.GetAnchorOffsetInfo(), offsets); ResolveAutoMarginsAfterLayout(kidReflowInput, cbSize, kidSize, margin, offsets); // If the inset is constrained as non-auto, we may have a child that does // not fill out the inset-reduced containing block. In this case, we need // to align the child by its margin box: // https://drafts.csswg.org/css-position-3/#abspos-layout const auto* stylePos = aKidFrame->StylePosition(); const auto anchorResolutionParams = AnchorPosOffsetResolutionParams::ExplicitCBFrameSize( AnchorPosResolutionParams::From(aKidFrame, aAnchorPosResolutionCache), &cbSize); const bool iStartInsetAuto = stylePos ->GetAnchorResolvedInset(LogicalSide::IStart, outerWM, anchorResolutionParams) ->IsAuto(); const bool iEndInsetAuto = stylePos ->GetAnchorResolvedInset(LogicalSide::IEnd, outerWM, anchorResolutionParams) ->IsAuto(); const bool iInsetAuto = iStartInsetAuto || iEndInsetAuto; const bool bStartInsetAuto = stylePos ->GetAnchorResolvedInset(LogicalSide::BStart, outerWM, anchorResolutionParams) ->IsAuto(); const bool bEndInsetAuto = stylePos ->GetAnchorResolvedInset(LogicalSide::BEnd, outerWM, anchorResolutionParams) ->IsAuto(); const bool bInsetAuto = bStartInsetAuto || bEndInsetAuto; const LogicalSize kidMarginBox{ outerWM, margin.IStartEnd(outerWM) + kidSize.ISize(outerWM), margin.BStartEnd(outerWM) + kidSize.BSize(outerWM)}; const auto* placeholderContainer = GetPlaceholderContainer(kidReflowInput.mFrame); insets = [&]() { auto result = offsets; // Zero out weaker insets, if one exists - This offset gets forced to // the margin edge of the child on that side, and for the purposes of // overflow checks, we consider them to be zero. if (iStartInsetAuto && !iEndInsetAuto) { result.IStart(outerWM) = 0; } else if (iInsetAuto) { result.IEnd(outerWM) = 0; } if (bStartInsetAuto && !bEndInsetAuto) { result.BStart(outerWM) = 0; } else if (bInsetAuto) { result.BEnd(outerWM) = 0; } return result.GetPhysicalMargin(outerWM); }(); if (aAnchorPosResolutionCache) { aAnchorPosResolutionCache->mReferenceData->mInsets = insets; } if (!iInsetAuto) { MOZ_ASSERT( !kidReflowInput.mFlags.mIOffsetsNeedCSSAlign, "Non-auto inline inset but requires CSS alignment for static " "position?"); auto alignOffset = OffsetToAlignedStaticPos( kidReflowInput, kidMarginBox, cbSize, placeholderContainer, outerWM, LogicalAxis::Inline, Some(NonAutoAlignParams{ offsets.IStart(outerWM), offsets.IEnd(outerWM), }), cb.GetAnchorOffsetInfo()); offsets.IStart(outerWM) += alignOffset; offsets.IEnd(outerWM) = cbSize.ISize(outerWM) - (offsets.IStart(outerWM) + kidMarginBox.ISize(outerWM)); } if (!bInsetAuto) { MOZ_ASSERT(!kidReflowInput.mFlags.mBOffsetsNeedCSSAlign, "Non-auto block inset but requires CSS alignment for static " "position?"); auto alignOffset = OffsetToAlignedStaticPos( kidReflowInput, kidMarginBox, cbSize, placeholderContainer, outerWM, LogicalAxis::Block, Some(NonAutoAlignParams{ offsets.BStart(outerWM), offsets.BEnd(outerWM), }), cb.GetAnchorOffsetInfo()); offsets.BStart(outerWM) += alignOffset; offsets.BEnd(outerWM) = cbSize.BSize(outerWM) - (offsets.BStart(outerWM) + kidMarginBox.BSize(outerWM)); } LogicalRect rect( outerWM, offsets.StartOffset(outerWM) + margin.StartOffset(outerWM), kidSize); nsRect r = rect.GetPhysicalRect(outerWM, cbSize.GetPhysicalSize(outerWM)); // So far, we've positioned against the padding edge of the containing // block, which is necessary for inset computation. However, the position // of a frame originates against the border box. r += cb.mFinalRect.TopLeft(); const auto scrollShift = [&]() -> nsPoint { if (!aAnchorPosResolutionCache) { return {}; } auto* referenceData = aAnchorPosResolutionCache->mReferenceData; if (referenceData->CompensatingForScrollAxes().isEmpty()) { return {}; } if (cb.mAnchorOffsetInfo && earlyScrollCompensation == referenceData->CompensatingForScrollAxes()) { // Able to use the already-resolved value. return cb.mAnchorOffsetInfo->mScrollOffset; } return AnchorPositioningUtils::GetScrollOffsetFor( referenceData->CompensatingForScrollAxes(), aKidFrame, aAnchorPosResolutionCache->mDefaultAnchorCache); }(); if (aAnchorPosResolutionCache) { aAnchorPosResolutionCache->mReferenceData->mDefaultScrollShift = scrollShift; } r -= scrollShift; aKidFrame->SetRect(r); } aKidFrame->DidReflow(aPresContext, &kidReflowInput); if (!firstTryRect) { firstTryRect.emplace(aKidFrame->GetRect()); } const auto FitsInContainingBlock = [&]() { if (aAnchorPosResolutionCache) { return AnchorPositioningUtils::FitsInContainingBlock( aKidFrame, *aAnchorPosResolutionCache->mReferenceData); } auto imcbSize = cb.mFinalRect.Size(); imcbSize -= nsSize{insets.LeftRight(), insets.TopBottom()}; return aKidFrame->GetMarginRectRelativeToSelf().Size() <= imcbSize; }; // FIXME(bug 2004495): Per spec this should be the inset-modified // containing-block, see: // https://drafts.csswg.org/css-anchor-position-1/#fallback-apply const auto fits = aStatus.IsComplete() && FitsInContainingBlock(); if (fallbacks.IsEmpty() || finalizing || (fits && (tryOrder == StylePositionTryOrder::Normal || currentFallbackIndex == firstTryIndex))) { // We completed the reflow - Either we had a fallback that fit, or we // didn't have any to try in the first place. isOverflowingCB = !fits; fallback.CommitCurrentFallback(); if (currentFallbackIndex.isNothing()) { if (auto* prop = aKidFrame->GetProperty( nsIFrame::LastSuccessfulPositionFallback())) { // When the fallback list changes, we clear the recorded fallback data // as per spec, so we shouldn't get there in this case. MOZ_ASSERT(!fallbacks.IsEmpty(), "how?"); prop->mLastIndex.reset(); prop->mLastStyle = nullptr; prop->mTriedAllFallbacks = isOverflowingCB; } } break; } if (fits) { auto imcbSize = cb.mFinalRect.Size(); imcbSize -= nsSize{insets.LeftRight(), insets.TopBottom()}; switch (tryOrder) { case StylePositionTryOrder::MostWidth: if (imcbSize.Width() > bestSize) { bestSize = imcbSize.Width(); bestIndex = currentFallbackIndex; } break; case StylePositionTryOrder::MostHeight: if (imcbSize.Height() > bestSize) { bestSize = imcbSize.Height(); bestIndex = currentFallbackIndex; } break; default: MOZ_ASSERT_UNREACHABLE("unexpected try-order value"); break; } } if (!TryAdvanceFallback()) { // If there are no further fallbacks, we're done. if (bestSize >= 0) { SeekFallbackTo(bestIndex); } else { // If we're going to roll back to the first try position, and the // target's size was different, we need to do a "finalizing" reflow // to ensure the inner layout is correct. If the size is unchanged, // we can just break the fallback loop now. if (isOverflowingCB && firstTryRect && firstTryRect->Size() != aKidFrame->GetSize()) { SeekFallbackTo(firstTryIndex); } else { break; } } // The fallback we've just selected is the final choice, regardless of // whether it overflows. finalizing = true; } // Try with the next fallback. aKidFrame->AddStateBits(NS_FRAME_IS_DIRTY); aStatus.Reset(); } while (true); [&]() { if (!isOverflowingCB || !firstTryRect) { return; } // We gave up applying fallbacks. Recover previous values, if changed, and // reset currentFallbackIndex/Style to match. // Because we rolled back to first try data, our cache should be up-to-date. currentFallbackIndex = firstTryIndex; currentFallbackStyle = firstTryStyle; auto rect = *firstTryRect; if (isOverflowingCB && !aKidFrame->StylePosition()->mPositionArea.IsNone()) { // The anchored element overflows the IMCB of its position-area. Would it // have fit within the original CB? If so, shift it to stay within that. if (rect.width <= aContainingBlockRects.mLocal.width && rect.height <= aContainingBlockRects.mLocal.height) { if (rect.x < aContainingBlockRects.mLocal.x) { rect.x = aContainingBlockRects.mLocal.x; } else if (rect.XMost() > aContainingBlockRects.mLocal.XMost()) { rect.x = aContainingBlockRects.mLocal.XMost() - rect.width; } if (rect.y < aContainingBlockRects.mLocal.y) { rect.y = aContainingBlockRects.mLocal.y; } else if (rect.YMost() > aContainingBlockRects.mLocal.YMost()) { rect.y = aContainingBlockRects.mLocal.YMost() - rect.height; } } } if (rect.TopLeft() == aKidFrame->GetPosition()) { return; } aKidFrame->SetPosition(rect.TopLeft()); aKidFrame->UpdateOverflow(); }(); if (currentFallbackIndex) { auto* lastSuccessfulPosition = aKidFrame->GetOrCreateDeletableProperty( nsIFrame::LastSuccessfulPositionFallback()); // NOTE: We don't touch the last recorded index, that's done at resize // observer time. lastSuccessfulPosition->mLastIndex = currentFallbackIndex; lastSuccessfulPosition->mLastStyle = std::move(currentFallbackStyle); lastSuccessfulPosition->mTriedAllFallbacks = isOverflowingCB; } #ifdef DEBUG if (nsBlockFrame::gNoisyReflow) { nsIFrame::IndentBy(stdout, nsBlockFrame::gNoiseIndent - 1); fmt::println("abspos {}: rect {}", aKidFrame->ListTag().get(), ToString(aKidFrame->GetRect())); } #endif // If author asked for `position-visibility: no-overflow` and we overflow // `usedCB`, treat as "strongly hidden". Note that for anchored frames this // happens in ComputePositionVisibility. But no-overflow also applies to // non-anchored frames. if (!aAnchorPosResolutionCache) { aKidFrame->AddOrRemoveStateBits( NS_FRAME_POSITION_VISIBILITY_HIDDEN, isOverflowingCB && aKidFrame->StylePosition()->mPositionVisibility & StylePositionVisibility::NO_OVERFLOW); } if (aOverflowAreas) { aDelegatingFrame->ConsiderChildOverflow( *aOverflowAreas, aKidFrame, OverflowAreaUnionFlags::ChildIsAbsPos); } }