Bug 1731541 - Implement text-wrap: balance for nsBlockFrame reflow. r=emilio

A simple form of balance for short blocks, implemented by incrementally reducing
the effective inline-size used during line-breaking, up to the point where an
extra line would be created.

This fails the test text-wrap-balance-line-clamp-001.html, but it's unclear to me
if that test is correct (see https://github.com/w3c/csswg-drafts/issues/9310).
If we do want the behavior expected by that test, an additional patch to handle
the interaction with line-clamp will be required.

Depends on D187543

Differential Revision: https://phabricator.services.mozilla.com/D187544
This commit is contained in:
Jonathan Kew 2023-09-30 15:53:12 +00:00
parent 0428bf09b5
commit bd93e1361b
16 changed files with 462 additions and 287 deletions

View File

@ -27,17 +27,16 @@
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)
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(

View File

@ -98,7 +98,8 @@ class BlockReflowState {
nsBlockFrame* aFrame, bool aBStartMarginRoot,
bool aBEndMarginRoot, bool aBlockNeedsFloatManager,
const nscoord aConsumedBSize,
const nscoord aEffectiveContentBoxBSize);
const nscoord aEffectiveContentBoxBSize,
const nscoord aInset = 0);
/**
* Get the available reflow space (the area not occupied by floats)
@ -243,9 +244,9 @@ class BlockReflowState {
// the block.
// The block frame that is using this object
nsBlockFrame* mBlock;
nsBlockFrame* const mBlock;
nsPresContext* mPresContext;
nsPresContext* const mPresContext;
const ReflowInput& mReflowInput;
@ -301,6 +302,10 @@ class BlockReflowState {
return mContentArea.Size(wm).ConvertTo(aWM, wm);
}
// Amount of inset to apply during line-breaking, used by text-wrap:balance
// to adjust line-breaks for more consistent lengths throughout the block.
nscoord mInsetForBalance;
// Physical size. Use only for physical <-> logical coordinate conversion.
nsSize mContainerSize;
const nsSize& ContainerSize() const { return mContainerSize; }
@ -345,7 +350,7 @@ class BlockReflowState {
// mBlock's computed logical border+padding with pre-reflow skip sides applied
// (See the constructor and nsIFrame::PreReflowBlockLevelLogicalSkipSides).
LogicalMargin mBorderPadding;
const LogicalMargin mBorderPadding;
// The overflow areas of all floats placed so far
OverflowAreas mFloatOverflowAreas;
@ -383,7 +388,7 @@ class BlockReflowState {
// placed, since we're on a nowrap context.
nsTArray<nsIFrame*> mNoWrapFloats;
nscoord mMinLineHeight;
const nscoord mMinLineHeight;
int32_t mLineNumber;

View File

@ -1414,6 +1414,16 @@ void nsBlockFrame::Reflow(nsPresContext* aPresContext, ReflowOutput& aMetrics,
}
}
if (IsFrameTreeTooDeep(aReflowInput, aMetrics, aStatus)) {
return;
}
// OK, some lines may be reflowed. Blow away any saved line cursor
// because we may invalidate the nondecreasing
// overflowArea.InkOverflow().y/yMost invariant, and we may even
// delete the line with the line cursor.
ClearLineCursors();
// See comment below about oldSize. Use *only* for the
// abs-pos-containing-block-size-change optimization!
nsSize oldSize = GetSize();
@ -1430,16 +1440,316 @@ void nsBlockFrame::Reflow(nsPresContext* aPresContext, ReflowOutput& aMetrics,
autoFloatManager.CreateFloatManager(aPresContext);
}
// OK, some lines may be reflowed. Blow away any saved line cursor
// because we may invalidate the nondecreasing
// overflowArea.InkOverflow().y/yMost invariant, and we may even
// delete the line with the line cursor.
ClearLineCursors();
if (IsFrameTreeTooDeep(aReflowInput, aMetrics, aStatus)) {
return;
if (HasAnyStateBits(NS_BLOCK_NEEDS_BIDI_RESOLUTION) &&
PresContext()->BidiEnabled()) {
static_cast<nsBlockFrame*>(FirstContinuation())->ResolveBidi();
}
// Whether to apply text-wrap: balance behavior.
bool tryBalance = StyleText()->mTextWrap == StyleTextWrap::Balance &&
!GetPrevContinuation();
// Target number of lines in the block while balancing; negative if no
// balancing is being done.
int32_t balanceTarget = -1;
// Helpers for text-wrap: balance implementation:
// Count the number of lines in the mLines list, but return -1 instead if the
// count is going to exceed aLimit.
auto countLinesUpTo = [&](int32_t aLimit) -> int32_t {
int32_t n = 0;
for (auto iter = mLines.begin(); iter != mLines.end(); ++iter) {
if (++n > aLimit) {
return -1;
}
}
return n;
};
// "balancing" is implemented by shortening the effective inline-size of the
// lines, so that content will tend to be pushed down to fill later lines of
// the block. `balanceInset` is the current amount of "inset" to apply, and
// `balanceStep` is the increment to adjust it by for the next iteration.
nscoord balanceStep = 0;
// text-wrap: balance loop, executed only once if balancing is not required.
nsReflowStatus reflowStatus;
TrialReflowState trialState(consumedBSize, effectiveContentBoxBSize,
needFloatManager);
while (true) {
// Save the initial floatManager state for repeated trial reflows.
// We'll restore (and re-save) the initial state each time we repeat the
// reflow.
nsFloatManager::SavedState floatManagerState;
aReflowInput.mFloatManager->PushState(&floatManagerState);
aMetrics = ReflowOutput(aMetrics.GetWritingMode());
reflowStatus =
TrialReflow(aPresContext, aMetrics, aReflowInput, trialState);
// Do we need to start a `text-wrap: balance` iteration?
if (tryBalance) {
tryBalance = false;
// Don't try to balance an incomplete block.
if (!reflowStatus.IsFullyComplete()) {
break;
}
balanceTarget =
countLinesUpTo(StaticPrefs::layout_css_text_wrap_balance_limit());
if (balanceTarget < 2) {
// If there are less than 2 lines, or the number exceeds the limit,
// no balancing is needed; just break from the balance loop.
break;
}
// Initialize the amount of inset to try, and the iteration step size.
balanceStep = aReflowInput.ComputedISize() / balanceTarget;
trialState.ResetForBalance(balanceStep);
balanceStep /= 2;
// Restore initial floatManager state for a new trial with updated inset.
aReflowInput.mFloatManager->PopState(&floatManagerState);
continue;
}
// If we're in the process of a balance operation, check whether we've
// inset by too much and either increase or reduce the inset for the next
// iteration.
if (balanceStep > 0) {
int32_t numLines =
countLinesUpTo(StaticPrefs::layout_css_text_wrap_balance_limit());
if (reflowStatus.IsFullyComplete() && numLines == balanceTarget) {
trialState.ResetForBalance(balanceStep);
} else {
trialState.ResetForBalance(-balanceStep);
}
balanceStep /= 2;
aReflowInput.mFloatManager->PopState(&floatManagerState);
continue;
}
// If we were attempting to balance, check whether the final iteration was
// successful, and if not, back up by one step.
if (balanceTarget >= 0) {
int32_t numLines =
countLinesUpTo(StaticPrefs::layout_css_text_wrap_balance_limit());
if (reflowStatus.IsFullyComplete() && numLines == balanceTarget) {
break;
}
trialState.ResetForBalance(-1);
aReflowInput.mFloatManager->PopState(&floatManagerState);
continue;
}
// If we reach here, no balancing was required, so just exit; we don't
// reset (pop) the floatManager state because this is the reflow we're
// going to keep. So the saved state is just dropped.
break;
} // End of text-wrap: balance retry loop
// If the block direction is right-to-left, we need to update the bounds of
// lines that were placed relative to mContainerSize during reflow, as
// we typically do not know the true container size until we've reflowed all
// its children. So we use a dummy mContainerSize during reflow (see
// BlockReflowState's constructor) and then fix up the positions of the
// lines here, once the final block size is known.
//
// Note that writing-mode:vertical-rl is the only case where the block
// logical direction progresses in a negative physical direction, and
// therefore block-dir coordinate conversion depends on knowing the width
// of the coordinate space in order to translate between the logical and
// physical origins.
if (aReflowInput.GetWritingMode().IsVerticalRL()) {
nsSize containerSize = aMetrics.PhysicalSize();
nscoord deltaX = containerSize.width - trialState.mContainerWidth;
if (deltaX != 0) {
// We compute our lines and markers' overflow areas later in
// ComputeOverflowAreas(), so we don't need to adjust their overflow areas
// here.
const nsPoint physicalDelta(deltaX, 0);
for (auto& line : Lines()) {
UpdateLineContainerSize(&line, containerSize);
}
trialState.mFcBounds.Clear();
for (nsIFrame* f : mFloats) {
f->MovePositionBy(physicalDelta);
ConsiderChildOverflow(trialState.mFcBounds, f);
}
nsFrameList* markerList = GetOutsideMarkerList();
if (markerList) {
for (nsIFrame* f : *markerList) {
f->MovePositionBy(physicalDelta);
}
}
if (nsFrameList* overflowContainers = GetOverflowContainers()) {
trialState.mOcBounds.Clear();
for (nsIFrame* f : *overflowContainers) {
f->MovePositionBy(physicalDelta);
ConsiderChildOverflow(trialState.mOcBounds, f);
}
}
}
}
aMetrics.SetOverflowAreasToDesiredBounds();
ComputeOverflowAreas(aMetrics.mOverflowAreas,
trialState.mBlockEndEdgeOfChildren,
aReflowInput.mStyleDisplay);
// Factor overflow container child bounds into the overflow area
aMetrics.mOverflowAreas.UnionWith(trialState.mOcBounds);
// Factor pushed float child bounds into the overflow area
aMetrics.mOverflowAreas.UnionWith(trialState.mFcBounds);
// Let the absolutely positioned container reflow any absolutely positioned
// child frames that need to be reflowed, e.g., elements with a percentage
// based width/height
// We want to do this under either of two conditions:
// 1. If we didn't do the incremental reflow above.
// 2. If our size changed.
// Even though it's the padding edge that's the containing block, we
// can use our rect (the border edge) since if the border style
// changed, the reflow would have been targeted at us so we'd satisfy
// condition 1.
// XXX checking oldSize is bogus, there are various reasons we might have
// reflowed but our size might not have been changed to what we
// asked for (e.g., we ended up being pushed to a new page)
// When WillReflowAgainForClearance is true, we will reflow again without
// resetting the size. Because of this, we must not reflow our abs-pos
// children in that situation --- what we think is our "new size" will not be
// our real new size. This also happens to be more efficient.
WritingMode parentWM = aMetrics.GetWritingMode();
if (HasAbsolutelyPositionedChildren()) {
nsAbsoluteContainingBlock* absoluteContainer = GetAbsoluteContainingBlock();
bool haveInterrupt = aPresContext->HasPendingInterrupt();
if (aReflowInput.WillReflowAgainForClearance() || haveInterrupt) {
// Make sure that when we reflow again we'll actually reflow all the abs
// pos frames that might conceivably depend on our size (or all of them,
// if we're dirty right now and interrupted; in that case we also need
// to mark them all with NS_FRAME_IS_DIRTY). Sadly, we can't do much
// better than that, because we don't really know what our size will be,
// and it might in fact not change on the followup reflow!
if (haveInterrupt && HasAnyStateBits(NS_FRAME_IS_DIRTY)) {
absoluteContainer->MarkAllFramesDirty();
} else {
absoluteContainer->MarkSizeDependentFramesDirty();
}
if (haveInterrupt) {
// We're not going to reflow absolute frames; make sure to account for
// their existing overflow areas, which is usually a side effect of this
// reflow.
//
// TODO(emilio): nsAbsoluteContainingBlock::Reflow already checks for
// interrupt, can we just rely on it and unconditionally take the else
// branch below? That's a bit more subtle / risky, since I don't see
// what would reflow them in that case if they depended on our size.
for (nsIFrame* kid = absoluteContainer->GetChildList().FirstChild();
kid; kid = kid->GetNextSibling()) {
ConsiderChildOverflow(aMetrics.mOverflowAreas, kid);
}
}
} else {
LogicalSize containingBlockSize =
CalculateContainingBlockSizeForAbsolutes(parentWM, aReflowInput,
aMetrics.Size(parentWM));
// Mark frames that depend on changes we just made to this frame as dirty:
// Now we can assume that the padding edge hasn't moved.
// We need to reflow the absolutes if one of them depends on
// its placeholder position, or the containing block size in a
// direction in which the containing block size might have
// changed.
// XXX "width" and "height" in this block will become ISize and BSize
// when nsAbsoluteContainingBlock is logicalized
bool cbWidthChanged = aMetrics.Width() != oldSize.width;
bool isRoot = !GetContent()->GetParent();
// If isRoot and we have auto height, then we are the initial
// containing block and the containing block height is the
// viewport height, which can't change during incremental
// reflow.
bool cbHeightChanged =
!(isRoot && NS_UNCONSTRAINEDSIZE == aReflowInput.ComputedHeight()) &&
aMetrics.Height() != oldSize.height;
nsRect containingBlock(nsPoint(0, 0),
containingBlockSize.GetPhysicalSize(parentWM));
AbsPosReflowFlags flags = AbsPosReflowFlags::ConstrainHeight;
if (cbWidthChanged) {
flags |= AbsPosReflowFlags::CBWidthChanged;
}
if (cbHeightChanged) {
flags |= AbsPosReflowFlags::CBHeightChanged;
}
// Setup the line cursor here to optimize line searching for
// calculating hypothetical position of absolutely-positioned
// frames.
SetupLineCursorForQuery();
absoluteContainer->Reflow(this, aPresContext, aReflowInput, reflowStatus,
containingBlock, flags,
&aMetrics.mOverflowAreas);
}
}
FinishAndStoreOverflow(&aMetrics, aReflowInput.mStyleDisplay);
aStatus = reflowStatus;
#ifdef DEBUG
// Between when we drain pushed floats and when we complete reflow,
// we're allowed to have multiple continuations of the same float on
// our floats list, since a first-in-flow might get pushed to a later
// continuation of its containing block. But it's not permitted
// outside that time.
nsLayoutUtils::AssertNoDuplicateContinuations(this, mFloats);
if (gNoisyReflow) {
IndentBy(stdout, gNoiseIndent);
ListTag(stdout);
printf(": status=%s metrics=%d,%d carriedMargin=%d",
ToString(aStatus).c_str(), aMetrics.ISize(parentWM),
aMetrics.BSize(parentWM), aMetrics.mCarriedOutBEndMargin.get());
if (HasOverflowAreas()) {
printf(" overflow-vis={%d,%d,%d,%d}", aMetrics.InkOverflow().x,
aMetrics.InkOverflow().y, aMetrics.InkOverflow().width,
aMetrics.InkOverflow().height);
printf(" overflow-scr={%d,%d,%d,%d}", aMetrics.ScrollableOverflow().x,
aMetrics.ScrollableOverflow().y,
aMetrics.ScrollableOverflow().width,
aMetrics.ScrollableOverflow().height);
}
printf("\n");
}
if (gLameReflowMetrics) {
PRTime end = PR_Now();
int32_t ectc = nsLineBox::GetCtorCount();
int32_t numLines = mLines.size();
if (!numLines) {
numLines = 1;
}
PRTime delta, perLineDelta, lines;
lines = int64_t(numLines);
delta = end - start;
perLineDelta = delta / lines;
ListTag(stdout);
char buf[400];
SprintfLiteral(buf,
": %" PRId64 " elapsed (%" PRId64
" per line) (%d lines; %d new lines)",
delta, perLineDelta, numLines, ectc - ctc);
printf("%s\n", buf);
}
#endif
}
nsReflowStatus nsBlockFrame::TrialReflow(nsPresContext* aPresContext,
ReflowOutput& aMetrics,
const ReflowInput& aReflowInput,
TrialReflowState& aTrialState) {
#ifdef DEBUG
// Between when we drain pushed floats and when we complete reflow,
// we're allowed to have multiple continuations of the same float on
@ -1458,21 +1768,18 @@ void nsBlockFrame::Reflow(nsPresContext* aPresContext, ReflowOutput& aMetrics,
IsMarginRoot(&blockStartMarginRoot, &blockEndMarginRoot);
BlockReflowState state(aReflowInput, aPresContext, this, blockStartMarginRoot,
blockEndMarginRoot, needFloatManager, consumedBSize,
effectiveContentBoxBSize);
if (HasAnyStateBits(NS_BLOCK_NEEDS_BIDI_RESOLUTION) &&
PresContext()->BidiEnabled()) {
static_cast<nsBlockFrame*>(FirstContinuation())->ResolveBidi();
}
blockEndMarginRoot, aTrialState.mNeedFloatManager,
aTrialState.mConsumedBSize,
aTrialState.mEffectiveContentBoxBSize,
aTrialState.mInset);
// Handle paginated overflow (see nsContainerFrame.h)
OverflowAreas ocBounds;
nsReflowStatus ocStatus;
if (GetPrevInFlow()) {
ReflowOverflowContainerChildren(
aPresContext, aReflowInput, ocBounds, ReflowChildFlags::Default,
ocStatus, DefaultChildFrameMerge, Some(state.ContainerSize()));
aPresContext, aReflowInput, aTrialState.mOcBounds,
ReflowChildFlags::Default, ocStatus, DefaultChildFrameMerge,
Some(state.ContainerSize()));
}
// Now that we're done cleaning up our overflow container lists, we can
@ -1482,8 +1789,7 @@ void nsBlockFrame::Reflow(nsPresContext* aPresContext, ReflowOutput& aMetrics,
// Drain & handle pushed floats
DrainPushedFloats();
OverflowAreas fcBounds;
ReflowPushedFloats(state, fcBounds);
ReflowPushedFloats(state, aTrialState.mFcBounds);
// If we're not dirty (which means we'll mark everything dirty later)
// and our inline-size has changed, mark the lines dirty that we need to
@ -1500,7 +1806,13 @@ void nsBlockFrame::Reflow(nsPresContext* aPresContext, ReflowOutput& aMetrics,
mLines.front()->MarkDirty();
}
LazyMarkLinesDirty();
// For text-wrap:balance trials, we need to reflow all the lines even if
// they're not all "dirty".
if (aTrialState.mBalancing) {
MarkAllDescendantLinesDirty(this);
} else {
LazyMarkLinesDirty();
}
// Now reflow...
ReflowDirtyLines(state);
@ -1613,204 +1925,12 @@ void nsBlockFrame::Reflow(nsPresContext* aPresContext, ReflowOutput& aMetrics,
CheckFloats(state);
// Compute our final size
nscoord blockEndEdgeOfChildren;
ComputeFinalSize(aReflowInput, state, aMetrics, &blockEndEdgeOfChildren);
// Compute our final size (for this trial layout)
aTrialState.mBlockEndEdgeOfChildren =
ComputeFinalSize(aReflowInput, state, aMetrics);
aTrialState.mContainerWidth = state.ContainerSize().width;
// If the block direction is right-to-left, we need to update the bounds of
// lines that were placed relative to mContainerSize during reflow, as
// we typically do not know the true container size until we've reflowed all
// its children. So we use a dummy mContainerSize during reflow (see
// BlockReflowState's constructor) and then fix up the positions of the
// lines here, once the final block size is known.
//
// Note that writing-mode:vertical-rl is the only case where the block
// logical direction progresses in a negative physical direction, and
// therefore block-dir coordinate conversion depends on knowing the width
// of the coordinate space in order to translate between the logical and
// physical origins.
if (wm.IsVerticalRL()) {
nsSize containerSize = aMetrics.PhysicalSize();
nscoord deltaX = containerSize.width - state.ContainerSize().width;
if (deltaX != 0) {
// We compute our lines and markers' overflow areas later in
// ComputeOverflowAreas(), so we don't need to adjust their overflow areas
// here.
const nsPoint physicalDelta(deltaX, 0);
for (auto& line : Lines()) {
UpdateLineContainerSize(&line, containerSize);
}
fcBounds.Clear();
for (nsIFrame* f : mFloats) {
f->MovePositionBy(physicalDelta);
ConsiderChildOverflow(fcBounds, f);
}
nsFrameList* markerList = GetOutsideMarkerList();
if (markerList) {
for (nsIFrame* f : *markerList) {
f->MovePositionBy(physicalDelta);
}
}
if (nsFrameList* overflowContainers = GetOverflowContainers()) {
ocBounds.Clear();
for (nsIFrame* f : *overflowContainers) {
f->MovePositionBy(physicalDelta);
ConsiderChildOverflow(ocBounds, f);
}
}
}
}
aMetrics.SetOverflowAreasToDesiredBounds();
ComputeOverflowAreas(aMetrics.mOverflowAreas, blockEndEdgeOfChildren,
aReflowInput.mStyleDisplay);
// Factor overflow container child bounds into the overflow area
aMetrics.mOverflowAreas.UnionWith(ocBounds);
// Factor pushed float child bounds into the overflow area
aMetrics.mOverflowAreas.UnionWith(fcBounds);
// Let the absolutely positioned container reflow any absolutely positioned
// child frames that need to be reflowed, e.g., elements with a percentage
// based width/height
// We want to do this under either of two conditions:
// 1. If we didn't do the incremental reflow above.
// 2. If our size changed.
// Even though it's the padding edge that's the containing block, we
// can use our rect (the border edge) since if the border style
// changed, the reflow would have been targeted at us so we'd satisfy
// condition 1.
// XXX checking oldSize is bogus, there are various reasons we might have
// reflowed but our size might not have been changed to what we
// asked for (e.g., we ended up being pushed to a new page)
// When WillReflowAgainForClearance is true, we will reflow again without
// resetting the size. Because of this, we must not reflow our abs-pos
// children in that situation --- what we think is our "new size" will not be
// our real new size. This also happens to be more efficient.
WritingMode parentWM = aMetrics.GetWritingMode();
if (HasAbsolutelyPositionedChildren()) {
nsAbsoluteContainingBlock* absoluteContainer = GetAbsoluteContainingBlock();
bool haveInterrupt = aPresContext->HasPendingInterrupt();
if (aReflowInput.WillReflowAgainForClearance() || haveInterrupt) {
// Make sure that when we reflow again we'll actually reflow all the abs
// pos frames that might conceivably depend on our size (or all of them,
// if we're dirty right now and interrupted; in that case we also need
// to mark them all with NS_FRAME_IS_DIRTY). Sadly, we can't do much
// better than that, because we don't really know what our size will be,
// and it might in fact not change on the followup reflow!
if (haveInterrupt && HasAnyStateBits(NS_FRAME_IS_DIRTY)) {
absoluteContainer->MarkAllFramesDirty();
} else {
absoluteContainer->MarkSizeDependentFramesDirty();
}
if (haveInterrupt) {
// We're not going to reflow absolute frames; make sure to account for
// their existing overflow areas, which is usually a side effect of this
// reflow.
//
// TODO(emilio): nsAbsoluteContainingBlock::Reflow already checks for
// interrupt, can we just rely on it and unconditionally take the else
// branch below? That's a bit more subtle / risky, since I don't see
// what would reflow them in that case if they depended on our size.
for (nsIFrame* kid = absoluteContainer->GetChildList().FirstChild();
kid; kid = kid->GetNextSibling()) {
ConsiderChildOverflow(aMetrics.mOverflowAreas, kid);
}
}
} else {
LogicalSize containingBlockSize =
CalculateContainingBlockSizeForAbsolutes(parentWM, aReflowInput,
aMetrics.Size(parentWM));
// Mark frames that depend on changes we just made to this frame as dirty:
// Now we can assume that the padding edge hasn't moved.
// We need to reflow the absolutes if one of them depends on
// its placeholder position, or the containing block size in a
// direction in which the containing block size might have
// changed.
// XXX "width" and "height" in this block will become ISize and BSize
// when nsAbsoluteContainingBlock is logicalized
bool cbWidthChanged = aMetrics.Width() != oldSize.width;
bool isRoot = !GetContent()->GetParent();
// If isRoot and we have auto height, then we are the initial
// containing block and the containing block height is the
// viewport height, which can't change during incremental
// reflow.
bool cbHeightChanged =
!(isRoot && NS_UNCONSTRAINEDSIZE == aReflowInput.ComputedHeight()) &&
aMetrics.Height() != oldSize.height;
nsRect containingBlock(nsPoint(0, 0),
containingBlockSize.GetPhysicalSize(parentWM));
AbsPosReflowFlags flags = AbsPosReflowFlags::ConstrainHeight;
if (cbWidthChanged) {
flags |= AbsPosReflowFlags::CBWidthChanged;
}
if (cbHeightChanged) {
flags |= AbsPosReflowFlags::CBHeightChanged;
}
// Setup the line cursor here to optimize line searching for
// calculating hypothetical position of absolutely-positioned
// frames.
SetupLineCursorForQuery();
absoluteContainer->Reflow(this, aPresContext, aReflowInput,
state.mReflowStatus, containingBlock, flags,
&aMetrics.mOverflowAreas);
}
}
FinishAndStoreOverflow(&aMetrics, aReflowInput.mStyleDisplay);
aStatus = state.mReflowStatus;
#ifdef DEBUG
// Between when we drain pushed floats and when we complete reflow,
// we're allowed to have multiple continuations of the same float on
// our floats list, since a first-in-flow might get pushed to a later
// continuation of its containing block. But it's not permitted
// outside that time.
nsLayoutUtils::AssertNoDuplicateContinuations(this, mFloats);
if (gNoisyReflow) {
IndentBy(stdout, gNoiseIndent);
ListTag(stdout);
printf(": status=%s metrics=%d,%d carriedMargin=%d",
ToString(aStatus).c_str(), aMetrics.ISize(parentWM),
aMetrics.BSize(parentWM), aMetrics.mCarriedOutBEndMargin.get());
if (HasOverflowAreas()) {
printf(" overflow-vis={%d,%d,%d,%d}", aMetrics.InkOverflow().x,
aMetrics.InkOverflow().y, aMetrics.InkOverflow().width,
aMetrics.InkOverflow().height);
printf(" overflow-scr={%d,%d,%d,%d}", aMetrics.ScrollableOverflow().x,
aMetrics.ScrollableOverflow().y,
aMetrics.ScrollableOverflow().width,
aMetrics.ScrollableOverflow().height);
}
printf("\n");
}
if (gLameReflowMetrics) {
PRTime end = PR_Now();
int32_t ectc = nsLineBox::GetCtorCount();
int32_t numLines = mLines.size();
if (!numLines) {
numLines = 1;
}
PRTime delta, perLineDelta, lines;
lines = int64_t(numLines);
delta = end - start;
perLineDelta = delta / lines;
ListTag(stdout);
char buf[400];
SprintfLiteral(buf,
": %" PRId64 " elapsed (%" PRId64
" per line) (%d lines; %d new lines)",
delta, perLineDelta, numLines, ectc - ctc);
printf("%s\n", buf);
}
#endif
return state.mReflowStatus;
}
bool nsBlockFrame::CheckForCollapsedBEndMarginFromClearanceLine() {
@ -1908,10 +2028,9 @@ static nscoord ApplyLineClamp(const ReflowInput& aReflowInput,
return edge;
}
void nsBlockFrame::ComputeFinalSize(const ReflowInput& aReflowInput,
BlockReflowState& aState,
ReflowOutput& aMetrics,
nscoord* aBEndEdgeOfChildren) {
nscoord nsBlockFrame::ComputeFinalSize(const ReflowInput& aReflowInput,
BlockReflowState& aState,
ReflowOutput& aMetrics) {
WritingMode wm = aState.mReflowInput.GetWritingMode();
const LogicalMargin& borderPadding = aState.BorderPadding();
#ifdef NOISY_FINAL_SIZE
@ -2149,7 +2268,6 @@ void nsBlockFrame::ComputeFinalSize(const ReflowInput& aReflowInput,
// Screen out negative block sizes --- can happen due to integer overflows :-(
finalSize.BSize(wm) = std::max(0, finalSize.BSize(wm));
*aBEndEdgeOfChildren = blockEndEdgeOfChildren;
if (blockEndEdgeOfChildren != finalSize.BSize(wm) - borderPadding.BEnd(wm)) {
SetProperty(BlockEndEdgeOfChildrenProperty(), blockEndEdgeOfChildren);
@ -2166,6 +2284,8 @@ void nsBlockFrame::ComputeFinalSize(const ReflowInput& aReflowInput,
printf(": WARNING: desired:%d,%d\n", aMetrics.Width(), aMetrics.Height());
}
#endif
return blockEndEdgeOfChildren;
}
void nsBlockFrame::ConsiderBlockEndEdgeOfChildren(
@ -4552,10 +4672,10 @@ void nsBlockFrame::DoReflowInlineFrames(
// because it might get disabled there
aLine->EnableResizeReflowOptimization();
aLineLayout.BeginLineReflow(iStart, aState.mBCoord, availISize, availBSize,
aFloatAvailableSpace.HasFloats(),
false, /*XXX isTopOfPage*/
lineWM, aState.mContainerSize);
aLineLayout.BeginLineReflow(
iStart, aState.mBCoord, availISize, availBSize,
aFloatAvailableSpace.HasFloats(), false, /*XXX isTopOfPage*/
lineWM, aState.mContainerSize, aState.mInsetForBalance);
aState.mFlags.mIsLineLayoutEmpty = false;

View File

@ -481,9 +481,9 @@ class nsBlockFrame : public nsContainerFrame {
// helper for SlideLine and UpdateLineContainerSize
void MoveChildFramesOfLine(nsLineBox* aLine, nscoord aDeltaBCoord);
void ComputeFinalSize(const ReflowInput& aReflowInput,
BlockReflowState& aState, ReflowOutput& aMetrics,
nscoord* aBEndEdgeOfChildren);
// Returns block-end edge of children.
nscoord ComputeFinalSize(const ReflowInput& aReflowInput,
BlockReflowState& aState, ReflowOutput& aMetrics);
/**
* Helper method for Reflow(). Computes the overflow areas created by our
@ -534,6 +534,61 @@ class nsBlockFrame : public nsContainerFrame {
*/
bool IsVisualFormControl(nsPresContext* aPresContext);
/**
* For text-wrap:balance, we iteratively try reflowing with adjusted inline
* size to find the "best" result (the tightest size that can be applied
* without increasing the total line count of the block).
* This record is used to manage the state of these "trial reflows", and
* return results from the final trial.
*/
struct TrialReflowState {
// Values pre-computed at start of Reflow(), constant across trials.
const nscoord mConsumedBSize;
const nscoord mEffectiveContentBoxBSize;
bool mNeedFloatManager;
// Settings for the current trial.
bool mBalancing = false;
nscoord mInset = 0;
// Results computed during the trial reflow. Values from the final trial
// will be used by the remainder of Reflow().
mozilla::OverflowAreas mOcBounds;
mozilla::OverflowAreas mFcBounds;
nscoord mBlockEndEdgeOfChildren = 0;
nscoord mContainerWidth = 0;
// Initialize for the initial trial reflow, with zero inset.
TrialReflowState(nscoord aConsumedBSize, nscoord aEffectiveContentBoxBSize,
bool aNeedFloatManager)
: mConsumedBSize(aConsumedBSize),
mEffectiveContentBoxBSize(aEffectiveContentBoxBSize),
mNeedFloatManager(aNeedFloatManager) {}
// Adjust the inset amount, and reset state for a new trial.
void ResetForBalance(nscoord aInsetDelta) {
// Tells the reflow-lines loop we must consider all lines "dirty" (as we
// are modifying the effective inline-size to be used).
mBalancing = true;
// Adjust inset to apply.
mInset += aInsetDelta;
// Re-initialize state that the reflow loop will compute.
mOcBounds.Clear();
mFcBounds.Clear();
mBlockEndEdgeOfChildren = 0;
mContainerWidth = 0;
}
};
/**
* Internal helper for Reflow(); may be called repeatedly during a single
* Reflow() in order to implement text-wrap:balance.
* This method applies aTrialState.mInset during line-breaking to reduce
* the effective available inline-size (without affecting alignment).
*/
nsReflowStatus TrialReflow(nsPresContext* aPresContext,
ReflowOutput& aMetrics,
const ReflowInput& aReflowInput,
TrialReflowState& aTrialState);
public:
/**
* Helper function for the frame ctor to register a ::marker frame.

View File

@ -140,7 +140,8 @@ void nsLineLayout::BeginLineReflow(nscoord aICoord, nscoord aBCoord,
nscoord aISize, nscoord aBSize,
bool aImpactedByFloats, bool aIsTopOfPage,
WritingMode aWritingMode,
const nsSize& aContainerSize) {
const nsSize& aContainerSize,
nscoord aInset) {
NS_ASSERTION(nullptr == mRootSpan, "bad linelayout user");
LAYOUT_WARN_IF_FALSE(aISize != NS_UNCONSTRAINEDSIZE,
"have unconstrained width; this should only result from "
@ -193,6 +194,9 @@ void nsLineLayout::BeginLineReflow(nscoord aICoord, nscoord aBCoord,
psd->mIStart = aICoord;
psd->mICoord = aICoord;
psd->mIEnd = aICoord + aISize;
// Set up inset to be used for text-wrap:balance implementation, but only if
// the available size is at least 2*inset.
psd->mInset = aISize < aInset * 2 ? 0 : aInset;
mContainerSize = aContainerSize;
mBStartEdge = aBCoord;
@ -406,6 +410,7 @@ void nsLineLayout::BeginSpan(nsIFrame* aFrame,
psd->mIStart = aIStart;
psd->mICoord = aIStart;
psd->mIEnd = aIEnd;
psd->mInset = mCurrentSpan->mInset;
psd->mBaseline = aBaseline;
nsIFrame* frame = aSpanReflowInput->mFrame;
@ -801,7 +806,7 @@ void nsLineLayout::ReflowFrame(nsIFrame* aFrame, nsReflowStatus& aReflowStatus,
"have unconstrained width; this should only result from "
"very large sizes, not attempts at intrinsic width "
"calculation");
nscoord availableSpaceOnLine = psd->mIEnd - psd->mICoord;
nscoord availableSpaceOnLine = psd->mIEnd - psd->mICoord - psd->mInset;
// Setup reflow input for reflowing the frame
Maybe<ReflowInput> reflowInputHolder;

View File

@ -48,7 +48,10 @@ class nsLineLayout {
void BeginLineReflow(nscoord aICoord, nscoord aBCoord, nscoord aISize,
nscoord aBSize, bool aImpactedByFloats,
bool aIsTopOfPage, mozilla::WritingMode aWritingMode,
const nsSize& aContainerSize);
const nsSize& aContainerSize,
// aInset is used during text-wrap:balance to reduce
// the effective available space on the line.
nscoord aInset = 0);
void EndLineReflow();
@ -494,6 +497,7 @@ class nsLineLayout {
nscoord mIStart;
nscoord mICoord;
nscoord mIEnd;
nscoord mInset;
nscoord mBStartLeading, mBEndLeading;
nscoord mLogicalBSize;

View File

@ -8777,6 +8777,12 @@
value: @IS_NIGHTLY_BUILD@
mirror: always
# Maximum number of lines to try balancing.
- name: layout.css.text-wrap-balance.limit
type: int32_t
value: 10
mirror: always
# Support for the css Zoom property.
- name: layout.css.zoom.enabled
type: RelaxedAtomicBool

View File

@ -1,3 +0,0 @@
[text-wrap-invalid.html]
expected:
if (os == "android") and fission: [OK, TIMEOUT]

View File

@ -1,39 +1,13 @@
[text-wrap-valid.html]
expected:
if (os == "android") and fission: [OK, TIMEOUT]
[e.style['text-wrap'\] = "wrap" should set the property value]
expected: FAIL
[e.style['text-wrap'\] = "nowrap" should set the property value]
expected: FAIL
[e.style['text-wrap'\] = "balance" should set the property value]
expected: FAIL
[e.style['text-wrap'\] = "stable" should set the property value]
expected: FAIL
[e.style['text-wrap'\] = "pretty" should set the property value]
expected: FAIL
[e.style['text-wrap'\] = "initial" should set the property value]
expected: FAIL
[e.style['text-wrap'\] = "inherit" should set the property value]
expected: FAIL
[e.style['text-wrap'\] = "unset" should set the property value]
expected: FAIL
[e.style['text-wrap'\] = "revert" should set the property value]
expected: FAIL
[e.style['text-wrap'\] = "revert-layer" should set the property value]
expected: FAIL
[e.style['text-wrap'\] = "auto" should set the property value]
expected: FAIL
[e.style['text-wrap'\] = "wrap auto" should set the property value]
expected: FAIL

View File

@ -1,15 +1,6 @@
[white-space-shorthand-text-wrap.html]
[`text-wrap: balance` should be set]
expected: FAIL
[`text-wrap` should not be affected by previous `white-space`]
expected: FAIL
[`white-space` should overwrite previous `text-wrap`]
expected: FAIL
[`text-wrap` should not be affected by `white-space` on the parent]
expected: FAIL
[`white-space` should overwrite `text-wrap` on the parent]
expected: FAIL

View File

@ -1,2 +0,0 @@
[text-wrap-balance-002.html]
expected: FAIL

View File

@ -0,0 +1,2 @@
[text-wrap-balance-line-clamp-001.html]
expected: FAIL

View File

@ -1,2 +0,0 @@
[text-wrap-balance-text-indent-001.html]
expected: FAIL

View File

@ -0,0 +1,6 @@
[accumulation-per-property-002.html]
[text-wrap: "nowrap" onto "wrap"]
expected: FAIL
[text-wrap: "wrap" onto "nowrap"]
expected: FAIL

View File

@ -1,3 +1,9 @@
[addition-per-property-002.html]
expected:
if (os == "android") and fission: [TIMEOUT, OK]
[text-wrap: "nowrap" onto "wrap"]
expected: FAIL
[text-wrap: "wrap" onto "nowrap"]
expected: FAIL

View File

@ -1,3 +1,12 @@
[interpolation-per-property-002.html]
expected:
if (os == "android") and fission: [OK, TIMEOUT]
[text-wrap uses discrete animation when animating between "wrap" and "nowrap" with linear easing]
expected: FAIL
[text-wrap uses discrete animation when animating between "wrap" and "nowrap" with effect easing]
expected: FAIL
[text-wrap uses discrete animation when animating between "wrap" and "nowrap" with keyframe easing]
expected: FAIL