The pref was enabled by default in bug 2002115 and shipped in Firefox 151. Since it has been two releases, we can remove it to simplify the code path. Differential Revision: https://phabricator.services.mozilla.com/D302349
2182 lines
95 KiB
C++
2182 lines
95 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/. */
|
||
|
||
/*
|
||
* 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<nsIFrame*, 8> 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<bool> 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<const nsIFrame*> allFrames;
|
||
for (const nsFrameList* list : {&mAbsoluteFrames, &mPushedAbsoluteFrames}) {
|
||
for (const nsIFrame* child : *list) {
|
||
allFrames.Insert(child);
|
||
}
|
||
}
|
||
|
||
nsTHashSet<const nsIFrame*> 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<AnchorOffsetInfo> 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<AnchorPosInfo> {
|
||
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> 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<NonAutoAlignParams> 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<CSSAlignUtils::AnchorAlignInfo> 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<None, AnchorPosResolutionCache::PositionTryBackup,
|
||
AnchorPosResolutionCache::PositionTryFullBackup>;
|
||
|
||
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<ComputedStyle> 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<uint32_t> currentFallbackIndex;
|
||
const StylePositionTryFallbacksItem* currentFallback = nullptr;
|
||
RefPtr<ComputedStyle> currentFallbackStyle;
|
||
RefPtr<ComputedStyle> firstTryStyle;
|
||
Maybe<uint32_t> 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<uint32_t> 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<uint32_t> 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<ComputedStyle> 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<nsRect> 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<nscoord> 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);
|
||
}
|
||
}
|