Bug 1789255: Anonymous children of elements with content-visibility should not be skipped for layout r=emilio

Some anonymous children are important for properly sizing their parents
even when those parents hide content with `content-visibility`. This is
shown by regressions in the proper layout of some form elements with
`content-visibility`.

This change introduces a more conservative approach for avoiding layout
of hidden content. Instead of leaving all children dirty during reflow,
reflow anonymous frames (and nsComboboxDisplayFrame, a specialized kind
of anonymous frame). This change means that frames may only lay out some
of their children, so it must introduce some more changes to assumptions
during line layout.

In addition, this change renames `content-visibility` related methods in
nsIFrame in order to make it more obvious what they do.

Differential Revision: https://phabricator.services.mozilla.com/D157306
This commit is contained in:
Martin Robinson 2022-09-15 17:56:42 +00:00
Родитель 99644a51e5
Коммит 631f1807ed
18 изменённых файлов: 114 добавлений и 59 удалений

Просмотреть файл

@ -613,7 +613,7 @@ IntersectionOutput DOMIntersectionObserver::Intersect(
// true even if both the root and the target elements are in the skipped
// contents."
// https://drafts.csswg.org/css-contain/#cv-notes
if (targetFrame->AncestorHidesContent()) {
if (targetFrame->IsHiddenByContentVisibilityOnAnyAncestor()) {
return {isSimilarOrigin};
}

Просмотреть файл

@ -724,7 +724,7 @@ bool Element::CheckVisibility(const CheckVisibilityOptions& aOptions) {
return false;
}
if (f->AncestorHidesContent()) {
if (f->IsHiddenByContentVisibilityOnAnyAncestor()) {
// 2. If a shadow-including ancestor of this has content-visibility: hidden,
// return false.
return false;

Просмотреть файл

@ -3145,11 +3145,11 @@ static bool IsVisibleAndNotInReplacedElement(nsIFrame* aFrame) {
aFrame->HasAnyStateBits(NS_FRAME_IS_NONDISPLAY)) {
return false;
}
if (aFrame->IsContentHidden()) {
if (aFrame->HidesContent()) {
return false;
}
for (nsIFrame* f = aFrame->GetParent(); f; f = f->GetParent()) {
if (f->IsContentHidden()) {
if (f->HidesContent()) {
return false;
}
if (f->IsFrameOfType(nsIFrame::eReplaced) &&

Просмотреть файл

@ -3720,7 +3720,7 @@ void PresShell::DoScrollContentIntoView() {
NS_ASSERTION(mDidInitialize, "should have done initial reflow by now");
nsIFrame* frame = mContentToScrollTo->GetPrimaryFrame();
if (!frame || frame->AncestorHidesContent()) {
if (!frame || frame->IsHiddenByContentVisibilityOnAnyAncestor()) {
mContentToScrollTo->RemoveProperty(nsGkAtoms::scrolling);
mContentToScrollTo = nullptr;
return;
@ -3799,7 +3799,7 @@ bool PresShell::ScrollFrameRectIntoView(nsIFrame* aFrame, const nsRect& aRect,
ScrollAxis aHorizontal,
ScrollFlags aScrollFlags,
const nsIFrame* aTarget) {
if (aFrame->AncestorHidesContent()) {
if (aFrame->IsHiddenByContentVisibilityOnAnyAncestor()) {
return false;
}
@ -11866,7 +11866,7 @@ void PresShell::EnsureReflowIfFrameHasHiddenContent(nsIFrame* aFrame) {
nsIFrame* topmostFrameWithContentHidden = nullptr;
for (nsIFrame* cur = aFrame->GetInFlowParent(); cur;
cur = cur->GetInFlowParent()) {
if (cur->IsContentHidden()) {
if (cur->HidesContent()) {
topmostFrameWithContentHidden = cur;
mHiddenContentInForcedLayout.Insert(cur->GetContent());
}

Просмотреть файл

@ -1314,7 +1314,7 @@ void nsBlockFrame::ClearLineClampEllipsis() { ::ClearLineClampEllipsis(this); }
void nsBlockFrame::Reflow(nsPresContext* aPresContext, ReflowOutput& aMetrics,
const ReflowInput& aReflowInput,
nsReflowStatus& aStatus) {
if (GetInFlowParent() && GetInFlowParent()->IsContentHiddenForLayout()) {
if (IsHiddenByContentVisibilityOfInFlowParentForLayout()) {
FinishAndStoreOverflow(&aMetrics, aReflowInput.mStyleDisplay);
return;
}
@ -2579,10 +2579,6 @@ static bool LinesAreEmpty(const nsLineList& aList) {
}
void nsBlockFrame::ReflowDirtyLines(BlockReflowState& aState) {
if (IsContentHiddenForLayout()) {
return;
}
bool keepGoing = true;
bool repositionViews = false; // should we really need this?
bool foundAnyClears = aState.mFloatBreakType != StyleClear::None;
@ -3369,6 +3365,15 @@ void nsBlockFrame::ReflowLine(BlockReflowState& aState, LineIterator aLine,
aLine->InvalidateCachedIsEmpty();
aLine->ClearHadFloatPushed();
// If this line contains a single block that is hidden by `content-visibility`
// don't reflow the line. If this line contains inlines and the first one is
// hidden by `content-visibility`, all of them are, so avoid reflow in that
// case as well.
nsIFrame* firstChild = aLine->mFirstChild;
if (firstChild->IsHiddenByContentVisibilityOfInFlowParentForLayout()) {
return;
}
// Now that we know what kind of line we have, reflow it
if (aLine->IsBlock()) {
ReflowBlockFrame(aState, aLine, aKeepReflowGoing);
@ -3577,6 +3582,10 @@ static inline bool IsNonAutoNonZeroBSize(const StyleSize& aCoord) {
/* virtual */
bool nsBlockFrame::IsSelfEmpty() {
if (IsHiddenByContentVisibilityOfInFlowParentForLayout()) {
return true;
}
// Blocks which are margin-roots (including inline-blocks) cannot be treated
// as empty for margin-collapsing and other purposes. They're more like
// replaced elements.

Просмотреть файл

@ -4532,7 +4532,7 @@ void nsFlexContainerFrame::Reflow(nsPresContext* aPresContext,
ReflowOutput& aReflowOutput,
const ReflowInput& aReflowInput,
nsReflowStatus& aStatus) {
if (GetInFlowParent() && GetInFlowParent()->IsContentHiddenForLayout()) {
if (IsHiddenByContentVisibilityOfInFlowParentForLayout()) {
return;
}
@ -5267,9 +5267,10 @@ std::tuple<nscoord, bool> nsFlexContainerFrame::ReflowChildren(
const LogicalMargin& aBorderPadding,
const nscoord aSumOfPrevInFlowsChildrenBlockSize,
const FlexboxAxisTracker& aAxisTracker, FlexLayoutResult& aFlr) {
if (IsContentHiddenForLayout()) {
if (HidesContentForLayout()) {
return {0, false};
}
// Before giving each child a final reflow, calculate the origin of the
// flex container's content box (with respect to its border-box), so that
// we can compute our flex item's final positions.
@ -5476,7 +5477,7 @@ void nsFlexContainerFrame::PopulateReflowOutput(
// as their baseline, or our content is hidden in which case, we'll use the
// wrong baseline (but no big deal).
NS_WARNING_ASSERTION(
IsContentHidden() || aLines[0].IsEmpty(),
HidesContentForLayout() || aLines[0].IsEmpty(),
"Have flex items but didn't get an ascent - that's odd (or there are "
"just gigantic sizes involved)");
// Per spec, synthesize baseline from the flex container's content box

Просмотреть файл

@ -8428,12 +8428,11 @@ nscoord nsGridContainerFrame::ReflowChildren(GridReflowInput& aState,
nsReflowStatus& aStatus) {
WritingMode wm = aState.mReflowInput->GetWritingMode();
nscoord bSize = aContentArea.BSize(wm);
if (IsContentHiddenForLayout()) {
return bSize;
}
MOZ_ASSERT(aState.mReflowInput);
MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!");
if (HidesContentForLayout()) {
return bSize;
}
OverflowAreas ocBounds;
nsReflowStatus ocStatus;
@ -8536,7 +8535,7 @@ void nsGridContainerFrame::Reflow(nsPresContext* aPresContext,
ReflowOutput& aDesiredSize,
const ReflowInput& aReflowInput,
nsReflowStatus& aStatus) {
if (GetInFlowParent() && GetInFlowParent()->IsContentHiddenForLayout()) {
if (IsHiddenByContentVisibilityOfInFlowParentForLayout()) {
return;
}

Просмотреть файл

@ -519,7 +519,7 @@ void nsHTMLCanvasFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder,
DisplayBorderBackgroundOutline(aBuilder, aLists);
if (IsContentHidden()) {
if (HidesContent()) {
DisplaySelectionOverlay(aBuilder, aLists.Content(),
nsISelectionDisplay::DISPLAY_IMAGES);
return;

Просмотреть файл

@ -378,7 +378,7 @@ bool nsIFrame::IsVisibleConsideringAncestors(uint32_t aFlags) const {
nsView* view = frame->GetView();
if (view && view->GetVisibility() == nsViewVisibility_kHide) return false;
if (this != frame && frame->IsContentHidden()) return false;
if (this != frame && frame->HidesContent()) return false;
nsIFrame* parent = frame->GetParent();
nsDeckFrame* deck = do_QueryFrame(parent);
@ -4019,7 +4019,7 @@ void nsIFrame::BuildDisplayListForChild(nsDisplayListBuilder* aBuilder,
return;
}
if (IsContentHidden()) {
if (HidesContent()) {
return;
}
@ -6750,7 +6750,7 @@ void nsIFrame::DidReflow(nsPresContext* aPresContext,
const ReflowInput* aReflowInput) {
NS_FRAME_TRACE(NS_FRAME_TRACE_CALLS, ("nsIFrame::DidReflow"));
if (GetInFlowParent() && GetInFlowParent()->IsContentHiddenForLayout()) {
if (IsHiddenByContentVisibilityOfInFlowParentForLayout()) {
return;
}
@ -6858,7 +6858,7 @@ bool nsIFrame::IsContentDisabled() const {
return element && element->IsDisabled();
}
bool nsIFrame::IsContentHidden() const {
bool nsIFrame::HidesContent() const {
if (!StyleDisplay()->IsContentVisibilityHidden()) {
return false;
}
@ -6866,18 +6866,30 @@ bool nsIFrame::IsContentHidden() const {
return IsFrameOfType(nsIFrame::eReplaced) || !StyleDisplay()->IsInlineFlow();
}
bool nsIFrame::IsContentHiddenForLayout() const {
return IsContentHidden() &&
!PresShell()->IsForcingLayoutForHiddenContent(this);
bool nsIFrame::HidesContentForLayout() const {
return HidesContent() && !PresShell()->IsForcingLayoutForHiddenContent(this);
}
bool nsIFrame::AncestorHidesContent() const {
bool nsIFrame::IsHiddenByContentVisibilityOfInFlowParentForLayout() const {
const auto* parent = GetInFlowParent();
return parent && parent->HidesContentForLayout() && !Style()->IsAnonBox();
}
bool nsIFrame::IsHiddenByContentVisibilityOnAnyAncestor() const {
if (!StaticPrefs::layout_css_content_visibility_enabled()) {
return false;
}
bool isAnonymousBox = Style()->IsAnonBox();
for (nsIFrame* cur = GetInFlowParent(); cur; cur = cur->GetInFlowParent()) {
if (cur->IsContentHidden()) {
// Anonymous boxes are not hidden by the content-visibility of their first
// non-anonymous ancestor, but can be hidden by ancestors further up the
// tree.
if (isAnonymousBox && !cur->Style()->IsAnonBox()) {
isAnonymousBox = false;
}
if (!isAnonymousBox && cur->HidesContent()) {
return true;
}
}
@ -8184,16 +8196,22 @@ bool nsIFrame::IsVisibleOrCollapsedForPainting() {
}
/* virtual */
bool nsIFrame::IsEmpty() { return false; }
bool nsIFrame::IsEmpty() {
return IsHiddenByContentVisibilityOfInFlowParentForLayout();
}
bool nsIFrame::CachedIsEmpty() {
MOZ_ASSERT(!HasAnyStateBits(NS_FRAME_IS_DIRTY),
"Must only be called on reflowed lines");
MOZ_ASSERT(!HasAnyStateBits(NS_FRAME_IS_DIRTY) ||
IsHiddenByContentVisibilityOfInFlowParentForLayout(),
"Must only be called on reflowed lines or those hidden by "
"content-visibility.");
return IsEmpty();
}
/* virtual */
bool nsIFrame::IsSelfEmpty() { return false; }
bool nsIFrame::IsSelfEmpty() {
return IsHiddenByContentVisibilityOfInFlowParentForLayout();
}
nsresult nsIFrame::GetSelectionController(nsPresContext* aPresContext,
nsISelectionController** aSelCon) {
@ -11637,7 +11655,7 @@ void nsIFrame::UpdateAnimationVisibility() {
return;
}
bool hidden = AncestorHidesContent();
bool hidden = IsHiddenByContentVisibilityOnAnyAncestor();
if (animationCollection) {
for (auto& animation : animationCollection->mAnimations) {
animation->SetHiddenByContentVisibility(hidden);

Просмотреть файл

@ -3146,22 +3146,30 @@ class nsIFrame : public nsQueryFrame {
bool IsContentDisabled() const;
/**
* Whether the content is hidden via the `content-visibilty` property.
* Whether this frame hides its contents via the `content-visibility`
* property.
*/
bool IsContentHidden() const;
bool HidesContent() const;
/**
* Whether the content is hidden via the `content-visibilty` property for
* layout. Hidden content might not be hidden for layout when forcing layout
* for size queries.
* Whether this frame hides its contents via the `content-visibility`
* property, while doing layout. This might be true when `HidesContent()` is
* true in the case that hidden content is being forced to lay out by position
* or size queries from script.
*/
bool IsContentHiddenForLayout() const;
bool HidesContentForLayout() const;
/**
* Returns true if this frame is entirely hidden due the `content-visibility`
* property on an ancestor.
*/
bool AncestorHidesContent() const;
bool IsHiddenByContentVisibilityOnAnyAncestor() const;
/**
* Returns true is this frame is hidden by its first unskipped in flow
* ancestor due to `content-visibility`.
*/
bool IsHiddenByContentVisibilityOfInFlowParentForLayout() const;
/**
* Get the "type" of the frame.

Просмотреть файл

@ -2315,7 +2315,7 @@ void nsImageFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder,
DisplayBorderBackgroundOutline(aBuilder, aLists);
if (IsContentHidden()) {
if (HidesContent()) {
DisplaySelectionOverlay(aBuilder, aLists.Content(),
nsISelectionDisplay::DISPLAY_IMAGES);
return;

Просмотреть файл

@ -356,7 +356,7 @@ void nsSubDocumentFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder,
return;
}
if (IsContentHidden()) {
if (HidesContent()) {
return;
}

Просмотреть файл

@ -720,7 +720,7 @@ void nsVideoFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder,
DisplayBorderBackgroundOutline(aBuilder, aLists);
if (IsContentHidden()) {
if (HidesContent()) {
return;
}

Просмотреть файл

@ -3605,7 +3605,7 @@ static nscoord Resolve(const StyleContainIntrinsicSize& aSize,
Maybe<float> lastSize = aAxis == eLogicalAxisBlock
? element->GetLastRememberedBSize()
: element->GetLastRememberedISize();
if (lastSize && aFrame.IsContentHidden()) {
if (lastSize && aFrame.HidesContent()) {
return CSSPixel::ToAppUnits(*lastSize);
}
}

Просмотреть файл

@ -1,12 +0,0 @@
[auto-007.html]
[.test 37]
expected: FAIL
[.test 16]
expected: FAIL
[.test 17]
expected: FAIL
[.test 18]
expected: FAIL

Просмотреть файл

@ -0,0 +1,14 @@
<!doctype HTML>
<html>
<meta charset="utf8">
<title>CSS Content Visibility: fieldset hiding content</title>
<link rel="author" title="Martin Robinson" href="mailto:mrobinson@igalia.com">
<link rel="help" href="https://drafts.csswg.org/css-contain/#content-visibility">
<style>
fieldset {
background: green;
}
</style>
<fieldset></fieldset>

Просмотреть файл

@ -0,0 +1,17 @@
<!doctype HTML>
<html>
<meta charset="utf8">
<title>CSS Content Visibility: fieldset hiding content</title>
<link rel="author" title="Martin Robinson" href="mailto:mrobinson@igalia.com">
<link rel="help" href="https://drafts.csswg.org/css-contain/#content-visibility">
<link rel="match" href="content-visibility-fieldset-size-ref.html">
<meta name="assert" content="A fieldset hiding content with content-visibility should be equivalent to an empty fieldset">
<style>
fieldset {
background: green;
content-visibility: hidden
}
</style>
<fieldset><legend>Hidden legend content</legend>Hidden fieldset content</fieldset>

Просмотреть файл

@ -143,7 +143,8 @@ static bool IsVisibleNode(const nsINode* aNode) {
return true;
}
if (frame->IsContentHidden() || frame->AncestorHidesContent()) {
if (frame->HidesContent() ||
frame->IsHiddenByContentVisibilityOnAnyAncestor()) {
return false;
}