Bug 1890238 Part 2 - Honor forced break values on flex items. r=dholbert

This patch stops the frame constructor from inserting nsPageBreakFrame as a flex
item, because flex container does not rely on it to do forced page break at all,
and nsPageBreakFrame can produce wrong layout result.

The majority of this patch is to honor forced break values on flex items in
nsFlexContainerFrame::ReflowChildren(). In this patch, we don't handle avoid
break values such as `break-before:avoid` and `break-after:avoid` since they are
not handled in other frame types yet.

WPTs are added in later parts.

Differential Revision: https://phabricator.services.mozilla.com/D207910
This commit is contained in:
Ting-Yu Lin 2024-05-03 20:43:35 +00:00
parent 2deec4dcb1
commit 5c698c69b5
6 changed files with 187 additions and 16 deletions

View file

@ -5283,7 +5283,7 @@ void nsCSSFrameConstructor::AddFrameConstructionItemsInternal(
aFlags.contains(ItemFlag::AllowPageBreak) &&
aState.mPresContext->IsPaginated() &&
!display.IsAbsolutelyPositionedStyle() &&
!(aParentFrame && aParentFrame->IsGridContainerFrame()) &&
!(aParentFrame && aParentFrame->IsFlexOrGridContainer()) &&
!(bits & FCDATA_IS_TABLE_PART) && !(bits & FCDATA_IS_SVG_TEXT);
if (canHavePageBreak && display.BreakBefore()) {
AppendPageBreakItem(aContent, aItems);

View file

@ -5527,14 +5527,19 @@ std::tuple<nscoord, nsReflowStatus> nsFlexContainerFrame::ReflowChildren(
const bool isSingleLine =
StyleFlexWrap::Nowrap == aReflowInput.mStylePosition->mFlexWrap;
// FINAL REFLOW: Give each child frame another chance to reflow, now that
// we know its final size and position.
const FlexLine& startmostLine = StartmostLine(aFlr.mLines, aAxisTracker);
const FlexLine& endmostLine = EndmostLine(aFlr.mLines, aAxisTracker);
const FlexItem* startmostItem =
startmostLine.IsEmpty() ? nullptr
: &startmostLine.StartmostItem(aAxisTracker);
const FlexItem* endmostItem =
endmostLine.IsEmpty() ? nullptr : &endmostLine.EndmostItem(aAxisTracker);
bool endmostItemOrLineHasBreakAfter = false;
// If true, push all remaining flex items to the container's next-in-flow.
bool shouldPushRemainingItems = false;
// FINAL REFLOW: Give each child frame another chance to reflow.
const size_t numLines = aFlr.mLines.Length();
for (size_t lineIdx = 0; lineIdx < numLines; ++lineIdx) {
// Iterate flex lines from the startmost to endmost (relative to flex
@ -5545,6 +5550,11 @@ std::tuple<nscoord, nsReflowStatus> nsFlexContainerFrame::ReflowChildren(
MOZ_ASSERT(lineIdx != 0 || &line == &startmostLine,
"Logic for finding startmost line should be consistent!");
// These two variables can be set when we are a row-oriented flex container
// during fragmentation.
bool lineHasBreakBefore = false;
bool lineHasBreakAfter = false;
const size_t numItems = line.Items().Length();
for (size_t itemIdx = 0; itemIdx < numItems; ++itemIdx) {
// Iterate flex items from the startmost to endmost (relative to flex
@ -5643,15 +5653,22 @@ std::tuple<nscoord, nsReflowStatus> nsFlexContainerFrame::ReflowChildren(
// (i.e. its frame rect), instead of the container's content-box:
framePos += containerContentBoxOrigin;
// Check if we actually need to reflow the item -- if the item's position
// is below the available space's block-end, push it to our next-in-flow;
// if it does need a reflow, and we already reflowed it with the right
// content-box size.
const bool childBPosExceedAvailableSpaceBEnd =
availableBSizeForItem != NS_UNCONSTRAINEDSIZE &&
availableBSizeForItem <= 0;
// Check if we can skip reflowing the item because it will be pushed to
// our next-in-flow -- i.e. if there was a forced break before it, or its
// position is beyond the available space's block-end.
bool itemInPushedItems = false;
if (childBPosExceedAvailableSpaceBEnd) {
if (shouldPushRemainingItems) {
FLEX_ITEM_LOG(
item.Frame(),
"[frag] Item needed to be pushed to container's next-in-flow due "
"to a forced break before it");
pushedItems.Insert(item.Frame());
itemInPushedItems = true;
} else if (availableBSizeForItem != NS_UNCONSTRAINEDSIZE &&
availableBSizeForItem <= 0) {
// The item's position is beyond the available space, so we have to push
// it.
//
// Note: Even if all of our items are beyond the available space & get
// pushed here, we'll be guaranteed to place at least one of them (and
// make progress) in one of the flex container's *next* fragment. It's
@ -5680,7 +5697,38 @@ std::tuple<nscoord, nsReflowStatus> nsFlexContainerFrame::ReflowChildren(
ReflowFlexItem(aAxisTracker, aReflowInput, item, framePos,
isAdjacentWithBStart, availableSize, aContainerSize);
if (aReflowInput.IsInFragmentedContext()) {
const bool itemHasBreakBefore =
item.Frame()->ShouldBreakBefore(aReflowInput.mBreakType) ||
childStatus.IsInlineBreakBefore();
if (itemHasBreakBefore) {
if (aAxisTracker.IsRowOriented()) {
lineHasBreakBefore = true;
} else if (isSingleLine) {
if (&item == startmostItem) {
if (!GetPrevInFlow() && !aReflowInput.mFlags.mIsTopOfPage) {
// If we are first-in-flow and not at top-of-page, early
// return here to propagate forced break-before from the
// startmost item to the flex container.
nsReflowStatus childrenStatus;
childrenStatus.SetInlineLineBreakBeforeAndReset();
return {0, childrenStatus};
}
} else {
shouldPushRemainingItems = true;
}
} else {
// Bug 1806717: We haven't implemented fragmentation for
// multi-line column-oriented flex container, so we just ignore
// forced breaks for now.
}
}
}
const bool shouldPushItem = [&]() {
if (shouldPushRemainingItems) {
return true;
}
if (availableBSizeForItem == NS_UNCONSTRAINEDSIZE) {
// If the available block-size is unconstrained, then we're not
// fragmenting and we don't want to push the item.
@ -5705,7 +5753,8 @@ std::tuple<nscoord, nsReflowStatus> nsFlexContainerFrame::ReflowChildren(
FLEX_ITEM_LOG(
item.Frame(),
"[frag] Item needed to be pushed to container's next-in-flow "
"because its block-size is larger than the available space");
"because it encounters a forced break before it, or its "
"block-size is larger than the available space");
pushedItems.Insert(item.Frame());
itemInPushedItems = true;
} else if (childStatus.IsIncomplete()) {
@ -5713,7 +5762,29 @@ std::tuple<nscoord, nsReflowStatus> nsFlexContainerFrame::ReflowChildren(
} else if (childStatus.IsOverflowIncomplete()) {
overflowIncompleteItems.Insert(item.Frame());
}
if (aReflowInput.IsInFragmentedContext()) {
const bool itemHasBreakAfter =
item.Frame()->ShouldBreakAfter(aReflowInput.mBreakType) ||
childStatus.IsInlineBreakAfter();
if (itemHasBreakAfter) {
if (aAxisTracker.IsRowOriented()) {
lineHasBreakAfter = true;
} else if (isSingleLine) {
shouldPushRemainingItems = true;
if (&item == endmostItem) {
endmostItemOrLineHasBreakAfter = true;
}
} else {
// Bug 1806717: We haven't implemented fragmentation for
// multi-line column-oriented flex container, so we just ignore
// forced breaks for now.
}
}
}
} else {
// We already reflowed the item with the right content-box size, so we
// can simply move it into place.
MoveFlexItemToFinalPosition(item, framePos, aContainerSize);
}
@ -5801,6 +5872,37 @@ std::tuple<nscoord, nsReflowStatus> nsFlexContainerFrame::ReflowChildren(
}
}
if (aReflowInput.IsInFragmentedContext() && aAxisTracker.IsRowOriented()) {
// Propagate forced break values from the flex items to its flex line.
if (lineHasBreakBefore) {
if (&line == &startmostLine) {
if (!GetPrevInFlow() && !aReflowInput.mFlags.mIsTopOfPage) {
// If we are first-in-flow and not at top-of-page, early return here
// to propagate forced break-before from the startmost line to the
// flex container.
nsReflowStatus childrenStatus;
childrenStatus.SetInlineLineBreakBeforeAndReset();
return {0, childrenStatus};
}
} else {
// Current non-startmost line has forced break-before, so push all the
// items in this line.
for (const FlexItem& item : line.Items()) {
pushedItems.Insert(item.Frame());
incompleteItems.Remove(item.Frame());
overflowIncompleteItems.Remove(item.Frame());
}
shouldPushRemainingItems = true;
}
}
if (lineHasBreakAfter) {
shouldPushRemainingItems = true;
if (&line == &endmostLine) {
endmostItemOrLineHasBreakAfter = true;
}
}
}
// Now we've finished processing all the items in the startmost line.
// Determine the amount by which the startmost line's block-end edge has
// shifted, so we can apply the same shift for the remaining lines.
@ -5822,6 +5924,8 @@ std::tuple<nscoord, nsReflowStatus> nsFlexContainerFrame::ReflowChildren(
childrenStatus.SetIncomplete();
} else if (!overflowIncompleteItems.IsEmpty()) {
childrenStatus.SetOverflowIncomplete();
} else if (endmostItemOrLineHasBreakAfter) {
childrenStatus.SetInlineLineBreakAfter();
}
PushIncompleteChildren(pushedItems, incompleteItems, overflowIncompleteItems);
@ -5966,6 +6070,14 @@ void nsFlexContainerFrame::PopulateReflowOutput(
return;
}
// Propagate forced break values from flex items or flex lines.
if (aChildrenStatus.IsInlineBreakBefore()) {
aStatus.SetInlineLineBreakBeforeAndReset();
}
if (aChildrenStatus.IsInlineBreakAfter()) {
aStatus.SetInlineLineBreakAfter();
}
// If we haven't established a baseline for the container yet, i.e. if we
// don't have any flex item in the startmost flex line that participates in
// baseline alignment, then use the startmost flex item to derive the

View file

@ -11640,6 +11640,47 @@ bool nsIFrame::HasUnreflowedContainerQueryAncestor() const {
return false;
}
bool nsIFrame::ShouldBreakBefore(
const ReflowInput::BreakType aBreakType) const {
const auto* display = StyleDisplay();
return ShouldBreakBetween(display, display->mBreakBefore, aBreakType);
}
bool nsIFrame::ShouldBreakAfter(const ReflowInput::BreakType aBreakType) const {
const auto* display = StyleDisplay();
return ShouldBreakBetween(display, display->mBreakAfter, aBreakType);
}
bool nsIFrame::ShouldBreakBetween(
const nsStyleDisplay* aDisplay, const StyleBreakBetween aBreakBetween,
const ReflowInput::BreakType aBreakType) const {
const bool shouldBreakBetween = [&] {
switch (aBreakBetween) {
case StyleBreakBetween::Always:
return true;
case StyleBreakBetween::Auto:
case StyleBreakBetween::Avoid:
return false;
case StyleBreakBetween::Page:
case StyleBreakBetween::Left:
case StyleBreakBetween::Right:
return aBreakType == ReflowInput::BreakType::Page;
}
MOZ_ASSERT_UNREACHABLE("Unknown break-between value!");
return false;
}();
if (!shouldBreakBetween) {
return false;
}
if (IsAbsolutelyPositioned(aDisplay)) {
// 'break-before' and 'break-after' properties does not apply to
// absolutely-positioned boxes.
return false;
}
return true;
}
#ifdef DEBUG
static void GetTagName(nsIFrame* aFrame, nsIContent* aContent, int aResultSize,
char* aResult) {

View file

@ -1386,7 +1386,25 @@ class nsIFrame : public nsQueryFrame {
bool HasUnreflowedContainerQueryAncestor() const;
// Return True if this frame has a forced break value before it.
//
// Note: this method only checks 'break-before' property on *this* frame, and
// it doesn't handle forced break value propagation from its first child.
// Callers should handle the propagation in reflow.
bool ShouldBreakBefore(const ReflowInput::BreakType aBreakType) const;
// Return True if this frame has a forced break value after it.
//
// Note: this method only checks 'break-after' property on *this* frame, and
// it doesn't handle forced break value propagation from its last child.
// Callers should handle the propagation in reflow.
bool ShouldBreakAfter(const ReflowInput::BreakType aBreakType) const;
private:
bool ShouldBreakBetween(const nsStyleDisplay* aDisplay,
const mozilla::StyleBreakBetween aBreakBetween,
const ReflowInput::BreakType aBreakType) const;
// The value that the CSS page-name "auto" keyword resolves to for children
// of this frame.
//

View file

@ -1533,8 +1533,10 @@ struct MOZ_NEEDS_MEMMOVABLE_MEMBERS nsStyleDisplay {
}
}
// These two methods are deprecated since they do not differentiate paginated
// context and multi-column context. Use nsIFrame::ShouldBreakBefore() /
// nsIFrame::ShouldBreakAfter() instead.
bool BreakBefore() const { return ShouldBreak(mBreakBefore); }
bool BreakAfter() const { return ShouldBreak(mBreakAfter); }
// These are defined in nsStyleStructInlines.h.

View file

@ -1,2 +0,0 @@
[multi-line-row-flex-fragmentation-019.html]
expected: FAIL