Bug 1779645 Part 1 - Switch CSS named page fragmentation to happen at reflow instead of frame construction r=dholbert

This also adds a small post-processing step for frame-construction to ensure
that replaced frames (or more generally frames with content but no children)
have the auto page-name set on them.

When page-name value tracking is switched to be lazy, this post-processing step
will likely be redundant and can be removed.

Differential Revision: https://phabricator.services.mozilla.com/D152701
This commit is contained in:
Emily McDonough
2022-09-12 22:44:48 +00:00
parent 334a736b2a
commit e114010ff0
3 changed files with 152 additions and 83 deletions

View File

@@ -5502,89 +5502,13 @@ void nsCSSFrameConstructor::AddFrameConstructionItemsInternal(
return;
}
bool pageNameBreak = false;
// TODO: We should document why the TextIsOnlyWhitespace() check is needed.
// This will be documented as part of fixing Bug 1782324
if (aParentFrame && aState.mPresContext->IsPaginated() &&
StaticPrefs::layout_css_named_pages_enabled() &&
!aContent->TextIsOnlyWhitespace()) {
// TODO: This is slightly incorrect! See Bug 1764437
// We should be waiting all of our descendent frames to be constructed.
//
// Alternatively, we could propagate this back up the frame tree after
// constructing this frame's first child, inspecting the parent frames and
// rewriting their first child page-name.
const StylePageName& pageName = aComputedStyle->StylePage()->mPage;
// Resolve auto against the parent frame's used page name, which has been
// determined and set on aState.mAutoPageNameValue. If this item is not
// block-level then we use the value that auto resolves to.
//
// This is to achieve the propagation behavior described in the spec:
//
// "A start page value and end page value is determined for each box as
// the value (if any) propagated from its first or last child box
// (respectively), else the used value on the box itself."
//
// "A child propagates its own start or end page value if and only if the
// page property applies to it."
//
// The page property only applies to "boxes that create class A break
// points". When taken together, means that non block-level children do
// not propagate start/end page values, and instead we use "the used
// value on the box itself", the "box itself" being aParentFrame. This
// value has been determined and saved as aState.mAutoPageNameValue
//
// https://www.w3.org/TR/css-page-3/#using-named-pages
// https://www.w3.org/TR/css-break-3/#btw-blocks
const nsAtom* const pageNameAtom =
(pageName.IsPageName() &&
aComputedStyle->StyleDisplay()->IsBlockOutsideStyle())
? pageName.AsPageName().AsAtom()
: aState.mAutoPageNameValue;
// Check if we are the first child of our parent. If so, propagate this
// child's page name up the frame tree for every frame while our ancestor
// is the first child of its parent.
nsIFrame::PageValues* const framePageValues =
aParentFrame->GetProperty(nsIFrame::PageValuesProperty());
// TODO alaskanemily: This assert should be removed when we move to lazily
// setting the PageValuesProperty
MOZ_ASSERT(framePageValues,
"child box page names should have been created by "
"AutoFrameConstructionPageName");
if (!framePageValues->mStartPageValue) {
framePageValues->mStartPageValue = pageNameAtom;
if (nsIFrame* const prevFrame = aParentFrame->GetPrevSibling()) {
const nsIFrame::PageValues* const prevPageValues =
prevFrame->GetProperty(nsIFrame::PageValuesProperty());
if (prevPageValues && prevPageValues->mEndPageValue != pageNameAtom) {
pageNameBreak = true;
}
}
// Propagate the start page value back up the frame tree.
// If the frame already has mStartPageValue set, then we are not a
// descendant of the frame's first child.
for (nsContainerFrame* frame = aParentFrame->GetParent(); frame;
frame = frame->GetParent()) {
nsIFrame::PageValues* const parentPageValues =
frame->GetProperty(nsIFrame::PageValuesProperty());
if (!parentPageValues || parentPageValues->mStartPageValue) {
break;
}
parentPageValues->mStartPageValue = pageNameAtom;
}
}
framePageValues->mEndPageValue = pageNameAtom;
}
const bool canHavePageBreak =
aFlags.contains(ItemFlag::AllowPageBreak) &&
aState.mPresContext->IsPaginated() &&
!display.IsAbsolutelyPositionedStyle() &&
!(aParentFrame && aParentFrame->IsGridContainerFrame()) &&
!(bits & FCDATA_IS_TABLE_PART) && !(bits & FCDATA_IS_SVG_TEXT);
if (canHavePageBreak && (pageNameBreak || display.BreakBefore())) {
if (canHavePageBreak && display.BreakBefore()) {
AppendPageBreakItem(aContent, aItems);
}
@@ -9626,6 +9550,87 @@ inline void nsCSSFrameConstructor::ConstructFramesFromItemList(
VerifyGridFlexContainerChildren(aParentFrame, aFrameList);
// Calculate and propagate page-name values for each frame in the frame list.
// This will be affected by https://bugzilla.mozilla.org/1782597
if (aState.mPresContext->IsPaginated() &&
StaticPrefs::layout_css_named_pages_enabled()) {
// Set the start/end page values while iterating the frame list, to walk
// up the frame tree only once after iterating the frame list.
// This also avoids extra property lookups on these frames.
const nsAtom* startPageValue = nullptr;
const nsAtom* endPageValue = nullptr;
for (nsIFrame* f : aFrameList) {
// Resolve auto against the parent frame's used page name, which has been
// determined and set on aState.mAutoPageNameValue. If this item is not
// block-level then we use the value that auto resolves to.
//
// This is to achieve the propagation behavior described in the spec:
//
// "A start page value and end page value is determined for each box as
// the value (if any) propagated from its first or last child box
// (respectively), else the used value on the box itself."
//
// "A child propagates its own start or end page value if and only if the
// page property applies to it."
//
// The page property only applies to "boxes that create class A break
// points". When taken together, this means that non block-level children
// do not propagate start/end page values, and instead we use "the used
// value on the box itself", the "box itself" being aParentFrame. This
// value has been determined and saved as aState.mAutoPageNameValue
//
// https://www.w3.org/TR/css-page-3/#using-named-pages
// https://www.w3.org/TR/css-break-3/#btw-blocks
const StylePageName& pageName = f->StylePage()->mPage;
const nsAtom* const pageNameAtom =
(pageName.IsPageName() && f->IsBlockOutside())
? pageName.AsPageName().AsAtom()
: aState.mAutoPageNameValue;
nsIFrame::PageValues* pageValues =
f->GetProperty(nsIFrame::PageValuesProperty());
if (!pageValues) {
pageValues = new nsIFrame::PageValues();
f->AddProperty(nsIFrame::PageValuesProperty(), pageValues);
}
MOZ_ASSERT(!pageValues->mStartPageValue == !pageValues->mEndPageValue,
"Both or neither mStartPageValue and mEndPageValue should "
"have been set");
if (!pageValues->mStartPageValue) {
pageValues->mStartPageValue = pageNameAtom;
pageValues->mEndPageValue = pageNameAtom;
}
if (!startPageValue) {
startPageValue = pageValues->mStartPageValue;
}
endPageValue = pageValues->mEndPageValue;
}
MOZ_ASSERT(!startPageValue == !endPageValue,
"Should have set both or neither page values");
if (startPageValue) {
// TODO: This is slightly incorrect! See Bug 1764437
// We should be waiting all of our descendent frames to be constructed.
//
// Alternatively, we could propagate this back up the frame tree after
// constructing this frame's first child, inspecting the parent frames
// and rewriting their first child page-name.
for (nsContainerFrame* frame = aParentFrame; frame;
frame = frame->GetParent()) {
nsIFrame::PageValues* const parentPageValues =
frame->GetProperty(nsIFrame::PageValuesProperty());
// Propagate the start page value back up the frame tree.
// If the frame already has mStartPageValue set, then we are not a
// descendant of the frame's first child.
if (!parentPageValues) {
break;
}
if (!parentPageValues->mStartPageValue) {
parentPageValues->mStartPageValue = startPageValue;
}
parentPageValues->mEndPageValue = endPageValue;
}
}
}
if (aParentIsWrapperAnonBox) {
for (nsIFrame* f : aFrameList) {
f->SetParentIsWrapperAnonBox();

View File

@@ -2626,6 +2626,29 @@ void nsBlockFrame::ReflowDirtyLines(BlockReflowState& aState) {
LineIterator line = LinesBegin(), line_end = LinesEnd();
// Determine if children of this frame could have breaks between them for
// page names.
//
// We need to check for paginated layout, the named-page pref, and if the
// available block-size is constrained.
//
// Note that we need to check for paginated layout as named-pages are only
// used during paginated reflow. We need to additionally check for
// unconstrained block-size to avoid introducing fragmentation breaks during
// "measuring" reflows within an overall paginated reflow, and to avoid
// fragmentation in monolithic containers like 'inline-block'.
//
// Because we can only break for named pages using Class A breakpoints, we
// also need to check that the block flow direction of the containing frame
// of these items (which is this block) is parallel to that of this page.
// See: https://www.w3.org/TR/css-break-3/#btw-blocks
const nsPresContext* const presCtx = aState.mPresContext;
const bool canBreakForPageNames =
aState.mReflowInput.AvailableBSize() != NS_UNCONSTRAINEDSIZE &&
presCtx->IsPaginated() && StaticPrefs::layout_css_named_pages_enabled() &&
presCtx->GetPresShell()->GetRootFrame()->GetWritingMode().IsVertical() ==
GetWritingMode().IsVertical();
// Reflow the lines that are already ours
for (; line != line_end; ++line, aState.AdvanceToNextLine()) {
DumpLine(aState, line, deltaBCoord, 0);
@@ -2779,6 +2802,38 @@ void nsBlockFrame::ReflowDirtyLines(BlockReflowState& aState) {
}
}
// Check for a page break caused by CSS named pages.
//
// We should break for named pages when two frames meet at a class A
// breakpoint, where the first frame has a different end page value to the
// second frame's start page value. canBreakForPageNames is true iff
// children of this frame can form class A breakpoints, and that we are not
// in a measurement reflow or in a monolithic container such as
// 'inline-block'.
//
// We specifically do not want to cause a page-break for named pages when
// we are at the top of a page. This would otherwise happen when the
// previous sibling is an nsPageBreakFrame, or all previous siblings on the
// current page are zero-height. The latter may not be per-spec, but is
// compatible with Chrome's implementation of named pages.
bool shouldBreakForPageName = false;
if (canBreakForPageNames && (!aState.mReflowInput.mFlags.mIsTopOfPage ||
!aState.IsAdjacentWithBStart())) {
const nsIFrame* const frame = line->mFirstChild;
if (const nsIFrame* prevFrame = frame->GetPrevSibling()) {
if (const nsIFrame::PageValues* const pageValues =
frame->GetProperty(nsIFrame::PageValuesProperty())) {
const nsIFrame::PageValues* const prevPageValues =
prevFrame->GetProperty(nsIFrame::PageValuesProperty());
if (prevPageValues &&
prevPageValues->mEndPageValue != pageValues->mStartPageValue) {
shouldBreakForPageName = true;
line->MarkDirty();
}
}
}
}
if (needToRecoverState && line->IsDirty()) {
// We need to reconstruct the block-end margin only if we didn't
// reflow the previous line and we do need to reflow (or repair
@@ -2826,9 +2881,18 @@ void nsBlockFrame::ReflowDirtyLines(BlockReflowState& aState) {
"Don't reflow blocks while willReflowAgain is true, reflow "
"of block abs-pos children depends on this");
// Reflow the dirty line. If it's an incremental reflow, then force
// it to invalidate the dirty area if necessary
ReflowLine(aState, line, &keepGoing);
if (shouldBreakForPageName) {
// Immediately fragment for page-name. It is possible we could break
// out of the loop right here, but this should make it more similar to
// what happens when reflow causes fragmentation.
keepGoing = false;
PushLines(aState, line.prev());
aState.mReflowStatus.SetIncomplete();
} else {
// Reflow the dirty line. If it's an incremental reflow, then force
// it to invalidate the dirty area if necessary
ReflowLine(aState, line, &keepGoing);
}
if (aState.mReflowInput.WillReflowAgainForClearance()) {
line->MarkDirty();

View File

@@ -2,10 +2,10 @@ defaults pref(layout.css.named-pages.enabled,true)
# https://bugzilla.mozilla.org/1764437
== page-name-display-none-child.html page-name-display-none-child-ref.html
fails == page-name-canvas-001.html page-name-canvas-001-ref.html
fails == page-name-canvas-002.html page-name-canvas-002-ref.html
== page-name-canvas-001.html page-name-canvas-001-ref.html
== page-name-canvas-002.html page-name-canvas-002-ref.html
== page-name-img-001.html page-name-img-001-ref.html
fails == page-name-img-002.html page-name-img-002-ref.html
== page-name-img-002.html page-name-img-002-ref.html
== page-name-siblings-001.html page-name-siblings-ref.html
== page-name-siblings-002.html page-name-siblings-ref.html
== page-name-siblings-003.html page-name-siblings-ref.html