diff options
Diffstat (limited to '')
-rw-r--r-- | layout/generic/nsColumnSetFrame.cpp | 1353 |
1 files changed, 1353 insertions, 0 deletions
diff --git a/layout/generic/nsColumnSetFrame.cpp b/layout/generic/nsColumnSetFrame.cpp new file mode 100644 index 0000000000..699afaece7 --- /dev/null +++ b/layout/generic/nsColumnSetFrame.cpp @@ -0,0 +1,1353 @@ +/* -*- 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/. */ + +/* rendering object for css3 multi-column layout */ + +#include "nsColumnSetFrame.h" + +#include "mozilla/ColumnUtils.h" +#include "mozilla/Logging.h" +#include "mozilla/PresShell.h" +#include "mozilla/StaticPrefs_layout.h" +#include "mozilla/ToString.h" +#include "nsCSSRendering.h" +#include "nsDisplayList.h" +#include "nsIFrameInlines.h" +#include "nsLayoutUtils.h" + +using namespace mozilla; +using namespace mozilla::layout; + +// To see this log, use $ MOZ_LOG=ColumnSet:4 ./mach run +static LazyLogModule sColumnSetLog("ColumnSet"); +#define COLUMN_SET_LOG(msg, ...) \ + MOZ_LOG(sColumnSetLog, LogLevel::Debug, (msg, ##__VA_ARGS__)) + +class nsDisplayColumnRule : public nsPaintedDisplayItem { + public: + nsDisplayColumnRule(nsDisplayListBuilder* aBuilder, nsIFrame* aFrame) + : nsPaintedDisplayItem(aBuilder, aFrame) { + MOZ_COUNT_CTOR(nsDisplayColumnRule); + } + MOZ_COUNTED_DTOR_OVERRIDE(nsDisplayColumnRule) + + nsRect GetBounds(nsDisplayListBuilder* aBuilder, bool* aSnap) const override { + *aSnap = false; + // We just return the frame's ink-overflow rect, which is guaranteed to + // contain all the column-rule areas. It's not worth calculating the exact + // union of those areas since it would only lead to performance improvements + // during painting in rare edge cases. + return mFrame->InkOverflowRect() + ToReferenceFrame(); + } + + bool CreateWebRenderCommands( + mozilla::wr::DisplayListBuilder& aBuilder, + mozilla::wr::IpcResourceUpdateQueue& aResources, + const StackingContextHelper& aSc, + mozilla::layers::RenderRootStateManager* aManager, + nsDisplayListBuilder* aDisplayListBuilder) override; + void Paint(nsDisplayListBuilder* aBuilder, gfxContext* aCtx) override; + + NS_DISPLAY_DECL_NAME("ColumnRule", TYPE_COLUMN_RULE); + + private: + nsTArray<nsCSSBorderRenderer> mBorderRenderers; +}; + +void nsDisplayColumnRule::Paint(nsDisplayListBuilder* aBuilder, + gfxContext* aCtx) { + static_cast<nsColumnSetFrame*>(mFrame)->CreateBorderRenderers( + mBorderRenderers, aCtx, GetPaintRect(aBuilder, aCtx), ToReferenceFrame()); + + for (auto iter = mBorderRenderers.begin(); iter != mBorderRenderers.end(); + iter++) { + iter->DrawBorders(); + } +} + +bool nsDisplayColumnRule::CreateWebRenderCommands( + mozilla::wr::DisplayListBuilder& aBuilder, + mozilla::wr::IpcResourceUpdateQueue& aResources, + const StackingContextHelper& aSc, + mozilla::layers::RenderRootStateManager* aManager, + nsDisplayListBuilder* aDisplayListBuilder) { + RefPtr dt = gfxPlatform::GetPlatform()->ScreenReferenceDrawTarget(); + if (!dt || !dt->IsValid()) { + return false; + } + gfxContext screenRefCtx(dt); + + bool dummy; + static_cast<nsColumnSetFrame*>(mFrame)->CreateBorderRenderers( + mBorderRenderers, &screenRefCtx, GetBounds(aDisplayListBuilder, &dummy), + ToReferenceFrame()); + + if (mBorderRenderers.IsEmpty()) { + return true; + } + + for (auto& renderer : mBorderRenderers) { + renderer.CreateWebRenderCommands(this, aBuilder, aResources, aSc); + } + + return true; +} + +/** + * Tracking issues: + * + * XXX cursor movement around the top and bottom of colums seems to make the + * editor lose the caret. + * + * XXX should we support CSS columns applied to table elements? + */ +nsContainerFrame* NS_NewColumnSetFrame(PresShell* aPresShell, + ComputedStyle* aStyle, + nsFrameState aStateFlags) { + nsColumnSetFrame* it = + new (aPresShell) nsColumnSetFrame(aStyle, aPresShell->GetPresContext()); + it->AddStateBits(aStateFlags); + return it; +} + +NS_IMPL_FRAMEARENA_HELPERS(nsColumnSetFrame) + +nsColumnSetFrame::nsColumnSetFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext) + : nsContainerFrame(aStyle, aPresContext, kClassID), + mLastBalanceBSize(NS_UNCONSTRAINEDSIZE) {} + +void nsColumnSetFrame::ForEachColumnRule( + const std::function<void(const nsRect& lineRect)>& aSetLineRect, + const nsPoint& aPt) const { + nsIFrame* child = mFrames.FirstChild(); + if (!child) return; // no columns + + nsIFrame* nextSibling = child->GetNextSibling(); + if (!nextSibling) return; // 1 column only - this means no gap to draw on + + const nsStyleColumn* colStyle = StyleColumn(); + nscoord ruleWidth = colStyle->GetColumnRuleWidth(); + if (!ruleWidth) return; + + WritingMode wm = GetWritingMode(); + bool isVertical = wm.IsVertical(); + bool isRTL = wm.IsBidiRTL(); + + nsRect contentRect = GetContentRectRelativeToSelf() + aPt; + nsSize ruleSize = isVertical ? nsSize(contentRect.width, ruleWidth) + : nsSize(ruleWidth, contentRect.height); + + while (nextSibling) { + // The frame tree goes RTL in RTL. + // The |prevFrame| and |nextFrame| frames here are the visually preceding + // (left/above) and following (right/below) frames, not in logical writing- + // mode direction. + nsIFrame* prevFrame = isRTL ? nextSibling : child; + nsIFrame* nextFrame = isRTL ? child : nextSibling; + + // Each child frame's position coordinates is actually relative to this + // nsColumnSetFrame. + // linePt will be at the top-left edge to paint the line. + nsPoint linePt; + if (isVertical) { + nscoord edgeOfPrev = prevFrame->GetRect().YMost() + aPt.y; + nscoord edgeOfNext = nextFrame->GetRect().Y() + aPt.y; + linePt = nsPoint(contentRect.x, + (edgeOfPrev + edgeOfNext - ruleSize.height) / 2); + } else { + nscoord edgeOfPrev = prevFrame->GetRect().XMost() + aPt.x; + nscoord edgeOfNext = nextFrame->GetRect().X() + aPt.x; + linePt = nsPoint((edgeOfPrev + edgeOfNext - ruleSize.width) / 2, + contentRect.y); + } + + aSetLineRect(nsRect(linePt, ruleSize)); + + child = nextSibling; + nextSibling = nextSibling->GetNextSibling(); + } +} + +void nsColumnSetFrame::CreateBorderRenderers( + nsTArray<nsCSSBorderRenderer>& aBorderRenderers, gfxContext* aCtx, + const nsRect& aDirtyRect, const nsPoint& aPt) { + WritingMode wm = GetWritingMode(); + bool isVertical = wm.IsVertical(); + const nsStyleColumn* colStyle = StyleColumn(); + StyleBorderStyle ruleStyle; + + // Per spec, inset => ridge and outset => groove + if (colStyle->mColumnRuleStyle == StyleBorderStyle::Inset) { + ruleStyle = StyleBorderStyle::Ridge; + } else if (colStyle->mColumnRuleStyle == StyleBorderStyle::Outset) { + ruleStyle = StyleBorderStyle::Groove; + } else { + ruleStyle = colStyle->mColumnRuleStyle; + } + + nscoord ruleWidth = colStyle->GetColumnRuleWidth(); + if (!ruleWidth) { + return; + } + + aBorderRenderers.Clear(); + nscolor ruleColor = + GetVisitedDependentColor(&nsStyleColumn::mColumnRuleColor); + + nsPresContext* pc = PresContext(); + // In order to re-use a large amount of code, we treat the column rule as a + // border. We create a new border style object and fill in all the details of + // the column rule as the left border. PaintBorder() does all the rendering + // for us, so we not only save an enormous amount of code but we'll support + // all the line styles that we support on borders! + nsStyleBorder border(*pc->Document()); + Sides skipSides; + if (isVertical) { + border.SetBorderWidth(eSideTop, ruleWidth, pc->AppUnitsPerDevPixel()); + border.SetBorderStyle(eSideTop, ruleStyle); + border.mBorderTopColor = StyleColor::FromColor(ruleColor); + skipSides |= mozilla::SideBits::eLeftRight; + skipSides |= mozilla::SideBits::eBottom; + } else { + border.SetBorderWidth(eSideLeft, ruleWidth, pc->AppUnitsPerDevPixel()); + border.SetBorderStyle(eSideLeft, ruleStyle); + border.mBorderLeftColor = StyleColor::FromColor(ruleColor); + skipSides |= mozilla::SideBits::eTopBottom; + skipSides |= mozilla::SideBits::eRight; + } + // If we use box-decoration-break: slice (the default), the border + // renderers will require clipping if we have continuations (see the + // aNeedsClip parameter to ConstructBorderRenderer in nsCSSRendering). + // + // Since it doesn't matter which box-decoration-break we use since + // we're only drawing borders (and not border-images), use 'clone'. + border.mBoxDecorationBreak = StyleBoxDecorationBreak::Clone; + + ForEachColumnRule( + [&](const nsRect& aLineRect) { + // Assert that we're not drawing a border-image here; if we were, we + // couldn't ignore the ImgDrawResult that PaintBorderWithStyleBorder + // returns. + MOZ_ASSERT(border.mBorderImageSource.IsNone()); + + gfx::DrawTarget* dt = aCtx ? aCtx->GetDrawTarget() : nullptr; + bool borderIsEmpty = false; + Maybe<nsCSSBorderRenderer> br = + nsCSSRendering::CreateBorderRendererWithStyleBorder( + pc, dt, this, aDirtyRect, aLineRect, border, Style(), + &borderIsEmpty, skipSides); + if (br.isSome()) { + MOZ_ASSERT(!borderIsEmpty); + aBorderRenderers.AppendElement(br.value()); + } + }, + aPt); +} + +static uint32_t ColumnBalancingDepth(const ReflowInput& aReflowInput, + uint32_t aMaxDepth) { + uint32_t depth = 0; + for (const ReflowInput* ri = aReflowInput.mParentReflowInput; + ri && depth < aMaxDepth; ri = ri->mParentReflowInput) { + if (ri->mFlags.mIsColumnBalancing) { + ++depth; + } + } + return depth; +} + +nsColumnSetFrame::ReflowConfig nsColumnSetFrame::ChooseColumnStrategy( + const ReflowInput& aReflowInput, bool aForceAuto = false) const { + const nsStyleColumn* colStyle = StyleColumn(); + nscoord availContentISize = aReflowInput.AvailableISize(); + if (aReflowInput.ComputedISize() != NS_UNCONSTRAINEDSIZE) { + availContentISize = aReflowInput.ComputedISize(); + } + + nscoord colBSize = aReflowInput.AvailableBSize(); + nscoord colGap = + ColumnUtils::GetColumnGap(this, aReflowInput.ComputedISize()); + int32_t numColumns = colStyle->mColumnCount; + + // If column-fill is set to 'balance' or we have a column-span sibling, then + // we want to balance the columns. + bool isBalancing = (colStyle->mColumnFill == StyleColumnFill::Balance || + HasColumnSpanSiblings()) && + !aForceAuto; + if (isBalancing) { + const uint32_t kMaxNestedColumnBalancingDepth = 2; + const uint32_t balancingDepth = + ColumnBalancingDepth(aReflowInput, kMaxNestedColumnBalancingDepth); + if (balancingDepth == kMaxNestedColumnBalancingDepth) { + isBalancing = false; + numColumns = 1; + } + } + + nscoord colISize; + // In vertical writing-mode, "column-width" (inline size) will actually be + // physical height, but its CSS name is still column-width. + if (colStyle->mColumnWidth.IsLength()) { + colISize = + ColumnUtils::ClampUsedColumnWidth(colStyle->mColumnWidth.AsLength()); + NS_ASSERTION(colISize >= 0, "negative column width"); + // Reduce column count if necessary to make columns fit in the + // available width. Compute max number of columns that fit in + // availContentISize, satisfying colGap*(maxColumns - 1) + + // colISize*maxColumns <= availContentISize + if (availContentISize != NS_UNCONSTRAINEDSIZE && colGap + colISize > 0 && + numColumns > 0) { + // This expression uses truncated rounding, which is what we + // want + int32_t maxColumns = + std::min(nscoord(nsStyleColumn::kMaxColumnCount), + (availContentISize + colGap) / (colGap + colISize)); + numColumns = std::max(1, std::min(numColumns, maxColumns)); + } + } else if (numColumns > 0 && availContentISize != NS_UNCONSTRAINEDSIZE) { + nscoord iSizeMinusGaps = availContentISize - colGap * (numColumns - 1); + colISize = iSizeMinusGaps / numColumns; + } else { + colISize = NS_UNCONSTRAINEDSIZE; + } + // Take care of the situation where there's only one column but it's + // still too wide + colISize = std::max(1, std::min(colISize, availContentISize)); + + nscoord expectedISizeLeftOver = 0; + + if (colISize != NS_UNCONSTRAINEDSIZE && + availContentISize != NS_UNCONSTRAINEDSIZE) { + // distribute leftover space + + // First, determine how many columns will be showing if the column + // count is auto + if (numColumns <= 0) { + // choose so that colGap*(nominalColumnCount - 1) + + // colISize*nominalColumnCount is nearly availContentISize + // make sure to round down + if (colGap + colISize > 0) { + numColumns = (availContentISize + colGap) / (colGap + colISize); + // The number of columns should never exceed kMaxColumnCount. + numColumns = + std::min(nscoord(nsStyleColumn::kMaxColumnCount), numColumns); + } + if (numColumns <= 0) { + numColumns = 1; + } + } + + // Compute extra space and divide it among the columns + nscoord extraSpace = + std::max(0, availContentISize - + (colISize * numColumns + colGap * (numColumns - 1))); + nscoord extraToColumns = extraSpace / numColumns; + colISize += extraToColumns; + expectedISizeLeftOver = extraSpace - (extraToColumns * numColumns); + } + + if (isBalancing) { + if (numColumns <= 0) { + // Hmm, auto column count, column width or available width is unknown, + // and balancing is required. Let's just use one column then. + numColumns = 1; + } + colBSize = std::min(mLastBalanceBSize, colBSize); + } else { + // CSS Fragmentation spec says, "To guarantee progress, fragmentainers are + // assumed to have a minimum block size of 1px regardless of their used + // size." https://drafts.csswg.org/css-break/#breaking-rules + // + // Note: we don't enforce the minimum block-size during balancing because + // this affects the result. If a balancing column container or its + // next-in-flows has zero block-size, it eventually gives up balancing, and + // ends up here. + colBSize = std::max(colBSize, nsPresContext::CSSPixelsToAppUnits(1)); + } + + ReflowConfig config; + config.mUsedColCount = numColumns; + config.mColISize = colISize; + config.mExpectedISizeLeftOver = expectedISizeLeftOver; + config.mColGap = colGap; + config.mColBSize = colBSize; + config.mIsBalancing = isBalancing; + config.mForceAuto = aForceAuto; + config.mKnownFeasibleBSize = NS_UNCONSTRAINEDSIZE; + config.mKnownInfeasibleBSize = 0; + + COLUMN_SET_LOG( + "%s: this=%p, mUsedColCount=%d, mColISize=%d, " + "mExpectedISizeLeftOver=%d, mColGap=%d, mColBSize=%d, mIsBalancing=%d", + __func__, this, config.mUsedColCount, config.mColISize, + config.mExpectedISizeLeftOver, config.mColGap, config.mColBSize, + config.mIsBalancing); + + return config; +} + +static void MarkPrincipalChildrenDirty(nsIFrame* aFrame) { + for (nsIFrame* childFrame : aFrame->PrincipalChildList()) { + childFrame->MarkSubtreeDirty(); + } +} + +static void MoveChildTo(nsIFrame* aChild, LogicalPoint aOrigin, WritingMode aWM, + const nsSize& aContainerSize) { + if (aChild->GetLogicalPosition(aWM, aContainerSize) == aOrigin) { + return; + } + + aChild->SetPosition(aWM, aOrigin, aContainerSize); + nsContainerFrame::PlaceFrameView(aChild); +} + +nscoord nsColumnSetFrame::GetMinISize(gfxContext* aRenderingContext) { + nscoord iSize = 0; + DISPLAY_MIN_INLINE_SIZE(this, iSize); + + if (mFrames.FirstChild()) { + // We want to ignore this in the case that we're size contained + // because our children should not contribute to our + // intrinsic size. + iSize = mFrames.FirstChild()->GetMinISize(aRenderingContext); + } + const nsStyleColumn* colStyle = StyleColumn(); + if (colStyle->mColumnWidth.IsLength()) { + nscoord colISize = + ColumnUtils::ClampUsedColumnWidth(colStyle->mColumnWidth.AsLength()); + // As available width reduces to zero, we reduce our number of columns + // to one, and don't enforce the column width, so just return the min + // of the child's min-width with any specified column width. + iSize = std::min(iSize, colISize); + } else { + NS_ASSERTION(colStyle->mColumnCount > 0, + "column-count and column-width can't both be auto"); + // As available width reduces to zero, we still have mColumnCount columns, + // so compute our minimum size based on the number of columns and their gaps + // and minimum per-column size. + nscoord colGap = ColumnUtils::GetColumnGap(this, NS_UNCONSTRAINEDSIZE); + iSize = ColumnUtils::IntrinsicISize(colStyle->mColumnCount, colGap, iSize); + } + // XXX count forced column breaks here? Maybe we should return the child's + // min-width times the minimum number of columns. + return iSize; +} + +nscoord nsColumnSetFrame::GetPrefISize(gfxContext* aRenderingContext) { + // Our preferred width is our desired column width, if specified, otherwise + // the child's preferred width, times the number of columns, plus the width + // of any required column gaps + // XXX what about forced column breaks here? + nscoord result = 0; + DISPLAY_PREF_INLINE_SIZE(this, result); + const nsStyleColumn* colStyle = StyleColumn(); + + nscoord colISize; + if (colStyle->mColumnWidth.IsLength()) { + colISize = + ColumnUtils::ClampUsedColumnWidth(colStyle->mColumnWidth.AsLength()); + } else if (mFrames.FirstChild()) { + // We want to ignore this in the case that we're size contained + // because our children should not contribute to our + // intrinsic size. + colISize = mFrames.FirstChild()->GetPrefISize(aRenderingContext); + } else { + colISize = 0; + } + + // If column-count is auto, assume one column. + uint32_t numColumns = + colStyle->mColumnCount == nsStyleColumn::kColumnCountAuto + ? 1 + : colStyle->mColumnCount; + nscoord colGap = ColumnUtils::GetColumnGap(this, NS_UNCONSTRAINEDSIZE); + result = ColumnUtils::IntrinsicISize(numColumns, colGap, colISize); + return result; +} + +nsColumnSetFrame::ColumnBalanceData nsColumnSetFrame::ReflowColumns( + ReflowOutput& aDesiredSize, const ReflowInput& aReflowInput, + nsReflowStatus& aStatus, const ReflowConfig& aConfig, + bool aUnboundedLastColumn) { + ColumnBalanceData colData; + bool allFit = true; + WritingMode wm = GetWritingMode(); + const bool isRTL = wm.IsBidiRTL(); + const bool shrinkingBSize = mLastBalanceBSize > aConfig.mColBSize; + const bool changingBSize = mLastBalanceBSize != aConfig.mColBSize; + + COLUMN_SET_LOG( + "%s: Doing column reflow pass: mLastBalanceBSize=%d," + " mColBSize=%d, RTL=%d, mUsedColCount=%d," + " mColISize=%d, mColGap=%d", + __func__, mLastBalanceBSize, aConfig.mColBSize, isRTL, + aConfig.mUsedColCount, aConfig.mColISize, aConfig.mColGap); + + DrainOverflowColumns(); + + if (changingBSize) { + mLastBalanceBSize = aConfig.mColBSize; + // XXX Seems like this could fire if incremental reflow pushed the column + // set down so we reflow incrementally with a different available height. + // We need a way to do an incremental reflow and be sure availableHeight + // changes are taken account of! Right now I think block frames with + // absolute children might exit early. + /* + NS_ASSERTION( + aKidReason != eReflowReason_Incremental, + "incremental reflow should not have changed the balance height"); + */ + } + + nsRect contentRect(0, 0, 0, 0); + OverflowAreas overflowRects; + + nsIFrame* child = mFrames.FirstChild(); + LogicalPoint childOrigin(wm, 0, 0); + + // In vertical-rl mode, columns will not be correctly placed if the + // reflowInput's ComputedWidth() is UNCONSTRAINED (in which case we'll get + // a containerSize.width of zero here). In that case, the column positions + // will be adjusted later, after our correct contentSize is known. + // + // When column-span is enabled, containerSize.width is always constrained. + // However, for RTL, we need to adjust the column positions as well after our + // correct containerSize is known. + nsSize containerSize = aReflowInput.ComputedSizeAsContainerIfConstrained(); + + const nscoord computedBSize = + aReflowInput.mParentReflowInput->ComputedBSize(); + nscoord contentBEnd = 0; + bool reflowNext = false; + + while (child) { + const bool reflowLastColumnWithUnconstrainedAvailBSize = + aUnboundedLastColumn && colData.mColCount == aConfig.mUsedColCount && + aConfig.mIsBalancing; + + // We need to reflow the child (column) ... + bool reflowChild = + // if we are told to do so; + aReflowInput.ShouldReflowAllKids() || + // if the child is dirty; + child->IsSubtreeDirty() || + // if it's the last child because we need to obtain the block-end + // margin; + !child->GetNextSibling() || + // if the next column is dirty, because the next column's first line(s) + // might be pullable back to this column; + child->GetNextSibling()->IsSubtreeDirty() || + // if this is the last column and we are supposed to assign unbounded + // block-size to it, because that could change the available block-size + // from the last time we reflowed it and we should try to pull all the + // content from its next sibling (Note that it might be the last column, + // but not be the last child because the desired number of columns has + // changed.) + reflowLastColumnWithUnconstrainedAvailBSize; + + // If column-fill is auto (not the default), then we might need to + // move content between columns for any change in column block-size. + // + // The same is true if we have a non-'auto' computed block-size. + // + // FIXME: It's not clear to me why it's *ever* valid to have + // reflowChild be false when changingBSize is true, since it + // seems like a child broken over multiple columns might need to + // change the size of the fragment in each column. + if (!reflowChild && changingBSize && + (StyleColumn()->mColumnFill == StyleColumnFill::Auto || + computedBSize != NS_UNCONSTRAINEDSIZE)) { + reflowChild = true; + } + // If we need to pull up content from the prev-in-flow then this is not just + // a block-size shrink. The prev in flow will have set the dirty bit. + // Check the overflow rect YMost instead of just the child's content + // block-size. The child may have overflowing content that cares about the + // available block-size boundary. (It may also have overflowing content that + // doesn't care about the available block-size boundary, but if so, too bad, + // this optimization is defeated.) We want scrollable overflow here since + // this is a calculation that affects layout. + if (!reflowChild && shrinkingBSize) { + switch (wm.GetBlockDir()) { + case WritingMode::eBlockTB: + if (child->ScrollableOverflowRect().YMost() > aConfig.mColBSize) { + reflowChild = true; + } + break; + case WritingMode::eBlockLR: + if (child->ScrollableOverflowRect().XMost() > aConfig.mColBSize) { + reflowChild = true; + } + break; + case WritingMode::eBlockRL: + // XXX not sure how to handle this, so for now just don't attempt + // the optimization + reflowChild = true; + break; + default: + MOZ_ASSERT_UNREACHABLE("unknown block direction"); + break; + } + } + + nscoord childContentBEnd = 0; + if (!reflowNext && !reflowChild) { + // This child does not need to be reflowed, but we may need to move it + MoveChildTo(child, childOrigin, wm, containerSize); + + // If this is the last frame then make sure we get the right status + nsIFrame* kidNext = child->GetNextSibling(); + if (kidNext) { + aStatus.Reset(); + if (kidNext->HasAnyStateBits(NS_FRAME_IS_OVERFLOW_CONTAINER)) { + aStatus.SetOverflowIncomplete(); + } else { + aStatus.SetIncomplete(); + } + } else { + aStatus = mLastFrameStatus; + } + childContentBEnd = nsLayoutUtils::CalculateContentBEnd(wm, child); + + COLUMN_SET_LOG("%s: Skipping child #%d %p: status=%s", __func__, + colData.mColCount, child, ToString(aStatus).c_str()); + } else { + LogicalSize availSize(wm, aConfig.mColISize, aConfig.mColBSize); + if (reflowLastColumnWithUnconstrainedAvailBSize) { + availSize.BSize(wm) = NS_UNCONSTRAINEDSIZE; + + COLUMN_SET_LOG( + "%s: Reflowing last column with unconstrained block-size. Change " + "available block-size from %d to %d", + __func__, aConfig.mColBSize, availSize.BSize(wm)); + } + + if (reflowNext) { + child->MarkSubtreeDirty(); + } + + LogicalSize kidCBSize(wm, availSize.ISize(wm), computedBSize); + ReflowInput kidReflowInput(PresContext(), aReflowInput, child, availSize, + Some(kidCBSize)); + kidReflowInput.mFlags.mIsTopOfPage = [&]() { + const bool isNestedMulticolOrPaginated = + aReflowInput.mParentReflowInput->mFrame->HasAnyStateBits( + NS_FRAME_HAS_MULTI_COLUMN_ANCESTOR) || + PresContext()->IsPaginated(); + if (isNestedMulticolOrPaginated) { + if (aConfig.mForceAuto) { + // If we are forced to fill columns sequentially, force fit the + // content whether we are at top of page or not. + return true; + } + if (aReflowInput.mFlags.mIsTopOfPage) { + // If this is the last balancing reflow, we want to force fit + // content to avoid infinite loops. + return !aConfig.mIsBalancing || aConfig.mIsLastBalancingReflow; + } + // If we are a not at the top of page, we shouldn't force fit content. + // This is because our ColumnSetWrapperFrame can be pushed to the next + // column or page and reflowed again with a potentially larger + // available block-size. + return false; + } + // We are a top-level multicol in non-paginated context. Force fit the + // content only if we are not balancing columns. + return !aConfig.mIsBalancing; + }(); + kidReflowInput.mFlags.mTableIsSplittable = false; + kidReflowInput.mFlags.mIsColumnBalancing = aConfig.mIsBalancing; + kidReflowInput.mBreakType = ReflowInput::BreakType::Column; + + // We need to reflow any float placeholders, even if our column block-size + // hasn't changed. + kidReflowInput.mFlags.mMustReflowPlaceholders = !changingBSize; + + COLUMN_SET_LOG( + "%s: Reflowing child #%d %p: availSize=(%d,%d), kidCBSize=(%d,%d), " + "child's mIsTopOfPage=%d", + __func__, colData.mColCount, child, availSize.ISize(wm), + availSize.BSize(wm), kidCBSize.ISize(wm), kidCBSize.BSize(wm), + kidReflowInput.mFlags.mIsTopOfPage); + + // Note if the column's next in flow is not being changed by this + // incremental reflow. This may allow the current column to avoid trying + // to pull lines from the next column. + if (child->GetNextSibling() && !HasAnyStateBits(NS_FRAME_IS_DIRTY) && + !child->GetNextSibling()->HasAnyStateBits(NS_FRAME_IS_DIRTY)) { + kidReflowInput.mFlags.mNextInFlowUntouched = true; + } + + ReflowOutput kidDesiredSize(wm); + + // XXX it would be cool to consult the float manager for the + // previous block to figure out the region of floats from the + // previous column that extend into this column, and subtract + // that region from the new float manager. So you could stick a + // really big float in the first column and text in following + // columns would flow around it. + + MOZ_ASSERT(kidReflowInput.ComputedLogicalMargin(wm).IsAllZero(), + "-moz-column-content has no margin!"); + aStatus.Reset(); + ReflowChild(child, PresContext(), kidDesiredSize, kidReflowInput, wm, + childOrigin, containerSize, ReflowChildFlags::Default, + aStatus); + + if (colData.mColCount == 1 && aStatus.IsInlineBreakBefore()) { + COLUMN_SET_LOG("%s: Content in the first column reports break-before!", + __func__); + allFit = false; + break; + } + + reflowNext = aStatus.NextInFlowNeedsReflow(); + + // The carried-out block-end margin of column content might be non-zero + // when we try to find the best column balancing block size, but it should + // never affect the size column set nor be further carried out. Set it to + // zero. + // + // FIXME: For some types of fragmentation, we should carry the margin into + // the next column. Also see + // https://drafts.csswg.org/css-break-4/#break-margins + // + // FIXME: This should never happen for the last column, since it should be + // a margin root; see nsBlockFrame::IsMarginRoot(). However, sometimes the + // last column has an empty continuation while searching for the best + // column balancing bsize, which prevents the last column from being a + // margin root. + kidDesiredSize.mCarriedOutBEndMargin.Zero(); + + NS_FRAME_TRACE_REFLOW_OUT("Column::Reflow", aStatus); + + FinishReflowChild(child, PresContext(), kidDesiredSize, &kidReflowInput, + wm, childOrigin, containerSize, + ReflowChildFlags::Default); + + childContentBEnd = nsLayoutUtils::CalculateContentBEnd(wm, child); + if (childContentBEnd > aConfig.mColBSize) { + allFit = false; + } + if (childContentBEnd > availSize.BSize(wm)) { + colData.mMaxOverflowingBSize = + std::max(childContentBEnd, colData.mMaxOverflowingBSize); + } + + COLUMN_SET_LOG( + "%s: Reflowed child #%d %p: status=%s, desiredSize=(%d,%d), " + "childContentBEnd=%d, CarriedOutBEndMargin=%d (ignored)", + __func__, colData.mColCount, child, ToString(aStatus).c_str(), + kidDesiredSize.ISize(wm), kidDesiredSize.BSize(wm), childContentBEnd, + kidDesiredSize.mCarriedOutBEndMargin.get()); + } + + contentRect.UnionRect(contentRect, child->GetRect()); + + ConsiderChildOverflow(overflowRects, child); + contentBEnd = std::max(contentBEnd, childContentBEnd); + colData.mLastBSize = childContentBEnd; + colData.mSumBSize += childContentBEnd; + + // Build a continuation column if necessary + nsIFrame* kidNextInFlow = child->GetNextInFlow(); + + if (aStatus.IsFullyComplete()) { + NS_ASSERTION(!kidNextInFlow, "next in flow should have been deleted"); + child = nullptr; + break; + } + + // Make sure that the column has a next-in-flow. If not, we must + // create one to hold the overflowing stuff, even if we're just + // going to put it on our overflow list and let *our* + // next in flow handle it. + if (!kidNextInFlow) { + NS_ASSERTION(aStatus.NextInFlowNeedsReflow(), + "We have to create a continuation, but the block doesn't " + "want us to reflow it?"); + + // We need to create a continuing column + kidNextInFlow = CreateNextInFlow(child); + } + + // Make sure we reflow a next-in-flow when it switches between being + // normal or overflow container + if (aStatus.IsOverflowIncomplete()) { + if (!kidNextInFlow->HasAnyStateBits(NS_FRAME_IS_OVERFLOW_CONTAINER)) { + aStatus.SetNextInFlowNeedsReflow(); + reflowNext = true; + kidNextInFlow->AddStateBits(NS_FRAME_IS_OVERFLOW_CONTAINER); + } + } else if (kidNextInFlow->HasAnyStateBits(NS_FRAME_IS_OVERFLOW_CONTAINER)) { + aStatus.SetNextInFlowNeedsReflow(); + reflowNext = true; + kidNextInFlow->RemoveStateBits(NS_FRAME_IS_OVERFLOW_CONTAINER); + } + + // We have reached the maximum number of columns. If we are balancing, stop + // this reflow and continue finding the optimal balancing block-size. + // + // Otherwise, i.e. we are not balancing, stop this reflow and let the parent + // of our multicol container create a next-in-flow if all of the following + // conditions are met. + // + // 1) We fill columns sequentially by the request of the style, not by our + // internal needs, i.e. aConfig.mForceAuto is false. + // + // We don't want to stop this reflow when we force fill the columns + // sequentially. We usually go into this mode when giving up balancing, and + // this is the last resort to fit all our children by creating overflow + // columns. + // + // 2) In a fragmented context, our multicol container still has block-size + // left for its next-in-flow, i.e. + // aReflowInput.mFlags.mColumnSetWrapperHasNoBSizeLeft is false. + // + // Note that in a continuous context, i.e. our multicol container's + // available block-size is unconstrained, if it has a fixed block-size + // mColumnSetWrapperHasNoBSizeLeft is always true because nothing stops it + // from applying all its block-size in the first-in-flow. Otherwise, i.e. + // our multicol container has an unconstrained block-size, we shouldn't be + // here because all our children should fit in the very first column even if + // mColumnSetWrapperHasNoBSizeLeft is false. + // + // According to the definition of mColumnSetWrapperHasNoBSizeLeft, if the + // bit is *not* set, either our multicol container has unconstrained + // block-size, or it has a constrained block-size and has block-size left + // for its next-in-flow. In either cases, the parent of our multicol + // container can create a next-in-flow for the container that guaranteed to + // have non-zero block-size for the container's children. + // + // Put simply, if either one of the above conditions is not met, we are + // going to create more overflow columns until all our children are fit. + if (colData.mColCount >= aConfig.mUsedColCount && + (aConfig.mIsBalancing || + (!aConfig.mForceAuto && + !aReflowInput.mFlags.mColumnSetWrapperHasNoBSizeLeft))) { + NS_ASSERTION(aConfig.mIsBalancing || + aReflowInput.AvailableBSize() != NS_UNCONSTRAINEDSIZE, + "Why are we here if we have unlimited block-size to fill " + "columns sequentially."); + + // No more columns allowed here. Stop. + aStatus.SetNextInFlowNeedsReflow(); + kidNextInFlow->MarkSubtreeDirty(); + // Move any of our leftover columns to our overflow list. Our + // next-in-flow will eventually pick them up. + nsFrameList continuationColumns = mFrames.TakeFramesAfter(child); + if (continuationColumns.NotEmpty()) { + SetOverflowFrames(std::move(continuationColumns)); + } + child = nullptr; + + COLUMN_SET_LOG("%s: We are not going to create overflow columns.", + __func__); + break; + } + + if (PresContext()->HasPendingInterrupt()) { + // Stop the loop now while |child| still points to the frame that bailed + // out. We could keep going here and condition a bunch of the code in + // this loop on whether there's an interrupt, or even just keep going and + // trying to reflow the blocks (even though we know they'll interrupt + // right after their first line), but stopping now is conceptually the + // simplest (and probably fastest) thing. + break; + } + + // Advance to the next column + child = child->GetNextSibling(); + ++colData.mColCount; + + if (child) { + childOrigin.I(wm) += aConfig.mColISize + aConfig.mColGap; + + COLUMN_SET_LOG("%s: Next childOrigin.iCoord=%d", __func__, + childOrigin.I(wm)); + } + } + + if (PresContext()->CheckForInterrupt(this) && + HasAnyStateBits(NS_FRAME_IS_DIRTY)) { + // Mark all our kids starting with |child| dirty + + // Note that this is a CheckForInterrupt call, not a HasPendingInterrupt, + // because we might have interrupted while reflowing |child|, and since + // we're about to add a dirty bit to |child| we need to make sure that + // |this| is scheduled to have dirty bits marked on it and its ancestors. + // Otherwise, when we go to mark dirty bits on |child|'s ancestors we'll + // bail out immediately, since it'll already have a dirty bit. + for (; child; child = child->GetNextSibling()) { + child->MarkSubtreeDirty(); + } + } + + colData.mMaxBSize = contentBEnd; + LogicalSize contentSize = LogicalSize(wm, contentRect.Size()); + contentSize.BSize(wm) = std::max(contentSize.BSize(wm), contentBEnd); + mLastFrameStatus = aStatus; + + if (computedBSize != NS_UNCONSTRAINEDSIZE && !HasColumnSpanSiblings()) { + NS_ASSERTION(aReflowInput.AvailableBSize() != NS_UNCONSTRAINEDSIZE, + "Available block-size should be constrained because it's " + "restricted by the computed block-size when our reflow " + "input is created in nsBlockFrame::ReflowBlockFrame()!"); + + // If a) our parent ColumnSetWrapper has constrained block-size + // (nsBlockFrame::ReflowBlockFrame() applies the block-size constraint + // when creating a ReflowInput for ColumnSetFrame child); and b) we are the + // sole ColumnSet or the last ColumnSet continuation split by column-spans + // in a ColumnSetWrapper, extend our block-size to consume the available + // block-size so that the column-rules are drawn to the content block-end + // edge of the multicol container. + contentSize.BSize(wm) = + std::max(contentSize.BSize(wm), aReflowInput.AvailableBSize()); + } + + aDesiredSize.SetSize(wm, contentSize); + aDesiredSize.mOverflowAreas = overflowRects; + aDesiredSize.UnionOverflowAreasWithDesiredBounds(); + + // In vertical-rl mode, make a second pass if necessary to reposition the + // columns with the correct container width. (In other writing modes, + // correct containerSize was not required for column positioning so we don't + // need this fixup.) + // + // RTL column positions also depend on ColumnSet's actual contentSize. We need + // this fixup, too. + if ((wm.IsVerticalRL() || isRTL) && + containerSize.width != contentSize.Width(wm)) { + const nsSize finalContainerSize = aDesiredSize.PhysicalSize(); + OverflowAreas overflowRects; + for (nsIFrame* child : mFrames) { + // Get the logical position as set previously using a provisional or + // dummy containerSize, and reset with the correct container size. + child->SetPosition(wm, child->GetLogicalPosition(wm, containerSize), + finalContainerSize); + ConsiderChildOverflow(overflowRects, child); + } + aDesiredSize.mOverflowAreas = overflowRects; + aDesiredSize.UnionOverflowAreasWithDesiredBounds(); + } + + colData.mFeasible = allFit && aStatus.IsFullyComplete(); + + COLUMN_SET_LOG( + "%s: Done column reflow pass: %s, mMaxBSize=%d, mSumBSize=%d, " + "mLastBSize=%d, mMaxOverflowingBSize=%d", + __func__, colData.mFeasible ? "Feasible :)" : "Infeasible :(", + colData.mMaxBSize, colData.mSumBSize, colData.mLastBSize, + colData.mMaxOverflowingBSize); + + return colData; +} + +void nsColumnSetFrame::DrainOverflowColumns() { + // First grab the prev-in-flows overflows and reparent them to this + // frame. + nsPresContext* presContext = PresContext(); + nsColumnSetFrame* prev = static_cast<nsColumnSetFrame*>(GetPrevInFlow()); + if (prev) { + AutoFrameListPtr overflows(presContext, prev->StealOverflowFrames()); + if (overflows) { + nsContainerFrame::ReparentFrameViewList(*overflows, prev, this); + + mFrames.InsertFrames(this, nullptr, std::move(*overflows)); + } + } + + // Now pull back our own overflows and append them to our children. + // We don't need to reparent them since we're already their parent. + AutoFrameListPtr overflows(presContext, StealOverflowFrames()); + if (overflows) { + // We're already the parent for these frames, so no need to set + // their parent again. + mFrames.AppendFrames(nullptr, std::move(*overflows)); + } +} + +void nsColumnSetFrame::FindBestBalanceBSize(const ReflowInput& aReflowInput, + nsPresContext* aPresContext, + ReflowConfig& aConfig, + ColumnBalanceData aColData, + ReflowOutput& aDesiredSize, + bool aUnboundedLastColumn, + nsReflowStatus& aStatus) { + MOZ_ASSERT(aConfig.mIsBalancing, + "Why are we here if we are not balancing columns?"); + + const nscoord availableContentBSize = aReflowInput.AvailableBSize(); + + // Termination of the algorithm below is guaranteed because + // aConfig.knownFeasibleBSize - aConfig.knownInfeasibleBSize decreases in + // every iteration. + int32_t iterationCount = 1; + + // We set this flag when we detect that we may contain a frame + // that can break anywhere (thus foiling the linear decrease-by-one + // search) + bool maybeContinuousBreakingDetected = false; + bool possibleOptimalBSizeDetected = false; + + // This is the extra block-size added to the optimal column block-size + // estimation which is calculated in the while-loop by dividing + // aColData.mSumBSize into N columns. + // + // The constant is arbitrary. We use a half of line-height first. In case a + // column container uses *zero* (or a very small) line-height, use a half of + // default line-height 1140/2 = 570 app units as the minimum value. Otherwise + // we might take more than necessary iterations before finding a feasible + // block-size. + nscoord extraBlockSize = std::max(570, aReflowInput.GetLineHeight() / 2); + + // We use divide-by-N to estimate the optimal column block-size only if the + // last column's available block-size is unbounded. + bool foundFeasibleBSizeCloserToBest = !aUnboundedLastColumn; + + // Stop the binary search when the difference of the feasible and infeasible + // block-size is within this gap. Here we use one device pixel. + const int32_t gapToStop = aPresContext->DevPixelsToAppUnits(1); + + while (!aPresContext->HasPendingInterrupt()) { + nscoord lastKnownFeasibleBSize = aConfig.mKnownFeasibleBSize; + + // Record what we learned from the last reflow + if (aColData.mFeasible) { + // mMaxBSize is feasible. Also, mLastBalanceBSize is feasible. + aConfig.mKnownFeasibleBSize = + std::min(aConfig.mKnownFeasibleBSize, aColData.mMaxBSize); + aConfig.mKnownFeasibleBSize = + std::min(aConfig.mKnownFeasibleBSize, mLastBalanceBSize); + + // Furthermore, no block-size less than the block-size of the last + // column can ever be feasible. (We might be able to reduce the + // block-size of a non-last column by moving content to a later column, + // but we can't do that with the last column.) + if (aColData.mColCount == aConfig.mUsedColCount) { + aConfig.mKnownInfeasibleBSize = + std::max(aConfig.mKnownInfeasibleBSize, aColData.mLastBSize - 1); + } + } else { + aConfig.mKnownInfeasibleBSize = + std::max(aConfig.mKnownInfeasibleBSize, mLastBalanceBSize); + + // If a column didn't fit in its available block-size, then its current + // block-size must be the minimum block-size for unbreakable content in + // the column, and therefore no smaller block-size can be feasible. + aConfig.mKnownInfeasibleBSize = std::max( + aConfig.mKnownInfeasibleBSize, aColData.mMaxOverflowingBSize - 1); + + if (aUnboundedLastColumn) { + // The last column is unbounded, so all content got reflowed, so the + // mMaxBSize is feasible. + aConfig.mKnownFeasibleBSize = + std::min(aConfig.mKnownFeasibleBSize, aColData.mMaxBSize); + + NS_ASSERTION(mLastFrameStatus.IsComplete(), + "Last column should be complete if the available " + "block-size is unconstrained!"); + } + } + + COLUMN_SET_LOG( + "%s: this=%p, mKnownInfeasibleBSize=%d, mKnownFeasibleBSize=%d", + __func__, this, aConfig.mKnownInfeasibleBSize, + aConfig.mKnownFeasibleBSize); + + if (aConfig.mKnownInfeasibleBSize >= aConfig.mKnownFeasibleBSize - 1) { + // aConfig.mKnownFeasibleBSize is where we want to be. This can happen in + // the very first iteration when a column container solely has a tall + // unbreakable child that overflows the container. + break; + } + + if (aConfig.mKnownInfeasibleBSize >= availableContentBSize) { + // There's no feasible block-size to fit our contents. We may need to + // reflow one more time after this loop. + break; + } + + const nscoord gap = + aConfig.mKnownFeasibleBSize - aConfig.mKnownInfeasibleBSize; + if (gap <= gapToStop && possibleOptimalBSizeDetected) { + // We detected a possible optimal block-size in the last iteration. If it + // is infeasible, we may need to reflow one more time after this loop. + break; + } + + if (lastKnownFeasibleBSize - aConfig.mKnownFeasibleBSize == 1) { + // We decreased the feasible block-size by one twip only. This could + // indicate that there is a continuously breakable child frame + // that we are crawling through. + maybeContinuousBreakingDetected = true; + } + + nscoord nextGuess = aConfig.mKnownInfeasibleBSize + gap / 2; + if (aConfig.mKnownFeasibleBSize - nextGuess < extraBlockSize && + !maybeContinuousBreakingDetected) { + // We're close to our target, so just try shrinking just the + // minimum amount that will cause one of our columns to break + // differently. + nextGuess = aConfig.mKnownFeasibleBSize - 1; + } else if (!foundFeasibleBSizeCloserToBest) { + // Make a guess by dividing mSumBSize into N columns and adding + // extraBlockSize to try to make it on the feasible side. + nextGuess = aColData.mSumBSize / aConfig.mUsedColCount + extraBlockSize; + // Sanitize it + nextGuess = clamped(nextGuess, aConfig.mKnownInfeasibleBSize + 1, + aConfig.mKnownFeasibleBSize - 1); + // We keep doubling extraBlockSize in every iteration until we find a + // feasible guess. + extraBlockSize *= 2; + } else if (aConfig.mKnownFeasibleBSize == NS_UNCONSTRAINEDSIZE) { + // This can happen when we had a next-in-flow so we didn't + // want to do an unbounded block-size measuring step. Let's just increase + // from the infeasible block-size by some reasonable amount. + nextGuess = aConfig.mKnownInfeasibleBSize * 2 + extraBlockSize; + } else if (gap <= gapToStop) { + // Floor nextGuess to the greatest multiple of gapToStop below or equal to + // mKnownFeasibleBSize. + nextGuess = aConfig.mKnownFeasibleBSize / gapToStop * gapToStop; + possibleOptimalBSizeDetected = true; + } + + // Don't bother guessing more than our block-size constraint. + nextGuess = std::min(availableContentBSize, nextGuess); + + COLUMN_SET_LOG("%s: Choosing next guess=%d, iteration=%d", __func__, + nextGuess, iterationCount); + ++iterationCount; + + aConfig.mColBSize = nextGuess; + + aUnboundedLastColumn = false; + MarkPrincipalChildrenDirty(this); + aColData = + ReflowColumns(aDesiredSize, aReflowInput, aStatus, aConfig, false); + + if (!foundFeasibleBSizeCloserToBest && aColData.mFeasible) { + foundFeasibleBSizeCloserToBest = true; + } + } + + if (!aColData.mFeasible && !aPresContext->HasPendingInterrupt()) { + // We need to reflow one more time at the feasible block-size to + // get a valid layout. + if (aConfig.mKnownInfeasibleBSize >= availableContentBSize) { + aConfig.mColBSize = availableContentBSize; + if (mLastBalanceBSize == availableContentBSize) { + // If we end up here, we have a constrained available content + // block-size, and our last column's block-size exceeds it. Also, if + // this is the first balancing iteration, the last column is given + // unconstrained available block-size, so it has a fully complete + // reflow status. Therefore, we always want to reflow again at the + // available content block-size to get a valid layout and a correct + // reflow status (likely an *incomplete* status) so that our column + // container can be fragmented if needed. + + if (aReflowInput.mFlags.mColumnSetWrapperHasNoBSizeLeft) { + // If our column container has a constrained block-size (either in a + // paginated context or in a nested column container), and is going + // to consume all its computed block-size in this fragment, then our + // column container has no block-size left to contain our + // next-in-flows. We have to give up balancing, and create our + // own overflow columns. + // + // We don't want to create overflow columns immediately when our + // content doesn't fit since this changes our reflow status from + // incomplete to complete. Valid reasons include 1) the outer column + // container might do column balancing, and it can enlarge the + // available content block-size so that the nested one could fit its + // content in next balancing iteration; or 2) the outer column + // container is filling columns sequentially, and may have more + // inline-size to create more column boxes for the nested column + // container's next-in-flows. + aConfig = ChooseColumnStrategy(aReflowInput, true); + } + } + } else { + aConfig.mColBSize = aConfig.mKnownFeasibleBSize; + } + + // This is our last attempt to reflow. If our column container's available + // block-size is unconstrained, make sure that the last column is + // allowed to have arbitrary block-size here, even though we were + // balancing. Otherwise we'd have to split, and it's not clear what we'd + // do with that. + COLUMN_SET_LOG("%s: Last attempt to call ReflowColumns", __func__); + aConfig.mIsLastBalancingReflow = true; + const bool forceUnboundedLastColumn = + aReflowInput.mParentReflowInput->AvailableBSize() == + NS_UNCONSTRAINEDSIZE; + MarkPrincipalChildrenDirty(this); + ReflowColumns(aDesiredSize, aReflowInput, aStatus, aConfig, + forceUnboundedLastColumn); + } +} + +void nsColumnSetFrame::Reflow(nsPresContext* aPresContext, + ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) { + MarkInReflow(); + // Don't support interruption in columns + nsPresContext::InterruptPreventer noInterrupts(aPresContext); + + DO_GLOBAL_REFLOW_COUNT("nsColumnSetFrame"); + DISPLAY_REFLOW(aPresContext, this, aReflowInput, aDesiredSize, aStatus); + MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!"); + + MOZ_ASSERT(aReflowInput.mCBReflowInput->mFrame->StyleColumn() + ->IsColumnContainerStyle(), + "The column container should have relevant column styles!"); + MOZ_ASSERT(aReflowInput.mParentReflowInput->mFrame->IsColumnSetWrapperFrame(), + "The column container should be ColumnSetWrapperFrame!"); + MOZ_ASSERT( + aReflowInput.ComputedLogicalBorderPadding(aReflowInput.GetWritingMode()) + .IsAllZero(), + "Only the column container can have border and padding!"); + MOZ_ASSERT( + GetChildList(FrameChildListID::OverflowContainers).IsEmpty() && + GetChildList(FrameChildListID::ExcessOverflowContainers).IsEmpty(), + "ColumnSetFrame should store overflow containers in principal " + "child list!"); + + //------------ Handle Incremental Reflow ----------------- + + COLUMN_SET_LOG("%s: Begin Reflow: this=%p, is nested multicol=%d", __func__, + this, + aReflowInput.mParentReflowInput->mFrame->HasAnyStateBits( + NS_FRAME_HAS_MULTI_COLUMN_ANCESTOR)); + + // If inline size is unconstrained, set aForceAuto to true to allow + // the columns to expand in the inline direction. (This typically + // happens in orthogonal flows where the inline direction is the + // container's block direction). + ReflowConfig config = ChooseColumnStrategy( + aReflowInput, aReflowInput.ComputedISize() == NS_UNCONSTRAINEDSIZE); + + // If balancing, then we allow the last column to grow to unbounded + // block-size during the first reflow. This gives us a way to estimate + // what the average column block-size should be, because we can measure + // the block-size of all the columns and sum them up. But don't do this + // if we have a next in flow because we don't want to suck all its + // content back here and then have to push it out again! + nsIFrame* nextInFlow = GetNextInFlow(); + bool unboundedLastColumn = config.mIsBalancing && !nextInFlow; + const ColumnBalanceData colData = ReflowColumns( + aDesiredSize, aReflowInput, aStatus, config, unboundedLastColumn); + + // If we're not balancing, then we're already done, since we should have + // reflown all of our children, and there is no need for a binary search to + // determine proper column block-size. + if (config.mIsBalancing && !aPresContext->HasPendingInterrupt()) { + FindBestBalanceBSize(aReflowInput, aPresContext, config, colData, + aDesiredSize, unboundedLastColumn, aStatus); + } + + if (aPresContext->HasPendingInterrupt() && + aReflowInput.AvailableBSize() == NS_UNCONSTRAINEDSIZE) { + // In this situation, we might be lying about our reflow status, because + // our last kid (the one that got interrupted) was incomplete. Fix that. + aStatus.Reset(); + } + + NS_ASSERTION(aStatus.IsFullyComplete() || + aReflowInput.AvailableBSize() != NS_UNCONSTRAINEDSIZE, + "Column set should be complete if the available block-size is " + "unconstrained"); + + MOZ_ASSERT(!HasAbsolutelyPositionedChildren(), + "ColumnSetWrapperFrame should be the abs.pos container!"); + FinishAndStoreOverflow(&aDesiredSize, aReflowInput.mStyleDisplay); + + COLUMN_SET_LOG("%s: End Reflow: this=%p", __func__, this); +} + +void nsColumnSetFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) { + DisplayBorderBackgroundOutline(aBuilder, aLists); + + if (IsVisibleForPainting()) { + aLists.BorderBackground()->AppendNewToTop<nsDisplayColumnRule>(aBuilder, + this); + } + + // Our children won't have backgrounds so it doesn't matter where we put them. + for (nsIFrame* f : mFrames) { + BuildDisplayListForChild(aBuilder, f, aLists); + } +} + +void nsColumnSetFrame::AppendDirectlyOwnedAnonBoxes( + nsTArray<OwnedAnonBox>& aResult) { + // Everything in mFrames is continuations of the first thing in mFrames. + nsIFrame* column = mFrames.FirstChild(); + + // We might not have any columns, apparently? + if (!column) { + return; + } + + MOZ_ASSERT(column->Style()->GetPseudoType() == PseudoStyleType::columnContent, + "What sort of child is this?"); + aResult.AppendElement(OwnedAnonBox(column)); +} + +Maybe<nscoord> nsColumnSetFrame::GetNaturalBaselineBOffset( + WritingMode aWM, BaselineSharingGroup aBaselineGroup, + BaselineExportContext aExportContext) const { + Maybe<nscoord> result; + for (const auto* kid : mFrames) { + auto kidBaseline = + kid->GetNaturalBaselineBOffset(aWM, aBaselineGroup, aExportContext); + if (!kidBaseline) { + continue; + } + // The kid frame may not necessarily be aligned with the columnset frame. + LogicalRect kidRect{aWM, kid->GetLogicalNormalPosition(aWM, GetSize()), + kid->GetLogicalSize(aWM)}; + if (aBaselineGroup == BaselineSharingGroup::First) { + *kidBaseline += kidRect.BStart(aWM); + } else { + *kidBaseline += (GetLogicalSize().BSize(aWM) - kidRect.BEnd(aWM)); + } + // Take the smallest of the baselines (i.e. Closest to border-block-start + // for `BaselineSharingGroup::First`, border-block-end for + // `BaselineSharingGroup::Last`) + if (!result || *kidBaseline < *result) { + result = kidBaseline; + } + } + return result; +} + +#ifdef DEBUG +void nsColumnSetFrame::SetInitialChildList(ChildListID aListID, + nsFrameList&& aChildList) { + MOZ_ASSERT(aListID != FrameChildListID::Principal || aChildList.OnlyChild(), + "initial principal child list must have exactly one child"); + nsContainerFrame::SetInitialChildList(aListID, std::move(aChildList)); +} + +void nsColumnSetFrame::AppendFrames(ChildListID aListID, + nsFrameList&& aFrameList) { + MOZ_CRASH("unsupported operation"); +} + +void nsColumnSetFrame::InsertFrames(ChildListID aListID, nsIFrame* aPrevFrame, + const nsLineList::iterator* aPrevFrameLine, + nsFrameList&& aFrameList) { + MOZ_CRASH("unsupported operation"); +} + +void nsColumnSetFrame::RemoveFrame(DestroyContext&, ChildListID, nsIFrame*) { + MOZ_CRASH("unsupported operation"); +} +#endif |