/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* 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/. */ /* state used in reflow of block frames */ #include "BlockReflowState.h" #include #include "LayoutLogging.h" #include "nsBlockFrame.h" #include "nsLineLayout.h" #include "nsPresContext.h" #include "nsIFrameInlines.h" #include "mozilla/AutoRestore.h" #include "mozilla/DebugOnly.h" #include "mozilla/Preferences.h" #include "mozilla/StaticPrefs_layout.h" #include "TextOverflow.h" #ifdef DEBUG # include "nsBlockDebugFlags.h" #endif using namespace mozilla; using namespace mozilla::layout; BlockReflowState::BlockReflowState( const ReflowInput& aReflowInput, nsPresContext* aPresContext, nsBlockFrame* aFrame, bool aBStartMarginRoot, bool aBEndMarginRoot, bool aBlockNeedsFloatManager, const nscoord aConsumedBSize, const nscoord aEffectiveContentBoxBSize, const nscoord aInset) : mBlock(aFrame), mPresContext(aPresContext), mReflowInput(aReflowInput), mContentArea(aReflowInput.GetWritingMode()), mInsetForBalance(aInset), mPushedFloats(nullptr), mOverflowTracker(nullptr), mBorderPadding( mReflowInput .ComputedLogicalBorderPadding(mReflowInput.GetWritingMode()) .ApplySkipSides(aFrame->PreReflowBlockLevelLogicalSkipSides())), mMinLineHeight(aReflowInput.GetLineHeight()), mLineNumber(0), mTrailingClearFromPIF(StyleClear::None), mConsumedBSize(aConsumedBSize) { NS_ASSERTION(mConsumedBSize != NS_UNCONSTRAINEDSIZE, "The consumed block-size should be constrained!"); WritingMode wm = aReflowInput.GetWritingMode(); // Note that mContainerSize is the physical size, needed to // convert logical block-coordinates in vertical-rl writing mode // (measured from a RHS origin) to physical coordinates within the // containing block. // If aReflowInput doesn't have a constrained ComputedWidth(), we set // mContainerSize.width to zero, which means lines will be positioned // (physically) incorrectly; we will fix them up at the end of // nsBlockFrame::Reflow, after we know the total block-size of the // frame. mContainerSize.width = aReflowInput.ComputedWidth(); if (mContainerSize.width == NS_UNCONSTRAINEDSIZE) { mContainerSize.width = 0; } mContainerSize.width += mBorderPadding.LeftRight(wm); // For now at least, we don't do that fix-up for mContainerHeight. // It's only used in nsBidiUtils::ReorderFrames for vertical rtl // writing modes, which aren't fully supported for the time being. mContainerSize.height = aReflowInput.ComputedHeight() + mBorderPadding.TopBottom(wm); if (aBStartMarginRoot || 0 != mBorderPadding.BStart(wm)) { mFlags.mIsBStartMarginRoot = true; mFlags.mShouldApplyBStartMargin = true; } if (aBEndMarginRoot || 0 != mBorderPadding.BEnd(wm)) { mFlags.mIsBEndMarginRoot = true; } if (aBlockNeedsFloatManager) { mFlags.mBlockNeedsFloatManager = true; } mFlags.mCanHaveOverflowMarkers = css::TextOverflow::CanHaveOverflowMarkers( mBlock, css::TextOverflow::BeforeReflow::Yes); MOZ_ASSERT(FloatManager(), "Float manager should be valid when creating BlockReflowState!"); // Save the coordinate system origin for later. FloatManager()->GetTranslation(mFloatManagerI, mFloatManagerB); FloatManager()->PushState(&mFloatManagerStateBefore); // never popped mNextInFlow = static_cast(mBlock->GetNextInFlow()); LAYOUT_WARN_IF_FALSE(NS_UNCONSTRAINEDSIZE != aReflowInput.ComputedISize(), "have unconstrained width; this should only result " "from very large sizes, not attempts at intrinsic " "width calculation"); mContentArea.ISize(wm) = aReflowInput.ComputedISize(); // Compute content area block-size. Unlike the inline-size, if we have a // specified style block-size, we ignore it since extra content is managed by // the "overflow" property. When we don't have a specified style block-size, // then we may end up limiting our block-size if the available block-size is // constrained (this situation occurs when we are paginated). if (const nscoord availableBSize = aReflowInput.AvailableBSize(); availableBSize != NS_UNCONSTRAINEDSIZE) { // We are in a paginated situation. The block-end edge of the available // space to reflow the children is within our block-end border and padding. // If we're cloning our border and padding, and we're going to request // additional continuations because of our excessive content-box block-size, // then reserve some of our available space for our (cloned) block-end // border and padding. const bool reserveSpaceForBlockEndBP = mReflowInput.mStyleBorder->mBoxDecorationBreak == StyleBoxDecorationBreak::Clone && (aEffectiveContentBoxBSize == NS_UNCONSTRAINEDSIZE || aEffectiveContentBoxBSize + mBorderPadding.BStartEnd(wm) > availableBSize); const nscoord bp = reserveSpaceForBlockEndBP ? mBorderPadding.BStartEnd(wm) : mBorderPadding.BStart(wm); mContentArea.BSize(wm) = std::max(0, availableBSize - bp); } else { // When we are not in a paginated situation, then we always use a // unconstrained block-size. mContentArea.BSize(wm) = NS_UNCONSTRAINEDSIZE; } mContentArea.IStart(wm) = mBorderPadding.IStart(wm); mBCoord = mContentArea.BStart(wm) = mBorderPadding.BStart(wm); mPrevChild = nullptr; mCurrentLine = aFrame->LinesEnd(); } void BlockReflowState::ComputeFloatAvoidingOffsets( nsIFrame* aFloatAvoidingBlock, const LogicalRect& aFloatAvailableSpace, nscoord& aIStartResult, nscoord& aIEndResult) const { WritingMode wm = mReflowInput.GetWritingMode(); // The frame is clueless about the float manager and therefore we // only give it free space. An example is a table frame - the // tables do not flow around floats. // However, we can let its margins intersect floats. NS_ASSERTION(aFloatAvailableSpace.IStart(wm) >= mContentArea.IStart(wm), "bad avail space rect inline-coord"); NS_ASSERTION(aFloatAvailableSpace.ISize(wm) == 0 || aFloatAvailableSpace.IEnd(wm) <= mContentArea.IEnd(wm), "bad avail space rect inline-size"); nscoord iStartOffset, iEndOffset; if (aFloatAvailableSpace.ISize(wm) == mContentArea.ISize(wm)) { // We don't need to compute margins when there are no floats around. iStartOffset = 0; iEndOffset = 0; } else { const LogicalMargin frameMargin = SizeComputationInput(aFloatAvoidingBlock, mReflowInput.mRenderingContext, wm, mContentArea.ISize(wm)) .ComputedLogicalMargin(wm); nscoord iStartFloatIOffset = aFloatAvailableSpace.IStart(wm) - mContentArea.IStart(wm); iStartOffset = std::max(iStartFloatIOffset, frameMargin.IStart(wm)) - frameMargin.IStart(wm); iStartOffset = std::max(iStartOffset, 0); // in case of negative margin nscoord iEndFloatIOffset = mContentArea.IEnd(wm) - aFloatAvailableSpace.IEnd(wm); iEndOffset = std::max(iEndFloatIOffset, frameMargin.IEnd(wm)) - frameMargin.IEnd(wm); iEndOffset = std::max(iEndOffset, 0); // in case of negative margin } aIStartResult = iStartOffset; aIEndResult = iEndOffset; } LogicalRect BlockReflowState::ComputeBlockAvailSpace( nsIFrame* aFrame, const nsFlowAreaRect& aFloatAvailableSpace, bool aBlockAvoidsFloats) { #ifdef REALLY_NOISY_REFLOW printf("CBAS frame=%p has floats %d\n", aFrame, aFloatAvailableSpace.HasFloats()); #endif WritingMode wm = mReflowInput.GetWritingMode(); LogicalRect result(wm); result.BStart(wm) = mBCoord; // Note: ContentBSize() and ContentBEnd() are not our content-box size and its // block-end edge. They really mean "the available block-size for children", // and "the block-end edge of the available space for children". result.BSize(wm) = ContentBSize() == NS_UNCONSTRAINEDSIZE ? NS_UNCONSTRAINEDSIZE : ContentBEnd() - mBCoord; // mBCoord might be greater than ContentBEnd() if the block's top margin // pushes it off the page/column. Negative available block-size can confuse // other code and is nonsense in principle. // XXX Do we really want this condition to be this restrictive (i.e., // more restrictive than it used to be)? The |else| here is allowed // by the CSS spec, but only out of desperation given implementations, // and the behavior it leads to is quite undesirable (it can cause // things to become extremely narrow when they'd fit quite well a // little bit lower). Should the else be a quirk or something that // applies to a specific set of frame classes and no new ones? // If we did that, then for those frames where the condition below is // true but nsBlockFrame::BlockCanIntersectFloats is false, // nsBlockFrame::ISizeToClearPastFloats would need to use the // shrink-wrap formula, max(MinISize, min(avail width, PrefISize)) // rather than just using MinISize. NS_ASSERTION( nsBlockFrame::BlockCanIntersectFloats(aFrame) == !aBlockAvoidsFloats, "unexpected replaced width"); if (!aBlockAvoidsFloats) { if (aFloatAvailableSpace.HasFloats()) { // Use the float-edge property to determine how the child block // will interact with the float. const nsStyleBorder* borderStyle = aFrame->StyleBorder(); switch (borderStyle->mFloatEdge) { default: case StyleFloatEdge::ContentBox: // content and only content does // runaround of floats // The child block will flow around the float. Therefore // give it all of the available space. result.IStart(wm) = mContentArea.IStart(wm); result.ISize(wm) = mContentArea.ISize(wm); break; case StyleFloatEdge::MarginBox: { // The child block's margins should be placed adjacent to, // but not overlap the float. result.IStart(wm) = aFloatAvailableSpace.mRect.IStart(wm); result.ISize(wm) = aFloatAvailableSpace.mRect.ISize(wm); } break; } } else { // Since there are no floats present the float-edge property // doesn't matter therefore give the block element all of the // available space since it will flow around the float itself. result.IStart(wm) = mContentArea.IStart(wm); result.ISize(wm) = mContentArea.ISize(wm); } } else { nscoord iStartOffset, iEndOffset; ComputeFloatAvoidingOffsets(aFrame, aFloatAvailableSpace.mRect, iStartOffset, iEndOffset); result.IStart(wm) = mContentArea.IStart(wm) + iStartOffset; result.ISize(wm) = mContentArea.ISize(wm) - iStartOffset - iEndOffset; } #ifdef REALLY_NOISY_REFLOW printf(" CBAS: result %d %d %d %d\n", result.IStart(wm), result.BStart(wm), result.ISize(wm), result.BSize(wm)); #endif return result; } LogicalSize BlockReflowState::ComputeAvailableSizeForFloat() const { const auto wm = mReflowInput.GetWritingMode(); const nscoord availBSize = ContentBSize() == NS_UNCONSTRAINEDSIZE ? NS_UNCONSTRAINEDSIZE : std::max(0, ContentBEnd() - mBCoord); return LogicalSize(wm, ContentISize(), availBSize); } bool BlockReflowState::FloatAvoidingBlockFitsInAvailSpace( nsIFrame* aFloatAvoidingBlock, const nsFlowAreaRect& aFloatAvailableSpace) const { if (!aFloatAvailableSpace.HasFloats()) { // If there aren't any floats here, then we always fit. // We check this before calling ISizeToClearPastFloats, which is // somewhat expensive. return true; } // |aFloatAvailableSpace| was computed as having a negative size, which means // there are floats on both sides pushing inwards past each other, and // |aFloatAvoidingBlock| would necessarily intersect a float if we put it // here. So, it doesn't fit. if (aFloatAvailableSpace.ISizeIsActuallyNegative()) { return false; } WritingMode wm = mReflowInput.GetWritingMode(); nsBlockFrame::FloatAvoidingISizeToClear replacedISize = nsBlockFrame::ISizeToClearPastFloats(*this, aFloatAvailableSpace.mRect, aFloatAvoidingBlock); // The inline-start side of the replaced element should be offset by // the larger of the float intrusion or the replaced element's own // start margin. The inline-end side is similar, except for Web // compatibility we ignore the margin. return std::max( aFloatAvailableSpace.mRect.IStart(wm) - mContentArea.IStart(wm), replacedISize.marginIStart) + replacedISize.borderBoxISize + (mContentArea.IEnd(wm) - aFloatAvailableSpace.mRect.IEnd(wm)) <= mContentArea.ISize(wm); } nsFlowAreaRect BlockReflowState::GetFloatAvailableSpaceWithState( nscoord aBCoord, ShapeType aShapeType, nsFloatManager::SavedState* aState) const { WritingMode wm = mReflowInput.GetWritingMode(); #ifdef DEBUG // Verify that the caller setup the coordinate system properly nscoord wI, wB; FloatManager()->GetTranslation(wI, wB); NS_ASSERTION((wI == mFloatManagerI) && (wB == mFloatManagerB), "bad coord system"); #endif nscoord blockSize = (mContentArea.BSize(wm) == nscoord_MAX) ? nscoord_MAX : std::max(mContentArea.BEnd(wm) - aBCoord, 0); nsFlowAreaRect result = FloatManager()->GetFlowArea( wm, aBCoord, blockSize, BandInfoType::BandFromPoint, aShapeType, mContentArea, aState, ContainerSize()); // Keep the inline size >= 0 for compatibility with nsSpaceManager. if (result.mRect.ISize(wm) < 0) { result.mRect.ISize(wm) = 0; } #ifdef DEBUG if (nsBlockFrame::gNoisyReflow) { nsIFrame::IndentBy(stdout, nsBlockFrame::gNoiseIndent); printf("%s: band=%d,%d,%d,%d hasfloats=%d\n", __func__, result.mRect.IStart(wm), result.mRect.BStart(wm), result.mRect.ISize(wm), result.mRect.BSize(wm), result.HasFloats()); } #endif return result; } nsFlowAreaRect BlockReflowState::GetFloatAvailableSpaceForBSize( nscoord aBCoord, nscoord aBSize, nsFloatManager::SavedState* aState) const { WritingMode wm = mReflowInput.GetWritingMode(); #ifdef DEBUG // Verify that the caller setup the coordinate system properly nscoord wI, wB; FloatManager()->GetTranslation(wI, wB); NS_ASSERTION((wI == mFloatManagerI) && (wB == mFloatManagerB), "bad coord system"); #endif nsFlowAreaRect result = FloatManager()->GetFlowArea( wm, aBCoord, aBSize, BandInfoType::WidthWithinHeight, ShapeType::ShapeOutside, mContentArea, aState, ContainerSize()); // Keep the width >= 0 for compatibility with nsSpaceManager. if (result.mRect.ISize(wm) < 0) { result.mRect.ISize(wm) = 0; } #ifdef DEBUG if (nsBlockFrame::gNoisyReflow) { nsIFrame::IndentBy(stdout, nsBlockFrame::gNoiseIndent); printf("%s: space=%d,%d,%d,%d hasfloats=%d\n", __func__, result.mRect.IStart(wm), result.mRect.BStart(wm), result.mRect.ISize(wm), result.mRect.BSize(wm), result.HasFloats()); } #endif return result; } /* * Reconstruct the vertical margin before the line |aLine| in order to * do an incremental reflow that begins with |aLine| without reflowing * the line before it. |aLine| may point to the fencepost at the end of * the line list, and it is used this way since we (for now, anyway) * always need to recover margins at the end of a block. * * The reconstruction involves walking backward through the line list to * find any collapsed margins preceding the line that would have been in * the reflow input's |mPrevBEndMargin| when we reflowed that line in * a full reflow (under the rule in CSS2 that all adjacent vertical * margins of blocks collapse). */ void BlockReflowState::ReconstructMarginBefore(nsLineList::iterator aLine) { mPrevBEndMargin.Zero(); nsBlockFrame* block = mBlock; nsLineList::iterator firstLine = block->LinesBegin(); for (;;) { --aLine; if (aLine->IsBlock()) { mPrevBEndMargin = aLine->GetCarriedOutBEndMargin(); break; } if (!aLine->IsEmpty()) { break; } if (aLine == firstLine) { // If the top margin was carried out (and thus already applied), // set it to zero. Either way, we're done. if (!mFlags.mIsBStartMarginRoot) { mPrevBEndMargin.Zero(); } break; } } } void BlockReflowState::SetupPushedFloatList() { MOZ_ASSERT(!mFlags.mIsFloatListInBlockPropertyTable == !mPushedFloats, "flag mismatch"); if (!mFlags.mIsFloatListInBlockPropertyTable) { // If we're being re-Reflow'd without our next-in-flow having been // reflowed, some pushed floats from our previous reflow might // still be on our pushed floats list. However, that's // actually fine, since they'll all end up being stolen and // reordered into the correct order again. // (nsBlockFrame::ReflowDirtyLines ensures that any lines with // pushed floats are reflowed.) mPushedFloats = mBlock->EnsurePushedFloats(); mFlags.mIsFloatListInBlockPropertyTable = true; } } void BlockReflowState::AppendPushedFloatChain(nsIFrame* aFloatCont) { SetupPushedFloatList(); while (true) { aFloatCont->AddStateBits(NS_FRAME_IS_PUSHED_FLOAT); mPushedFloats->AppendFrame(mBlock, aFloatCont); aFloatCont = aFloatCont->GetNextInFlow(); if (!aFloatCont || aFloatCont->GetParent() != mBlock) { break; } mBlock->StealFrame(aFloatCont); } } /** * Restore information about floats into the float manager for an * incremental reflow, and simultaneously push the floats by * |aDeltaBCoord|, which is the amount |aLine| was pushed relative to its * parent. The recovery of state is one of the things that makes * incremental reflow O(N^2) and this state should really be kept * around, attached to the frame tree. */ void BlockReflowState::RecoverFloats(nsLineList::iterator aLine, nscoord aDeltaBCoord) { WritingMode wm = mReflowInput.GetWritingMode(); if (aLine->HasFloats()) { // Place the floats into the float manager again. Also slide // them, just like the regular frames on the line. for (nsIFrame* floatFrame : aLine->Floats()) { if (aDeltaBCoord != 0) { floatFrame->MovePositionBy(nsPoint(0, aDeltaBCoord)); nsContainerFrame::PositionFrameView(floatFrame); nsContainerFrame::PositionChildViews(floatFrame); } #ifdef DEBUG if (nsBlockFrame::gNoisyReflow || nsBlockFrame::gNoisyFloatManager) { nscoord tI, tB; FloatManager()->GetTranslation(tI, tB); nsIFrame::IndentBy(stdout, nsBlockFrame::gNoiseIndent); printf("RecoverFloats: tIB=%d,%d (%d,%d) ", tI, tB, mFloatManagerI, mFloatManagerB); floatFrame->ListTag(stdout); LogicalRect region = nsFloatManager::GetRegionFor(wm, floatFrame, ContainerSize()); printf(" aDeltaBCoord=%d region={%d,%d,%d,%d}\n", aDeltaBCoord, region.IStart(wm), region.BStart(wm), region.ISize(wm), region.BSize(wm)); } #endif FloatManager()->AddFloat( floatFrame, nsFloatManager::GetRegionFor(wm, floatFrame, ContainerSize()), wm, ContainerSize()); } } else if (aLine->IsBlock()) { nsBlockFrame::RecoverFloatsFor(aLine->mFirstChild, *FloatManager(), wm, ContainerSize()); } } /** * Everything done in this function is done O(N) times for each pass of * reflow so it is O(N*M) where M is the number of incremental reflow * passes. That's bad. Don't do stuff here. * * When this function is called, |aLine| has just been slid by |aDeltaBCoord| * and the purpose of RecoverStateFrom is to ensure that the * BlockReflowState is in the same state that it would have been in * had the line just been reflowed. * * Most of the state recovery that we have to do involves floats. */ void BlockReflowState::RecoverStateFrom(nsLineList::iterator aLine, nscoord aDeltaBCoord) { // Make the line being recovered the current line mCurrentLine = aLine; // Place floats for this line into the float manager if (aLine->HasFloats() || aLine->IsBlock()) { RecoverFloats(aLine, aDeltaBCoord); #ifdef DEBUG if (nsBlockFrame::gNoisyReflow || nsBlockFrame::gNoisyFloatManager) { FloatManager()->List(stdout); } #endif } } // This is called by the line layout's AddFloat method when a // place-holder frame is reflowed in a line. If the float is a // left-most child (it's x coordinate is at the line's left margin) // then the float is place immediately, otherwise the float // placement is deferred until the line has been reflowed. // XXXldb This behavior doesn't quite fit with CSS1 and CSS2 -- // technically we're supposed let the current line flow around the // float as well unless it won't fit next to what we already have. // But nobody else implements it that way... bool BlockReflowState::AddFloat(nsLineLayout* aLineLayout, nsIFrame* aFloat, nscoord aAvailableISize) { MOZ_ASSERT(aLineLayout, "must have line layout"); MOZ_ASSERT(mBlock->LinesEnd() != mCurrentLine, "null ptr"); MOZ_ASSERT(aFloat->HasAnyStateBits(NS_FRAME_OUT_OF_FLOW), "aFloat must be an out-of-flow frame"); MOZ_ASSERT(aFloat->GetParent(), "float must have parent"); MOZ_ASSERT(aFloat->GetParent()->IsBlockFrameOrSubclass(), "float's parent must be block"); if (aFloat->HasAnyStateBits(NS_FRAME_IS_PUSHED_FLOAT) || aFloat->GetParent() != mBlock) { MOZ_ASSERT(aFloat->HasAnyStateBits(NS_FRAME_IS_PUSHED_FLOAT | NS_FRAME_FIRST_REFLOW), "float should be in this block unless it was marked as " "pushed float, or just inserted"); MOZ_ASSERT(aFloat->GetParent()->FirstContinuation() == mBlock->FirstContinuation()); // If, in a previous reflow, the float was pushed entirely to // another column/page, we need to steal it back. (We might just // push it again, though.) Likewise, if that previous reflow // reflowed this block but not its next continuation, we might need // to steal it from our own float-continuations list. // // For more about pushed floats, see the comment above // nsBlockFrame::DrainPushedFloats. auto* floatParent = static_cast(aFloat->GetParent()); floatParent->StealFrame(aFloat); aFloat->RemoveStateBits(NS_FRAME_IS_PUSHED_FLOAT); // Appending is fine, since if a float was pushed to the next // page/column, all later floats were also pushed. mBlock->mFloats.AppendFrame(mBlock, aFloat); } // Because we are in the middle of reflowing a placeholder frame // within a line (and possibly nested in an inline frame or two // that's a child of our block) we need to restore the space // manager's translation to the space that the block resides in // before placing the float. nscoord oI, oB; FloatManager()->GetTranslation(oI, oB); nscoord dI = oI - mFloatManagerI; nscoord dB = oB - mFloatManagerB; FloatManager()->Translate(-dI, -dB); bool placed = false; // Now place the float immediately if possible. Otherwise stash it // away in mBelowCurrentLineFloats and place it later. // If one or more floats has already been pushed to the next line, // don't let this one go on the current line, since that would violate // float ordering. bool shouldPlaceFloatBelowCurrentLine = false; if (mBelowCurrentLineFloats.IsEmpty()) { // If the current line is empty, we don't impose any inline-size constraint // from the line layout. Maybe availableISizeInCurrentLine = aLineLayout->LineIsEmpty() ? Nothing() : Some(aAvailableISize); PlaceFloatResult result = FlowAndPlaceFloat(aFloat, availableISizeInCurrentLine); if (result == PlaceFloatResult::Placed) { placed = true; // Pass on updated available space to the current inline reflow engine WritingMode wm = mReflowInput.GetWritingMode(); // If we have mLineBSize, we are reflowing the line again due to // LineReflowStatus::RedoMoreFloats. We should use mLineBSize to query the // correct available space. nsFlowAreaRect floatAvailSpace = mLineBSize.isNothing() ? GetFloatAvailableSpace(mBCoord) : GetFloatAvailableSpaceForBSize( mBCoord, mLineBSize.value(), nullptr); LogicalRect availSpace(wm, floatAvailSpace.mRect.IStart(wm), mBCoord, floatAvailSpace.mRect.ISize(wm), floatAvailSpace.mRect.BSize(wm)); aLineLayout->UpdateBand(wm, availSpace, aFloat); // Record this float in the current-line list mCurrentLineFloats.AppendElement(aFloat); } else if (result == PlaceFloatResult::ShouldPlaceInNextContinuation) { (*aLineLayout->GetLine())->SetHadFloatPushed(); } else { MOZ_ASSERT(result == PlaceFloatResult::ShouldPlaceBelowCurrentLine); shouldPlaceFloatBelowCurrentLine = true; } } else { shouldPlaceFloatBelowCurrentLine = true; } if (shouldPlaceFloatBelowCurrentLine) { // Always claim to be placed; we don't know whether we fit yet, so we // deal with this in PlaceBelowCurrentLineFloats placed = true; // This float will be placed after the line is done (it is a // below-current-line float). mBelowCurrentLineFloats.AppendElement(aFloat); } // Restore coordinate system FloatManager()->Translate(dI, dB); return placed; } bool BlockReflowState::CanPlaceFloat( nscoord aFloatISize, const nsFlowAreaRect& aFloatAvailableSpace) { // A float fits at a given block-dir position if there are no floats // at its inline-dir position (no matter what its inline size) or if // its inline size fits in the space remaining after prior floats have // been placed. // FIXME: We should allow overflow by up to half a pixel here (bug 21193). return !aFloatAvailableSpace.HasFloats() || aFloatAvailableSpace.mRect.ISize(mReflowInput.GetWritingMode()) >= aFloatISize; } // Return the inline-size that the float (including margins) will take up // in the writing mode of the containing block. If this returns // NS_UNCONSTRAINEDSIZE, we're dealing with an orthogonal block that // has block-size:auto, and we'll need to actually reflow it to find out // how much inline-size it will occupy in the containing block's mode. static nscoord FloatMarginISize(WritingMode aCBWM, const ReflowInput& aFloatRI) { if (aFloatRI.ComputedSize(aCBWM).ISize(aCBWM) == NS_UNCONSTRAINEDSIZE) { return NS_UNCONSTRAINEDSIZE; // reflow is needed to get the true size } return aFloatRI.ComputedSizeWithMarginBorderPadding(aCBWM).ISize(aCBWM); } // A frame property that stores the last shape source / margin / etc. if there's // any shape, in order to invalidate the float area properly when it changes. // // TODO(emilio): This could really belong to GetRegionFor / StoreRegionFor, but // when I tried it was a bit awkward because of the logical -> physical // conversion that happens there. // // Maybe all this code could be refactored to make this cleaner, but keeping the // two properties separated was slightly nicer. struct ShapeInvalidationData { StyleShapeOutside mShapeOutside{StyleShapeOutside::None()}; float mShapeImageThreshold = 0.0; LengthPercentage mShapeMargin; ShapeInvalidationData() = default; explicit ShapeInvalidationData(const nsStyleDisplay& aDisplay) { Update(aDisplay); } static bool IsNeeded(const nsStyleDisplay& aDisplay) { return !aDisplay.mShapeOutside.IsNone(); } void Update(const nsStyleDisplay& aDisplay) { MOZ_ASSERT(IsNeeded(aDisplay)); mShapeOutside = aDisplay.mShapeOutside; mShapeImageThreshold = aDisplay.mShapeImageThreshold; mShapeMargin = aDisplay.mShapeMargin; } bool Matches(const nsStyleDisplay& aDisplay) const { return mShapeOutside == aDisplay.mShapeOutside && mShapeImageThreshold == aDisplay.mShapeImageThreshold && mShapeMargin == aDisplay.mShapeMargin; } }; NS_DECLARE_FRAME_PROPERTY_DELETABLE(ShapeInvalidationDataProperty, ShapeInvalidationData) BlockReflowState::PlaceFloatResult BlockReflowState::FlowAndPlaceFloat( nsIFrame* aFloat, Maybe aAvailableISizeInCurrentLine) { MOZ_ASSERT(aFloat->GetParent() == mBlock, "Float frame has wrong parent"); WritingMode wm = mReflowInput.GetWritingMode(); // Save away the block-dir coordinate before placing the float. We will // restore mBCoord at the end after placing the float. This is // necessary because any adjustments to mBCoord during the float // placement are for the float only, not for any non-floating // content. AutoRestore restoreBCoord(mBCoord); // Whether the block-direction position available to place a float has been // pushed down due to the presence of other floats. auto HasFloatPushedDown = [this, &restoreBCoord]() { return mBCoord != restoreBCoord.SavedValue(); }; // Grab the float's display information const nsStyleDisplay* floatDisplay = aFloat->StyleDisplay(); // The float's old region, so we can propagate damage. LogicalRect oldRegion = nsFloatManager::GetRegionFor(wm, aFloat, ContainerSize()); ShapeInvalidationData* invalidationData = aFloat->GetProperty(ShapeInvalidationDataProperty()); // Enforce CSS2 9.5.1 rule [2], i.e., make sure that a float isn't // ``above'' another float that preceded it in the flow. mBCoord = std::max(FloatManager()->LowestFloatBStart(), mBCoord); // See if the float should clear any preceding floats... // XXX We need to mark this float somehow so that it gets reflowed // when floats are inserted before it. if (StyleClear::None != floatDisplay->mClear) { // XXXldb Does this handle vertical margins correctly? auto [bCoord, result] = ClearFloats(mBCoord, floatDisplay->mClear); if (result == ClearFloatsResult::FloatsPushedOrSplit) { PushFloatPastBreak(aFloat); return PlaceFloatResult::ShouldPlaceInNextContinuation; } mBCoord = bCoord; } LogicalSize availSize = ComputeAvailableSizeForFloat(); const WritingMode floatWM = aFloat->GetWritingMode(); Maybe floatRI(std::in_place, mPresContext, mReflowInput, aFloat, availSize.ConvertTo(floatWM, wm)); nscoord floatMarginISize = FloatMarginISize(wm, *floatRI); LogicalMargin floatMargin = floatRI->ComputedLogicalMargin(wm); nsReflowStatus reflowStatus; // If it's a floating first-letter, we need to reflow it before we // know how wide it is (since we don't compute which letters are part // of the first letter until reflow!). // We also need to do this early reflow if FloatMarginISize returned // an unconstrained inline-size, which can occur if the float had an // orthogonal writing mode and 'auto' block-size (in its mode). bool earlyFloatReflow = aFloat->IsLetterFrame() || floatMarginISize == NS_UNCONSTRAINEDSIZE; if (earlyFloatReflow) { mBlock->ReflowFloat(*this, *floatRI, aFloat, reflowStatus); floatMarginISize = aFloat->ISize(wm) + floatMargin.IStartEnd(wm); NS_ASSERTION(reflowStatus.IsComplete(), "letter frames and orthogonal floats with auto block-size " "shouldn't break, and if they do now, then they're breaking " "at the wrong point"); } // Now we've computed the float's margin inline-size. if (aAvailableISizeInCurrentLine && floatMarginISize > *aAvailableISizeInCurrentLine) { // The float cannot fit in the available inline-size of the current line. // Let's notify our caller to place it later. return PlaceFloatResult::ShouldPlaceBelowCurrentLine; } // Find a place to place the float. The CSS2 spec doesn't want // floats overlapping each other or sticking out of the containing // block if possible (CSS2 spec section 9.5.1, see the rule list). StyleFloat floatStyle = floatDisplay->mFloat; MOZ_ASSERT(StyleFloat::Left == floatStyle || StyleFloat::Right == floatStyle, "Invalid float type!"); // Are we required to place at least part of the float because we're // at the top of the page (to avoid an infinite loop of pushing and // breaking). bool mustPlaceFloat = mReflowInput.mFlags.mIsTopOfPage && IsAdjacentWithBStart(); // Get the band of available space with respect to margin box. nsFlowAreaRect floatAvailableSpace = GetFloatAvailableSpaceForPlacingFloat(mBCoord); for (;;) { if (mReflowInput.AvailableBSize() != NS_UNCONSTRAINEDSIZE && floatAvailableSpace.mRect.BSize(wm) <= 0 && !mustPlaceFloat) { // No space, nowhere to put anything. PushFloatPastBreak(aFloat); return PlaceFloatResult::ShouldPlaceInNextContinuation; } if (CanPlaceFloat(floatMarginISize, floatAvailableSpace)) { // We found an appropriate place. break; } // Nope. try to advance to the next band. mBCoord += floatAvailableSpace.mRect.BSize(wm); floatAvailableSpace = GetFloatAvailableSpaceForPlacingFloat(mBCoord); mustPlaceFloat = false; } // If the float is continued, it will get the same absolute x value as its // prev-in-flow // We don't worry about the geometry of the prev in flow, let the continuation // place and size itself as required. // Assign inline and block dir coordinates to the float. We don't use // LineLeft() and LineRight() here, because we would only have to // convert the result back into this block's writing mode. LogicalPoint floatPos(wm); bool leftFloat = floatStyle == StyleFloat::Left; if (leftFloat == wm.IsBidiLTR()) { floatPos.I(wm) = floatAvailableSpace.mRect.IStart(wm); } else { floatPos.I(wm) = floatAvailableSpace.mRect.IEnd(wm) - floatMarginISize; } // CSS2 spec, 9.5.1 rule [4]: "A floating box's outer top may not // be higher than the top of its containing block." (Since the // containing block is the content edge of the block box, this // means the margin edge of the float can't be higher than the // content edge of the block that contains it.) floatPos.B(wm) = std::max(mBCoord, ContentBStart()); // Reflow the float after computing its vertical position so it knows // where to break. if (!earlyFloatReflow) { const LogicalSize oldAvailSize = availSize; availSize = ComputeAvailableSizeForFloat(); if (oldAvailSize != availSize) { floatRI.reset(); floatRI.emplace(mPresContext, mReflowInput, aFloat, availSize.ConvertTo(floatWM, wm)); } // Normally the mIsTopOfPage state is copied from the parent reflow input. // However, when reflowing a float, if we've placed other floats that force // this float being pushed down, we should unset the mIsTopOfPage bit. if (floatRI->mFlags.mIsTopOfPage && HasFloatPushedDown()) { // HasFloatPushedDown() implies that we increased mBCoord, and we // should've turned off mustPlaceFloat when we did that. NS_ASSERTION(!mustPlaceFloat, "mustPlaceFloat shouldn't be set if we're not at the " "top-of-page!"); floatRI->mFlags.mIsTopOfPage = false; } mBlock->ReflowFloat(*this, *floatRI, aFloat, reflowStatus); } if (aFloat->GetPrevInFlow()) { floatMargin.BStart(wm) = 0; } if (reflowStatus.IsIncomplete()) { floatMargin.BEnd(wm) = 0; } // If the float cannot fit (e.g. via fragmenting itself if applicable), or if // we're forced to break before it for CSS break-* reasons, then it needs to // be pushed in its entirety to the next column/page. // // Note we use the available block-size in floatRI rather than use // availSize.BSize() because nsBlockReflowContext::ReflowBlock() might adjust // floatRI's available size. const nscoord availBSize = floatRI->AvailableSize(floatWM).BSize(floatWM); const bool isTruncated = availBSize != NS_UNCONSTRAINEDSIZE && aFloat->BSize(floatWM) > availBSize; if ((!floatRI->mFlags.mIsTopOfPage && isTruncated) || reflowStatus.IsInlineBreakBefore()) { PushFloatPastBreak(aFloat); return PlaceFloatResult::ShouldPlaceInNextContinuation; } // We can't use aFloat->ShouldAvoidBreakInside(mReflowInput) here since // its mIsTopOfPage may be true even though the float isn't at the // top when floatPos.B(wm) > 0. if (ContentBSize() != NS_UNCONSTRAINEDSIZE && !mustPlaceFloat && (!mReflowInput.mFlags.mIsTopOfPage || floatPos.B(wm) > 0) && StyleBreakWithin::Avoid == aFloat->StyleDisplay()->mBreakInside && (!reflowStatus.IsFullyComplete() || aFloat->BSize(wm) + floatMargin.BStartEnd(wm) > ContentBEnd() - floatPos.B(wm)) && !aFloat->GetPrevInFlow()) { PushFloatPastBreak(aFloat); return PlaceFloatResult::ShouldPlaceInNextContinuation; } // Calculate the actual origin of the float frame's border rect // relative to the parent block; the margin must be added in // to get the border rect LogicalPoint origin(wm, floatMargin.IStart(wm) + floatPos.I(wm), floatMargin.BStart(wm) + floatPos.B(wm)); // If float is relatively positioned, factor that in as well const LogicalMargin floatOffsets = floatRI->ComputedLogicalOffsets(wm); ReflowInput::ApplyRelativePositioning(aFloat, wm, floatOffsets, &origin, ContainerSize()); // Position the float and make sure and views are properly // positioned. We need to explicitly position its child views as // well, since we're moving the float after flowing it. bool moved = aFloat->GetLogicalPosition(wm, ContainerSize()) != origin; if (moved) { aFloat->SetPosition(wm, origin, ContainerSize()); nsContainerFrame::PositionFrameView(aFloat); nsContainerFrame::PositionChildViews(aFloat); } // Update the float combined area state // XXX Floats should really just get invalidated here if necessary mFloatOverflowAreas.UnionWith(aFloat->GetOverflowAreasRelativeToParent()); // Place the float in the float manager // calculate region LogicalRect region = nsFloatManager::CalculateRegionFor( wm, aFloat, floatMargin, ContainerSize()); // if the float split, then take up all of the vertical height if (reflowStatus.IsIncomplete() && (NS_UNCONSTRAINEDSIZE != ContentBSize())) { region.BSize(wm) = std::max(region.BSize(wm), ContentBSize() - floatPos.B(wm)); } FloatManager()->AddFloat(aFloat, region, wm, ContainerSize()); // store region nsFloatManager::StoreRegionFor(wm, aFloat, region, ContainerSize()); const bool invalidationDataNeeded = ShapeInvalidationData::IsNeeded(*floatDisplay); // If the float's dimensions or shape have changed, note the damage in the // float manager. if (!region.IsEqualEdges(oldRegion) || !!invalidationData != invalidationDataNeeded || (invalidationData && !invalidationData->Matches(*floatDisplay))) { // XXXwaterson conservative: we could probably get away with noting // less damage; e.g., if only height has changed, then only note the // area into which the float has grown or from which the float has // shrunk. nscoord blockStart = std::min(region.BStart(wm), oldRegion.BStart(wm)); nscoord blockEnd = std::max(region.BEnd(wm), oldRegion.BEnd(wm)); FloatManager()->IncludeInDamage(blockStart, blockEnd); } if (invalidationDataNeeded) { if (invalidationData) { invalidationData->Update(*floatDisplay); } else { aFloat->SetProperty(ShapeInvalidationDataProperty(), new ShapeInvalidationData(*floatDisplay)); } } else if (invalidationData) { invalidationData = nullptr; aFloat->RemoveProperty(ShapeInvalidationDataProperty()); } if (!reflowStatus.IsFullyComplete()) { mBlock->SplitFloat(*this, aFloat, reflowStatus); } else { MOZ_ASSERT(!aFloat->GetNextInFlow()); } #ifdef DEBUG if (nsBlockFrame::gNoisyFloatManager) { nscoord tI, tB; FloatManager()->GetTranslation(tI, tB); mBlock->ListTag(stdout); printf(": FlowAndPlaceFloat: AddFloat: tIB=%d,%d (%d,%d) {%d,%d,%d,%d}\n", tI, tB, mFloatManagerI, mFloatManagerB, region.IStart(wm), region.BStart(wm), region.ISize(wm), region.BSize(wm)); } if (nsBlockFrame::gNoisyReflow) { nsRect r = aFloat->GetRect(); nsIFrame::IndentBy(stdout, nsBlockFrame::gNoiseIndent); printf("placed float: "); aFloat->ListTag(stdout); printf(" %d,%d,%d,%d\n", r.x, r.y, r.width, r.height); } #endif return PlaceFloatResult::Placed; } void BlockReflowState::PushFloatPastBreak(nsIFrame* aFloat) { // This ensures that we: // * don't try to place later but smaller floats (which CSS says // must have their tops below the top of this float) // * don't waste much time trying to reflow this float again until // after the break StyleFloat floatStyle = aFloat->StyleDisplay()->mFloat; if (floatStyle == StyleFloat::Left) { FloatManager()->SetPushedLeftFloatPastBreak(); } else { MOZ_ASSERT(floatStyle == StyleFloat::Right, "Unexpected float value!"); FloatManager()->SetPushedRightFloatPastBreak(); } // Put the float on the pushed floats list, even though it // isn't actually a continuation. mBlock->StealFrame(aFloat); AppendPushedFloatChain(aFloat); mReflowStatus.SetOverflowIncomplete(); } /** * Place below-current-line floats. */ void BlockReflowState::PlaceBelowCurrentLineFloats(nsLineBox* aLine) { MOZ_ASSERT(!mBelowCurrentLineFloats.IsEmpty()); nsTArray floatsPlacedInLine; for (nsIFrame* f : mBelowCurrentLineFloats) { #ifdef DEBUG if (nsBlockFrame::gNoisyReflow) { nsIFrame::IndentBy(stdout, nsBlockFrame::gNoiseIndent); printf("placing bcl float: "); f->ListTag(stdout); printf("\n"); } #endif // Place the float PlaceFloatResult result = FlowAndPlaceFloat(f); MOZ_ASSERT(result != PlaceFloatResult::ShouldPlaceBelowCurrentLine, "We are already dealing with below current line floats!"); if (result == PlaceFloatResult::Placed) { floatsPlacedInLine.AppendElement(f); } } if (floatsPlacedInLine.Length() != mBelowCurrentLineFloats.Length()) { // We have some floats having ShouldPlaceInNextContinuation result. aLine->SetHadFloatPushed(); } aLine->AppendFloats(std::move(floatsPlacedInLine)); mBelowCurrentLineFloats.Clear(); } std::tuple BlockReflowState::ClearFloats(nscoord aBCoord, StyleClear aClearType, nsIFrame* aFloatAvoidingBlock) { #ifdef DEBUG if (nsBlockFrame::gNoisyReflow) { nsIFrame::IndentBy(stdout, nsBlockFrame::gNoiseIndent); printf("clear floats: in: aBCoord=%d\n", aBCoord); } #endif if (!FloatManager()->HasAnyFloats()) { return {aBCoord, ClearFloatsResult::BCoordNoChange}; } nscoord newBCoord = aBCoord; if (aClearType != StyleClear::None) { newBCoord = FloatManager()->ClearFloats(newBCoord, aClearType); if (FloatManager()->ClearContinues(aClearType)) { return {newBCoord, ClearFloatsResult::FloatsPushedOrSplit}; } } if (aFloatAvoidingBlock) { for (;;) { nsFlowAreaRect floatAvailableSpace = GetFloatAvailableSpace(newBCoord); if (FloatAvoidingBlockFitsInAvailSpace(aFloatAvoidingBlock, floatAvailableSpace)) { break; } // See the analogous code for inlines in // nsBlockFrame::DoReflowInlineFrames if (!AdvanceToNextBand(floatAvailableSpace.mRect, &newBCoord)) { // Stop trying to clear here; we'll just get pushed to the // next column or page and try again there. break; } } } #ifdef DEBUG if (nsBlockFrame::gNoisyReflow) { nsIFrame::IndentBy(stdout, nsBlockFrame::gNoiseIndent); printf("clear floats: out: y=%d\n", newBCoord); } #endif ClearFloatsResult result = newBCoord == aBCoord ? ClearFloatsResult::BCoordNoChange : ClearFloatsResult::BCoordAdvanced; return {newBCoord, result}; }