Bug 1791759 - Add support for `content-visibility: auto` r=emilio

This change adds support for `content-visibilty: auto` as well as
showing and hiding content based on the relevancy of the content as
defined in the specification. Changes to relevancy are handled by
triggering updates in a set of `content-visibility: auto` frames stored
in PresShell at the appropriate time in the document lifecycle.

Some tests are now failing due to this feature exposing the failures,
but they will be fixed in later changes.

This change is a reland of an earlier version that properly updates
intrinsic sizes and triggers an update of remembered size for the
purposes of contain-intrinsic-size when content relevancy changes.

Co-authored-by: Jihye Hong <jihye@igalia.com>

Differential Revision: https://phabricator.services.mozilla.com/D159693
This commit is contained in:
Martin Robinson 2022-11-30 18:00:27 +00:00
Родитель 76bef44509
Коммит 281ff8ade5
37 изменённых файлов: 616 добавлений и 91 удалений

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

@ -14,6 +14,7 @@
#include "nsRefreshDriver.h"
#include "mozilla/PresShell.h"
#include "mozilla/StaticPrefs_dom.h"
#include "mozilla/StaticPrefs_layout.h"
#include "mozilla/ServoBindings.h"
#include "mozilla/dom/BrowserChild.h"
#include "mozilla/dom/BrowsingContext.h"
@ -150,6 +151,20 @@ static void LazyLoadCallback(
}
}
static void ContentVisibilityCallback(
const Sequence<OwningNonNull<DOMIntersectionObserverEntry>>& aEntries) {
for (const auto& entry : aEntries) {
entry->Target()->SetVisibleForContentVisibility(entry->IsIntersecting());
if (RefPtr<Document> doc = entry->Target()->GetComposedDoc()) {
if (RefPtr<PresShell> presShell = doc->GetPresShell()) {
presShell->ScheduleContentRelevancyUpdate(
ContentRelevancyReason::Visible);
}
}
}
}
static LengthPercentage PrefMargin(float aValue, bool aIsPercentage) {
return aIsPercentage ? LengthPercentage::FromPercentage(aValue / 100.0f)
: LengthPercentage::FromPixels(aValue);
@ -182,6 +197,25 @@ DOMIntersectionObserver::CreateLazyLoadObserver(Document& aDocument) {
return observer.forget();
}
already_AddRefed<DOMIntersectionObserver>
DOMIntersectionObserver::CreateContentVisibilityObserver(Document& aDocument) {
RefPtr<DOMIntersectionObserver> observer =
new DOMIntersectionObserver(aDocument, ContentVisibilityCallback);
observer->mThresholds.AppendElement(0.0f);
auto margin = LengthPercentage::FromPercentage(
StaticPrefs::layout_css_content_visibility_relevant_content_margin() /
100.0f);
observer->mRootMargin.Get(eSideTop) = margin;
observer->mRootMargin.Get(eSideRight) = margin;
observer->mRootMargin.Get(eSideBottom) = margin;
observer->mRootMargin.Get(eSideLeft) = margin;
return observer.forget();
}
bool DOMIntersectionObserver::SetRootMargin(const nsACString& aString) {
return Servo_IntersectionObserverRootMargin_Parse(&aString, &mRootMargin);
}

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

@ -163,6 +163,9 @@ class DOMIntersectionObserver final : public nsISupports,
static already_AddRefed<DOMIntersectionObserver>
CreateLazyLoadObserverViewport(Document&);
static already_AddRefed<DOMIntersectionObserver>
CreateContentVisibilityObserver(Document&);
protected:
void Connect();
void QueueIntersectionObserverEntry(Element* aTarget,

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

@ -2510,6 +2510,7 @@ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INTERNAL(Document)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mLazyLoadImageObserver)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mLazyLoadImageObserverViewport)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mLastRememberedSizeObserver)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mContentVisibilityObserver)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDOMImplementation)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mImageMaps)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOrientationPendingPromise)
@ -2628,6 +2629,7 @@ NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(Document)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mDisplayDocument)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mLazyLoadImageObserver)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mLazyLoadImageObserverViewport)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mContentVisibilityObserver)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mLastRememberedSizeObserver)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mFontFaceSet)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mReadyForIdle)
@ -15767,6 +15769,24 @@ DOMIntersectionObserver& Document::EnsureLazyLoadImageObserver() {
return *mLazyLoadImageObserver;
}
DOMIntersectionObserver& Document::EnsureContentVisibilityObserver() {
if (!mContentVisibilityObserver) {
mContentVisibilityObserver =
DOMIntersectionObserver::CreateContentVisibilityObserver(*this);
}
return *mContentVisibilityObserver;
}
void Document::ObserveForContentVisibility(Element& aElement) {
EnsureContentVisibilityObserver().Observe(aElement);
}
void Document::UnobserveForContentVisibility(Element& aElement) {
if (mContentVisibilityObserver) {
mContentVisibilityObserver->Unobserve(aElement);
}
}
ResizeObserver& Document::EnsureLastRememberedSizeObserver() {
if (!mLastRememberedSizeObserver) {
mLastRememberedSizeObserver =

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

@ -3768,6 +3768,10 @@ class Document : public nsINode,
}
DOMIntersectionObserver& EnsureLazyLoadImageObserver();
DOMIntersectionObserver& EnsureContentVisibilityObserver();
void ObserveForContentVisibility(Element&);
void UnobserveForContentVisibility(Element&);
ResizeObserver* GetLastRememberedSizeObserver() {
return mLastRememberedSizeObserver;
}
@ -5137,6 +5141,10 @@ class Document : public nsINode,
// Used to measure how effective the lazyload thresholds are.
RefPtr<DOMIntersectionObserver> mLazyLoadImageObserverViewport;
// Used for detecting when `content-visibility: auto` elements are near
// or far from the viewport.
RefPtr<DOMIntersectionObserver> mContentVisibilityObserver;
// ResizeObserver for storing and removing the last remembered size.
// @see {@link https://drafts.csswg.org/css-sizing-4/#last-remembered}
RefPtr<ResizeObserver> mLastRememberedSizeObserver;

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

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

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

@ -1333,6 +1333,29 @@ class Element : public FragmentOrElement {
return GetLastRememberedISize().isSome();
}
const Maybe<ContentRelevancy> GetContentRelevancy() const {
const auto* slots = GetExistingExtendedDOMSlots();
return slots ? slots->mContentRelevancy : Nothing();
}
void SetContentRelevancy(ContentRelevancy relevancy) {
ExtendedDOMSlots()->mContentRelevancy = Some(relevancy);
}
const Maybe<bool> GetVisibleForContentVisibility() const {
const auto* slots = GetExistingExtendedDOMSlots();
return slots ? slots->mVisibleForContentVisibility : Nothing();
}
void SetVisibleForContentVisibility(bool visible) {
ExtendedDOMSlots()->mVisibleForContentVisibility = Some(visible);
}
void ClearContentRelevancy() {
if (auto* slots = GetExistingExtendedDOMSlots()) {
slots->mContentRelevancy.reset();
slots->mVisibleForContentVisibility.reset();
}
}
// https://drafts.csswg.org/cssom-view-1/#dom-element-checkvisibility
MOZ_CAN_RUN_SCRIPT bool CheckVisibility(const CheckVisibilityOptions&);

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

@ -14,6 +14,7 @@
#define FragmentOrElement_h___
#include "mozilla/Attributes.h"
#include "mozilla/EnumSet.h"
#include "mozilla/MemoryReporting.h"
#include "mozilla/UniquePtr.h"
#include "nsCycleCollectionParticipant.h" // NS_DECL_CYCLE_*
@ -33,6 +34,8 @@ class nsIURI;
namespace mozilla {
class DeclarationBlock;
enum class ContentRelevancyReason;
using ContentRelevancy = EnumSet<ContentRelevancyReason, uint8_t>;
namespace dom {
struct CustomElementData;
class Element;
@ -198,6 +201,18 @@ class FragmentOrElement : public nsIContent {
*/
Maybe<float> mLastRememberedBSize;
Maybe<float> mLastRememberedISize;
/**
* Whether the content of this element is relevant for the purposes
* of `content-visibility: auto.
*/
Maybe<ContentRelevancy> mContentRelevancy;
/**
* Whether the content of this element is considered visible for
* the purposes of `content-visibility: auto.
*/
Maybe<bool> mVisibleForContentVisibility;
};
class nsDOMSlots : public nsIContent::nsContentSlots {

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

@ -9,6 +9,8 @@
*/
#include "mozilla/dom/Selection.h"
#include "LayoutConstants.h"
#include "mozilla/intl/BidiEmbeddingLevel.h"
#include "mozilla/AccessibleCaretEventHub.h"
@ -3200,6 +3202,7 @@ void Selection::NotifySelectionListeners() {
PresShell* presShell = GetPresShell();
if (presShell) {
doc = presShell->GetDocument();
presShell->ScheduleContentRelevancyUpdate(ContentRelevancyReason::Selected);
}
// We've notified all selection listeners even when some of them are removed

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

@ -8,6 +8,7 @@
#include "nsFocusManager.h"
#include "LayoutConstants.h"
#include "ChildIterator.h"
#include "nsIInterfaceRequestorUtils.h"
#include "nsGkAtoms.h"
@ -2882,6 +2883,9 @@ void nsFocusManager::FireFocusOrBlurEvent(EventMessage aEventMessage,
}
#endif
aPresShell->ScheduleContentRelevancyUpdate(
ContentRelevancyReason::FocusInSubtree);
nsContentUtils::AddScriptRunner(
new FocusBlurEvent(aTarget, aEventMessage, aPresShell->GetPresContext(),
aWindowRaised, aIsRefocus, aRelatedTarget));

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

@ -353,6 +353,18 @@ class nsRange final : public mozilla::dom::AbstractRange,
*/
MOZ_CAN_RUN_SCRIPT void NotifySelectionListenersAfterRangeSet();
/**
* For a range for which IsInSelection() is true, return the closest common
* inclusive ancestor
* (https://dom.spec.whatwg.org/#concept-tree-inclusive-ancestor)
* for the range, which we had to compute when the common ancestor changed or
* IsInSelection became true, so we could register with it. That is, it's a
* faster version of GetClosestCommonInclusiveAncestor that only works for
* ranges in a Selection. The method will assert and the behavior is undefined
* if called on a range where IsInSelection() is false.
*/
nsINode* GetRegisteredClosestCommonInclusiveAncestor();
protected:
/**
* https://dom.spec.whatwg.org/#concept-tree-inclusive-ancestor
@ -386,18 +398,6 @@ class nsRange final : public mozilla::dom::AbstractRange,
const mozilla::RangeBoundaryBase<EPT, ERT>& aEndBoundary,
nsINode* aRootNode, bool aNotInsertedYet = false);
/**
* For a range for which IsInSelection() is true, return the closest common
* inclusive ancestor
* (https://dom.spec.whatwg.org/#concept-tree-inclusive-ancestor)
* for the range, which we had to compute when the common ancestor changed or
* IsInSelection became true, so we could register with it. That is, it's a
* faster version of GetClosestCommonInclusiveAncestor that only works for
* ranges in a Selection. The method will assert and the behavior is undefined
* if called on a range where IsInSelection() is false.
*/
nsINode* GetRegisteredClosestCommonInclusiveAncestor();
// Assume that this is guaranteed that this is held by the caller when
// this is used. (Note that we cannot use AutoRestore for mCalledByJS
// due to a bit field.)

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

@ -92,6 +92,21 @@ inline constexpr nsSize kFallbackIntrinsicSize(kFallbackIntrinsicWidth,
*/
enum class IntrinsicISizeType { MinISize, PrefISize };
enum class ContentRelevancyReason {
// If the content of this Frame is on screen or nearly on screen.
Visible,
// If this Frame's element is a descendant of a top layer element.
DescendantOfTopLayerElement,
// If this Frame's element has focus in its subtree.
FocusInSubtree,
// If this Frame's content is part of a selection.
Selected,
};
using ContentRelevancy = EnumSet<ContentRelevancyReason, uint8_t>;
} // namespace mozilla
#endif // LayoutConstants_h___

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

@ -2278,6 +2278,8 @@ void PresShell::NotifyDestroyingFrame(nsIFrame* aFrame) {
mPendingScrollAnchorAdjustment.Remove(scrollableFrame);
mPendingScrollResnap.Remove(scrollableFrame);
}
mContentVisibilityAutoFrames.Remove(aFrame);
}
}
@ -3661,6 +3663,18 @@ nsresult PresShell::ScrollContentIntoView(nsIContent* aContent,
mContentToScrollTo = nullptr;
}
// If the target frame is an ancestor of a `content-visibility: auto`
// element ensure that it is laid out, so that the boundary rectangle is
// correct.
if (mContentToScrollTo) {
if (nsIFrame* frame = mContentToScrollTo->GetPrimaryFrame()) {
if (frame->IsHiddenByContentVisibilityOnAnyAncestor(
nsIFrame::IncludeContentVisibility::Auto)) {
frame->PresShell()->EnsureReflowIfFrameHasHiddenContent(frame);
}
}
}
// Flush layout and attempt to scroll in the process.
if (PresShell* presShell = composedDoc->GetPresShell()) {
presShell->SetNeedLayoutFlush();
@ -3703,7 +3717,9 @@ void PresShell::DoScrollContentIntoView() {
NS_ASSERTION(mDidInitialize, "should have done initial reflow by now");
nsIFrame* frame = mContentToScrollTo->GetPrimaryFrame();
if (!frame || frame->IsHiddenByContentVisibilityOnAnyAncestor()) {
if (!frame || frame->IsHiddenByContentVisibilityOnAnyAncestor(
nsIFrame::IncludeContentVisibility::Hidden)) {
mContentToScrollTo->RemoveProperty(nsGkAtoms::scrolling);
mContentToScrollTo = nullptr;
return;
@ -3730,10 +3746,6 @@ void PresShell::DoScrollContentIntoView() {
bool PresShell::ScrollFrameIntoView(
nsIFrame* aTargetFrame, const Maybe<nsRect>& aKnownRectRelativeToTarget,
ScrollAxis aVertical, ScrollAxis aHorizontal, ScrollFlags aScrollFlags) {
if (aTargetFrame->IsHiddenByContentVisibilityOnAnyAncestor()) {
return false;
}
// The scroll margin only applies to the whole bounds of the element, so don't
// apply it if we get an arbitrary rect / point to scroll to.
const nsMargin scrollMargin =
@ -4271,6 +4283,13 @@ void PresShell::DoFlushPendingNotifications(mozilla::ChangesToFlush aFlush) {
*/
FlushType flushType = aFlush.mFlushType;
// If this is a layout flush, first update the relevancy of any content
// of elements with `content-visibility: auto` so that the values
// returned from script queries are up-to-date.
if (flushType >= mozilla::FlushType::Layout) {
UpdateRelevancyOfContentVisibilityAutoFrames();
}
MOZ_ASSERT(NeedFlush(flushType), "Why did we get called?");
AUTO_PROFILER_MARKER_TEXT(
@ -11898,3 +11917,28 @@ void PresShell::EnsureReflowIfFrameHasHiddenContent(nsIFrame* aFrame) {
bool PresShell::IsForcingLayoutForHiddenContent(const nsIFrame* aFrame) const {
return mHiddenContentInForcedLayout.Contains(aFrame->GetContent());
}
void PresShell::UpdateRelevancyOfContentVisibilityAutoFrames() {
if (mContentVisibilityRelevancyToUpdate.isEmpty()) {
return;
}
for (nsIFrame* frame : mContentVisibilityAutoFrames) {
frame->UpdateIsRelevantContent(mContentVisibilityRelevancyToUpdate);
}
mContentVisibilityRelevancyToUpdate.clear();
}
void PresShell::ScheduleContentRelevancyUpdate(ContentRelevancyReason aReason) {
if (MOZ_UNLIKELY(mIsDestroying)) {
return;
}
mContentVisibilityRelevancyToUpdate += aReason;
SetNeedLayoutFlush();
if (nsPresContext* presContext = GetPresContext()) {
presContext->RefreshDriver()->EnsureContentRelevancyUpdateHappens();
}
}

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

@ -13,6 +13,7 @@
#include <stdio.h> // for FILE definition
#include "FrameMetrics.h"
#include "LayoutConstants.h"
#include "TouchManager.h"
#include "Units.h"
#include "Visibility.h"
@ -1749,6 +1750,13 @@ class PresShell final : public nsStubDocumentObserver,
*/
bool IsForcingLayoutForHiddenContent(const nsIFrame*) const;
void RegisterContentVisibilityAutoFrame(nsIFrame* aFrame) {
mContentVisibilityAutoFrames.Insert(aFrame);
}
void UpdateRelevancyOfContentVisibilityAutoFrames();
void ScheduleContentRelevancyUpdate(ContentRelevancyReason aReason);
private:
~PresShell();
@ -3009,6 +3017,12 @@ class PresShell final : public nsStubDocumentObserver,
nsTHashSet<nsIContent*> mHiddenContentInForcedLayout;
nsTHashSet<nsIFrame*> mContentVisibilityAutoFrames;
// The type of content relevancy to update the next time content relevancy
// updates are triggered for `content-visibility: auto` frames.
ContentRelevancy mContentVisibilityRelevancyToUpdate;
nsCallbackEventRequest* mFirstCallbackEventRequest = nullptr;
nsCallbackEventRequest* mLastCallbackEventRequest = nullptr;

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

@ -1339,6 +1339,7 @@ nsRefreshDriver::nsRefreshDriver(nsPresContext* aPresContext)
mResizeSuppressed(false),
mNotifyDOMContentFlushed(false),
mNeedToUpdateIntersectionObservations(false),
mNeedToUpdateContentRelevancy(false),
mInNormalTick(false),
mAttemptedExtraTickSinceLastVsync(false),
mHasExceededAfterLoadTickPeriod(false),
@ -1938,6 +1939,9 @@ auto nsRefreshDriver::GetReasonsToTick() const -> TickReasons {
if (mNeedToUpdateIntersectionObservations) {
reasons |= TickReasons::eNeedsToUpdateIntersectionObservations;
}
if (mNeedToUpdateContentRelevancy) {
reasons |= TickReasons::eNeedsToUpdateContentRelevancy;
}
if (!mVisualViewportResizeEvents.IsEmpty()) {
reasons |= TickReasons::eHasVisualViewportResizeEvents;
}
@ -1968,6 +1972,9 @@ void nsRefreshDriver::AppendTickReasonsToString(TickReasons aReasons,
if (aReasons & TickReasons::eNeedsToUpdateIntersectionObservations) {
aStr.AppendLiteral(" NeedsToUpdateIntersectionObservations");
}
if (aReasons & TickReasons::eNeedsToUpdateContentRelevancy) {
aStr.AppendLiteral(" NeedsToUpdateContentRelevancy");
}
if (aReasons & TickReasons::eHasVisualViewportResizeEvents) {
aStr.AppendLiteral(" HasVisualViewportResizeEvents");
}
@ -2165,6 +2172,25 @@ void nsRefreshDriver::UpdateIntersectionObservations(TimeStamp aNowTime) {
mNeedToUpdateIntersectionObservations = false;
}
void nsRefreshDriver::UpdateRelevancyOfContentVisibilityAutoFrames() {
if (!mNeedToUpdateContentRelevancy) {
return;
}
if (RefPtr<PresShell> topLevelPresShell = mPresContext->GetPresShell()) {
topLevelPresShell->UpdateRelevancyOfContentVisibilityAutoFrames();
}
mPresContext->Document()->EnumerateSubDocuments([](Document& aSubDoc) {
if (PresShell* presShell = aSubDoc.GetPresShell()) {
presShell->UpdateRelevancyOfContentVisibilityAutoFrames();
}
return CallState::Continue;
});
mNeedToUpdateContentRelevancy = false;
}
void nsRefreshDriver::DispatchAnimationEvents() {
if (!mPresContext) {
return;
@ -2649,6 +2675,14 @@ void nsRefreshDriver::Tick(VsyncId aId, TimeStamp aNowTime,
pm->UpdatePopupPositions(this);
}
// Update the relevancy of the content of any `content-visibility: auto`
// elements. The specification says: "Specifically, such changes will
// take effect between steps 13 and 14 of Update the Rendering step of
// the Processing Model (between “run the animation frame callbacks” and
// “run the update intersection observations steps”)."
// https://drafts.csswg.org/css-contain/#cv-notes
UpdateRelevancyOfContentVisibilityAutoFrames();
UpdateIntersectionObservations(aNowTime);
/*

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

@ -417,6 +417,11 @@ class nsRefreshDriver final : public mozilla::layers::TransactionIdAllocator,
mNeedToUpdateIntersectionObservations = true;
}
void EnsureContentRelevancyUpdateHappens() {
EnsureTimerStarted();
mNeedToUpdateContentRelevancy = true;
}
// Register a composition payload that will be forwarded to the layer manager
// if the current or upcoming refresh tick does a paint.
// If no paint happens, the payload is discarded.
@ -429,9 +434,10 @@ class nsRefreshDriver final : public mozilla::layers::TransactionIdAllocator,
eHasObservers = 1 << 0,
eHasImageRequests = 1 << 1,
eNeedsToUpdateIntersectionObservations = 1 << 2,
eHasVisualViewportResizeEvents = 1 << 3,
eHasScrollEvents = 1 << 4,
eHasVisualViewportScrollEvents = 1 << 5,
eNeedsToUpdateContentRelevancy = 1 << 3,
eHasVisualViewportResizeEvents = 1 << 4,
eHasScrollEvents = 1 << 5,
eHasVisualViewportScrollEvents = 1 << 6,
};
void AddForceNotifyContentfulPaintPresContext(nsPresContext* aPresContext);
@ -473,6 +479,7 @@ class nsRefreshDriver final : public mozilla::layers::TransactionIdAllocator,
MOZ_CAN_RUN_SCRIPT
void RunFrameRequestCallbacks(mozilla::TimeStamp aNowTime);
void UpdateIntersectionObservations(mozilla::TimeStamp aNowTime);
void UpdateRelevancyOfContentVisibilityAutoFrames();
enum class IsExtraTick {
No,
@ -611,6 +618,10 @@ class nsRefreshDriver final : public mozilla::layers::TransactionIdAllocator,
// all our documents.
bool mNeedToUpdateIntersectionObservations : 1;
// True if we need to update the relevancy of `content-visibility: auto`
// elements in our documents.
bool mNeedToUpdateContentRelevancy : 1;
// True if we're currently within the scope of Tick() handling a normal
// (timer-driven) tick.
bool mInNormalTick : 1;

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

@ -177,7 +177,8 @@ nsDisplayWrapList* ViewportFrame::BuildDisplayListForTopLayer(
continue;
}
if (frame->IsHiddenByContentVisibilityOnAnyAncestor()) {
if (frame->IsHiddenByContentVisibilityOnAnyAncestor(
nsIFrame::IncludeContentVisibility::Hidden)) {
continue;
}

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

@ -46,6 +46,7 @@
#include "nsCOMPtr.h"
#include "nsFieldSetFrame.h"
#include "nsFlexContainerFrame.h"
#include "nsFocusManager.h"
#include "nsFrameList.h"
#include "nsPlaceholderFrame.h"
#include "nsIBaseWindow.h"
@ -381,7 +382,12 @@ bool nsIFrame::IsVisibleConsideringAncestors(uint32_t aFlags) const {
return false;
}
if (this != frame && frame->HidesContent()) {
// This method is used to determine if a frame is focusable, because it's
// called by nsIFrame::IsFocusable. `content-visibility: auto` should not
// force this frame to be unfocusable, so we only take into account
// `content-visibility: hidden` here.
if (this != frame &&
frame->HidesContent(IncludeContentVisibility::Hidden)) {
return false;
}
@ -780,6 +786,16 @@ void nsIFrame::Init(nsIContent* aContent, nsContainerFrame* aParent,
UpdateVisibleDescendantsState();
}
if (disp->IsContentVisibilityAuto() &&
IsContentVisibilityPropertyApplicable()) {
PresShell()->RegisterContentVisibilityAutoFrame(this);
auto* element = Element::FromNodeOrNull(GetContent());
MOZ_ASSERT(element);
PresContext()->Document()->ObserveForContentVisibility(*element);
} else if (auto* element = Element::FromNodeOrNull(GetContent())) {
element->ClearContentRelevancy();
}
// TODO(mrobinson): Once bug 1765615 is fixed, this should be called on
// layout changes. In addition, when `content-visibility: auto` is implemented
// this should also be called when scrolling or focus causes content to be
@ -855,6 +871,13 @@ void nsIFrame::DestroyFrom(nsIFrame* aDestructRoot,
}
}
if (disp->IsContentVisibilityAuto() &&
IsContentVisibilityPropertyApplicable()) {
if (auto* element = Element::FromNodeOrNull(GetContent())) {
PresContext()->Document()->UnobserveForContentVisibility(*element);
}
}
// Disable visibility tracking. Note that we have to do this before we clear
// frame properties and lose track of whether we were previously visible.
// XXX(seth): It'd be ideal to assert that we're already marked nonvisible
@ -6832,12 +6855,53 @@ bool nsIFrame::IsContentDisabled() const {
return element && element->IsDisabled();
}
bool nsIFrame::HidesContent() const {
if (!StyleDisplay()->IsContentVisibilityHidden()) {
bool nsIFrame::IsContentVisibilityPropertyApplicable() const {
return GetContent() && GetContent()->IsElement() &&
(!StyleDisplay()->IsInlineFlow() ||
IsFrameOfType(nsIFrame::eReplaced));
}
bool nsIFrame::IsContentRelevant() const {
MOZ_ASSERT(IsContentVisibilityPropertyApplicable());
MOZ_ASSERT(StyleDisplay()->IsContentVisibilityAuto());
auto* element = Element::FromNodeOrNull(GetContent());
MOZ_ASSERT(element);
Maybe<ContentRelevancy> relevancy = element->GetContentRelevancy();
if (relevancy.isSome()) {
return !relevancy->isEmpty();
}
// If there is no relevancy set, then this frame still has not received had
// the initial visibility callback call. In that case, only rely on whether
// or not it is inside a top layer element which will never change for this
// frame and allows proper rendering of the top layer.
return IsDescendantOfTopLayerElement();
}
bool nsIFrame::HidesContent(
const EnumSet<IncludeContentVisibility>& aInclude) const {
const auto& disp = *StyleDisplay();
if (disp.IsContentVisibilityVisible()) {
return false;
};
if (!IsContentVisibilityPropertyApplicable()) {
return false;
}
return IsFrameOfType(nsIFrame::eReplaced) || !StyleDisplay()->IsInlineFlow();
if (aInclude.contains(IncludeContentVisibility::Hidden) &&
disp.IsContentVisibilityHidden()) {
return true;
}
if (aInclude.contains(IncludeContentVisibility::Auto) &&
disp.IsContentVisibilityAuto()) {
return !IsContentRelevant();
}
return false;
}
bool nsIFrame::HidesContentForLayout() const {
@ -6850,7 +6914,8 @@ bool nsIFrame::IsHiddenByContentVisibilityOfInFlowParentForLayout() const {
!(Style()->IsAnonBox() && !IsFrameOfType(nsIFrame::eLineParticipant));
}
bool nsIFrame::IsHiddenByContentVisibilityOnAnyAncestor() const {
bool nsIFrame::IsHiddenByContentVisibilityOnAnyAncestor(
const EnumSet<IncludeContentVisibility>& aInclude) const {
if (!StaticPrefs::layout_css_content_visibility_enabled()) {
return false;
}
@ -6858,7 +6923,7 @@ bool nsIFrame::IsHiddenByContentVisibilityOnAnyAncestor() const {
bool isAnonymousBlock =
Style()->IsAnonBox() && !IsFrameOfType(nsIFrame::eLineParticipant);
for (nsIFrame* cur = GetInFlowParent(); cur; cur = cur->GetInFlowParent()) {
if (!isAnonymousBlock && cur->HidesContent()) {
if (!isAnonymousBlock && cur->HidesContent(aInclude)) {
return true;
}
@ -6871,6 +6936,111 @@ bool nsIFrame::IsHiddenByContentVisibilityOnAnyAncestor() const {
return false;
}
bool nsIFrame::HasSelectionInSubtree() {
if (IsSelected()) {
return true;
}
RefPtr<nsFrameSelection> frameSelection = GetFrameSelection();
const Selection* selection =
frameSelection->GetSelection(SelectionType::eNormal);
if (!selection) {
return false;
}
for (uint32_t i = 0; i < selection->RangeCount(); i++) {
auto* range = selection->GetRangeAt(i);
MOZ_ASSERT(range);
const auto* commonAncestorNode =
range->GetRegisteredClosestCommonInclusiveAncestor();
if (commonAncestorNode->IsInclusiveDescendantOf(GetContent())) {
return true;
}
}
return false;
}
bool nsIFrame::IsDescendantOfTopLayerElement() const {
if (!GetContent()) {
return false;
}
nsTArray<dom::Element*> topLayer = PresContext()->Document()->GetTopLayer();
for (auto* element : topLayer) {
if (GetContent()->IsInclusiveFlatTreeDescendantOf(element)) {
return true;
}
}
return false;
}
void nsIFrame::UpdateIsRelevantContent(
const ContentRelevancy& aRelevancyToUpdate) {
MOZ_ASSERT(IsContentVisibilityPropertyApplicable());
MOZ_ASSERT(StyleDisplay()->IsContentVisibilityAuto());
auto* element = Element::FromNodeOrNull(GetContent());
MOZ_ASSERT(element);
ContentRelevancy newRelevancy;
Maybe<ContentRelevancy> oldRelevancy = element->GetContentRelevancy();
if (oldRelevancy.isSome()) {
newRelevancy = *oldRelevancy;
}
auto setRelevancyValue = [&](ContentRelevancyReason reason, bool value) {
if (value) {
newRelevancy += reason;
} else {
newRelevancy -= reason;
}
};
if (!oldRelevancy ||
aRelevancyToUpdate.contains(ContentRelevancyReason::Visible)) {
Maybe<bool> visible = element->GetVisibleForContentVisibility();
if (visible.isSome()) {
setRelevancyValue(ContentRelevancyReason::Visible, *visible);
}
}
if (!oldRelevancy ||
aRelevancyToUpdate.contains(ContentRelevancyReason::FocusInSubtree)) {
setRelevancyValue(ContentRelevancyReason::FocusInSubtree,
element->State().HasAtLeastOneOfStates(
ElementState::FOCUS_WITHIN | ElementState::FOCUS));
}
if (!oldRelevancy ||
aRelevancyToUpdate.contains(ContentRelevancyReason::Selected)) {
setRelevancyValue(ContentRelevancyReason::Selected,
HasSelectionInSubtree());
}
if (!oldRelevancy ||
aRelevancyToUpdate.contains(
ContentRelevancyReason::DescendantOfTopLayerElement)) {
setRelevancyValue(ContentRelevancyReason::DescendantOfTopLayerElement,
IsDescendantOfTopLayerElement());
}
bool overallRelevancyChanged =
!oldRelevancy || oldRelevancy->isEmpty() != newRelevancy.isEmpty();
if (!oldRelevancy || *oldRelevancy != newRelevancy) {
element->SetContentRelevancy(newRelevancy);
}
if (overallRelevancyChanged) {
HandleLastRememberedSize();
PresShell()->FrameNeedsReflow(this, IntrinsicDirty::StyleChange,
NS_FRAME_IS_DIRTY);
InvalidateFrame();
}
}
nsresult nsIFrame::CharacterDataChanged(const CharacterDataChangeInfo&) {
MOZ_ASSERT_UNREACHABLE("should only be called for text frames");
return NS_OK;

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

@ -536,6 +536,7 @@ class nsIFrame : public nsQueryFrame {
using ReflowOutput = mozilla::ReflowOutput;
using Visibility = mozilla::Visibility;
using LengthPercentage = mozilla::LengthPercentage;
using ContentRelevancy = mozilla::ContentRelevancy;
using nsDisplayItem = mozilla::nsDisplayItem;
using nsDisplayList = mozilla::nsDisplayList;
@ -3180,11 +3181,34 @@ class nsIFrame : public nsQueryFrame {
*/
bool IsContentDisabled() const;
/**
* Whether the content-visibility CSS property applies to this frame.
*/
bool IsContentVisibilityPropertyApplicable() const;
enum class IncludeContentVisibility {
Auto,
Hidden,
};
constexpr static mozilla::EnumSet<IncludeContentVisibility>
IncludeAllContentVisibility() {
return {IncludeContentVisibility::Auto, IncludeContentVisibility::Hidden};
}
/**
* Returns true if this frame's `content-visibility: auto` element is
* considered relevant content.
*/
bool IsContentRelevant() const;
/**
* Whether this frame hides its contents via the `content-visibility`
* property.
* @param aInclude specifies what kind of `content-visibility` to include.
*/
bool HidesContent() const;
bool HidesContent(const mozilla::EnumSet<IncludeContentVisibility>& =
IncludeAllContentVisibility()) const;
/**
* Whether this frame hides its contents via the `content-visibility`
@ -3197,8 +3221,11 @@ class nsIFrame : public nsQueryFrame {
/**
* Returns true if this frame is entirely hidden due the `content-visibility`
* property on an ancestor.
* @param aInclude specifies what kind of `content-visibility` to include.
*/
bool IsHiddenByContentVisibilityOnAnyAncestor() const;
bool IsHiddenByContentVisibilityOnAnyAncestor(
const mozilla::EnumSet<IncludeContentVisibility>& =
IncludeAllContentVisibility()) const;
/**
* Returns true is this frame is hidden by its first unskipped in flow
@ -3206,6 +3233,27 @@ class nsIFrame : public nsQueryFrame {
*/
bool IsHiddenByContentVisibilityOfInFlowParentForLayout() const;
/**
* Whether or not this frame's content is a descendant of a top layer element
* used to determine if this frame is relevant content for
* `content-visibility: auto`.
*/
bool IsDescendantOfTopLayerElement() const;
/**
* Returns true if this frame has a SelectionType::eNormal type selection in
* somewhere in its subtree of frames. This is used to determine content
* relevancy for `content-visibility: auto`.
*/
bool HasSelectionInSubtree();
/**
* Update the whether or not this frame is considered relevant content for the
* purposes of `content-visibility: auto` according to the rules specified in
* https://drafts.csswg.org/css-contain-2/#relevant-to-the-user.
*/
void UpdateIsRelevantContent(const ContentRelevancy& aRelevancyToUpdate);
/**
* Get the "type" of the frame.
*

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

@ -1612,6 +1612,10 @@ struct MOZ_NEEDS_MEMMOVABLE_MEMBERS nsStyleDisplay {
return mContentVisibility == StyleContentVisibility::Hidden;
}
bool IsContentVisibilityAuto() const {
return mContentVisibility == StyleContentVisibility::Auto;
}
/* Returns whether the element has the transform property or a related
* property. */
bool HasTransformStyle() const {

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

@ -8655,6 +8655,12 @@
value: true
mirror: always
# The margin used for detecting relevancy for `content-visibility: auto`.
- name: layout.css.content-visibility-relevant-content-margin
type: float
value: 50 # 50%
mirror: always
# Whether to block large cursors intersecting UI.
- name: layout.cursor.block.enabled
type: bool

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

@ -1,2 +1,2 @@
prefs: [layout.css.content-visibility.enabled:true]
prefs: [layout.css.content-visibility.enabled:true, layout.css.contain-intrinsic-size.enabled:true]
leak-threshold: [rdd:51200]

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

@ -0,0 +1,3 @@
[content-visibility-058.html]
bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1797467
expected: FAIL

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

@ -1,3 +1,3 @@
[content-visibility-064.html]
fuzzy:
if os == "mac": maxDifference=0-11;totalPixels=0-44
bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1800868
expected: FAIL

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

@ -2,4 +2,5 @@
expected:
if (os == "android") and fission: [OK, TIMEOUT]
[Content Visibility: off-screen focus]
bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1797447
expected: FAIL

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

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

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

@ -1,5 +0,0 @@
[content-visibility-070.html]
expected:
if (os == "android") and fission: [OK, TIMEOUT]
[Content Visibility: off-screen selection]
expected: FAIL

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

@ -1,20 +0,0 @@
[content-visibility-071.html]
expected:
if (os == "android") and fission: [OK, TIMEOUT]
[Range flipped from back to front: ]
expected: FAIL
[Range goes back and forth: ]
expected: FAIL
[Range flipped from front to back: ]
expected: FAIL
[Range shrunk to cover fewer elements: ]
expected: FAIL
[Range extended to cover more elements: ]
expected: FAIL
[One elements selected: ]
expected: FAIL

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

@ -1,12 +0,0 @@
[content-visibility-072.html]
[two.getBoundingClientRect(): ]
expected: FAIL
[three.getBoundingClientRect(): ]
expected: FAIL
[four.getBoundingClientRect(): ]
expected: FAIL
[five.getBoundingClientRect(): ]
expected: FAIL

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

@ -0,0 +1,3 @@
[content-visibility-with-top-layer-005.html]
fuzzy:
if os == "win": maxDifference=0-92;totalPixels=0-2

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

@ -1,5 +0,0 @@
[auto-003.html]
expected:
if (os == "android") and fission: [OK, TIMEOUT]
[contain-intrinsic-size: auto]
expected: FAIL

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

@ -1,5 +0,0 @@
[auto-012.html]
[Still sizing with last remembered size]
expected: FAIL
[Sizing with new last remembered size]
expected: FAIL

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

@ -59,8 +59,8 @@ async_test((t) => {
function step2() {
const r = container.getBoundingClientRect();
t.step(() => {
assert_equals(r.y, 3000, "step3 offset");
assert_equals(r.height, 10, "step3 height");
assert_equals(r.y, 3000, "step2 offset");
assert_equals(r.height, 10, "step2 height");
});
document.scrollingElement.scrollTop = 3000;
requestAnimationFrame(step3);
@ -69,8 +69,8 @@ async_test((t) => {
function step3() {
const r = container.getBoundingClientRect();
t.step(() => {
assert_less_than(r.y, 3000, "step2 offset");
assert_equals(r.height, 10, "step2 height");
assert_less_than(r.y, 3000, "step3 offset");
assert_equals(r.height, 10, "step3 height");
});
document.scrollingElement.scrollTop = 0;
requestAnimationFrame(step4);
@ -88,8 +88,8 @@ async_test((t) => {
function step5() {
const r = container.getBoundingClientRect();
t.step(() => {
assert_equals(r.y, 3000, "step4 offset");
assert_equals(r.height, 10, "step4 height");
assert_equals(r.y, 3000, "step5 offset");
assert_equals(r.height, 10, "step5 height");
});
selection.removeAllRanges();

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

@ -0,0 +1,25 @@
<!doctype HTML>
<html>
<meta charset="utf8">
<title>CSS Content Visibility: auto container in an iframe (reference)</title>
<link rel="author" title="Martin Robinson" href="mailto:mrobinson@igalia.com">
<link rel="help" href="https://drafts.csswg.org/css-contain/#content-visibility">
<iframe id="frame" srcdoc='
<style>
#container {
width: 200px;
height: 200px;
}
#child {
width: 100px;
height: 100px;
background: green;
}
</style>
<div id="container">
<div id="child"></div>
</div>
hello
'></iframe>
</html>

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

@ -0,0 +1,28 @@
<!doctype HTML>
<html>
<meta charset="utf8">
<title>CSS Content Visibility: auto container in an iframe</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-auto-in-iframe-ref.html">
<meta name="assert" content="content-visibility: auto shows on screen iframe contents">
<iframe id="frame" srcdoc='
<style>
#container {
width: 200px;
height: 200px;
content-visibility: auto;
}
#child {
width: 100px;
height: 100px;
background: green;
}
</style>
<div id="container">
<div id="child"></div>
</div>
hello
'></iframe>
</html>

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

@ -0,0 +1,25 @@
<!doctype HTML>
<html>
<meta charset="utf8">
<title>Content Visibility: Elements with content-visibility: auto and intrinsic width should render correctly</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="container-ref.html">
<meta name="assert" content="Elements with content-visibility: auto and intrinsic width should render correctly">
<style>
#container {
content-visibility: auto;
width: max-content;
background: lightblue;
}
#child {
width: 150px;
height: 150px;
}
</style>
<div id="container">
<div id="child"></div>
</div>

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

@ -0,0 +1,31 @@
<!doctype html>
<html class="reftest-wait">
<meta charset="utf8">
<title>CSS Content Visibility: dialog shows under c-v auto</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="spacer-with-top-layer-ref.html">
<meta name="assert" content="top layer dialogs render under c-v auto">
<script src="/common/reftest-wait.js"></script>
<style>
.box { width: 100px; height: 100px; border: 1px solid black; }
.auto { content-visibility: auto }
.spacer { width: 10px; height: 3000px; background: lightblue; }
</style>
<div class=spacer></div>
<div id=container class="box">
content
<dialog id=dialog><div id=inner class="auto">PASS</div></dialog>
</div>
<script>
function runTest() {
dialog.showModal();
takeScreenshot();
}
onload = () => requestAnimationFrame(() => requestAnimationFrame(runTest));
</script>

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

@ -143,8 +143,9 @@ static bool IsVisibleNode(const nsINode* aNode) {
return true;
}
if (frame->HidesContent() ||
frame->IsHiddenByContentVisibilityOnAnyAncestor()) {
if (frame->HidesContent(nsIFrame::IncludeContentVisibility::Hidden) ||
frame->IsHiddenByContentVisibilityOnAnyAncestor(
nsIFrame::IncludeContentVisibility::Hidden)) {
return false;
}