Bug 1777478 - Animations in content hidden by `content-visibility` should not fire events or resolve promises r=hiro

Keep a separate list of animations in the timeline that are hidden by
content visibility. This allows the DocumentTimeline to disconnect from
the refresh driver when all animations are hidden and prevents ticking
hidden animations in general.

Differential Revision: https://phabricator.services.mozilla.com/D150764
This commit is contained in:
Martin Robinson 2022-07-19 09:57:21 +00:00
Родитель e08e5475c4
Коммит 2fbf04543f
9 изменённых файлов: 103 добавлений и 13 удалений

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

@ -1965,6 +1965,20 @@ bool Animation::IsInEffect() const {
return GetEffect() && GetEffect()->IsInEffect();
}
void Animation::SetHiddenByContentVisibility(bool hidden) {
if (mHiddenByContentVisibility == hidden) {
return;
}
mHiddenByContentVisibility = hidden;
if (!GetTimeline()) {
return;
}
GetTimeline()->NotifyAnimationContentVisibilityChanged(this, !hidden);
}
StickyTimeDuration Animation::IntervalStartTime(
const StickyTimeDuration& aActiveDuration) const {
MOZ_ASSERT(AsCSSTransition() || AsCSSAnimation(),

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

@ -474,6 +474,11 @@ class Animation : public DOMEventTargetHelper,
mPlaybackRate);
}
void SetHiddenByContentVisibility(bool hidden);
bool IsHiddenByContentVisibility() const {
return mHiddenByContentVisibility;
}
protected:
void SilentlySetCurrentTime(const TimeDuration& aNewCurrentTime);
void CancelNoUpdate();
@ -641,6 +646,8 @@ class Animation : public DOMEventTargetHelper,
bool mFinishedAtLastComposeStyle = false;
bool mWasReplaceableAtLastTick = false;
bool mHiddenByContentVisibility = false;
// Indicates that the animation should be exposed in an element's
// getAnimations() list.
bool mIsRelevant = false;

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

@ -42,6 +42,8 @@ bool AnimationTimeline::Tick() {
for (Animation* animation = mAnimationOrder.getFirst(); animation;
animation =
static_cast<LinkedListElement<Animation>*>(animation)->getNext()) {
MOZ_ASSERT(!animation->IsHiddenByContentVisibility());
// Skip any animations that are longer need associated with this timeline.
if (animation->GetTimeline() != this) {
// If animation has some other timeline, it better not be also in the
@ -74,7 +76,9 @@ void AnimationTimeline::NotifyAnimationUpdated(Animation& aAnimation) {
if (aAnimation.GetTimeline() && aAnimation.GetTimeline() != this) {
aAnimation.GetTimeline()->RemoveAnimation(&aAnimation);
}
mAnimationOrder.insertBack(&aAnimation);
if (!aAnimation.IsHiddenByContentVisibility()) {
mAnimationOrder.insertBack(&aAnimation);
}
}
}
@ -86,4 +90,15 @@ void AnimationTimeline::RemoveAnimation(Animation* aAnimation) {
mAnimations.Remove(aAnimation);
}
void AnimationTimeline::NotifyAnimationContentVisibilityChanged(
Animation* aAnimation, bool visible) {
bool inList =
static_cast<LinkedListElement<Animation>*>(aAnimation)->isInList();
if (visible && !inList) {
mAnimationOrder.insertBack(aAnimation);
} else if (!visible && inList) {
static_cast<LinkedListElement<Animation>*>(aAnimation)->remove();
}
}
} // namespace mozilla::dom

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

@ -98,6 +98,8 @@ class AnimationTimeline : public nsISupports, public nsWrapperCache {
bool HasAnimations() const { return !mAnimations.IsEmpty(); }
virtual void RemoveAnimation(Animation* aAnimation);
virtual void NotifyAnimationContentVisibilityChanged(Animation* aAnimation,
bool visible);
virtual Document* GetDocument() const = 0;
@ -119,7 +121,8 @@ class AnimationTimeline : public nsISupports, public nsWrapperCache {
// Animations observing this timeline
//
// We store them in (a) a hashset for quick lookup, and (b) an array
// to maintain a fixed sampling order.
// to maintain a fixed sampling order. Animations that are hidden by
// `content-visibility` are not sampled and will only be in the hashset.
//
// The hashset keeps a strong reference to each animation since
// dealing with addref/release with LinkedList is difficult.

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

@ -155,7 +155,7 @@ Nullable<TimeDuration> DocumentTimeline::ToTimelineTime(
void DocumentTimeline::NotifyAnimationUpdated(Animation& aAnimation) {
AnimationTimeline::NotifyAnimationUpdated(aAnimation);
if (!mIsObservingRefreshDriver) {
if (!mIsObservingRefreshDriver && !mAnimationOrder.isEmpty()) {
nsRefreshDriver* refreshDriver = GetRefreshDriver();
if (refreshDriver) {
MOZ_ASSERT(isInList(),
@ -245,9 +245,32 @@ void DocumentTimeline::NotifyRefreshDriverDestroying(nsRefreshDriver* aDriver) {
void DocumentTimeline::RemoveAnimation(Animation* aAnimation) {
AnimationTimeline::RemoveAnimation(aAnimation);
if (mIsObservingRefreshDriver && mAnimations.IsEmpty()) {
if (!mIsObservingRefreshDriver || !mAnimationOrder.isEmpty()) {
return;
}
UnregisterFromRefreshDriver();
}
void DocumentTimeline::NotifyAnimationContentVisibilityChanged(
Animation* aAnimation, bool visible) {
AnimationTimeline::NotifyAnimationContentVisibilityChanged(aAnimation,
visible);
if (mIsObservingRefreshDriver && mAnimationOrder.isEmpty()) {
UnregisterFromRefreshDriver();
}
if (!mIsObservingRefreshDriver && !mAnimationOrder.isEmpty()) {
nsRefreshDriver* refreshDriver = GetRefreshDriver();
if (refreshDriver) {
MOZ_ASSERT(isInList(),
"We should not register with the refresh driver if we are not"
" in the document's list of timelines");
ObserveRefreshDriver(refreshDriver);
}
}
}
TimeStamp DocumentTimeline::ToTimeStamp(

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

@ -56,6 +56,8 @@ class DocumentTimeline final : public AnimationTimeline,
void NotifyAnimationUpdated(Animation& aAnimation) override;
void RemoveAnimation(Animation* aAnimation) override;
void NotifyAnimationContentVisibilityChanged(Animation* aAnimation,
bool visible) override;
// nsARefreshObserver methods
void WillRefresh(TimeStamp aTime) override;

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

@ -17,6 +17,8 @@
#include "mozilla/ComputedStyle.h"
#include "mozilla/DebugOnly.h"
#include "mozilla/DisplayPortUtils.h"
#include "mozilla/dom/CSSAnimation.h"
#include "mozilla/dom/CSSTransition.h"
#include "mozilla/dom/DocumentInlines.h"
#include "mozilla/dom/AncestorIterator.h"
#include "mozilla/dom/ElementInlines.h"
@ -808,6 +810,12 @@ void nsIFrame::Init(nsIContent* aContent, nsContainerFrame* aParent,
if (!IsPlaceholderFrame() && !aPrevInFlow) {
UpdateVisibleDescendantsState();
}
// 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
// skipped or unskipped.
UpdateAnimationVisibility();
}
void nsIFrame::DestroyFrom(nsIFrame* aDestructRoot,
@ -11642,6 +11650,31 @@ void nsIFrame::UpdateVisibleDescendantsState() {
}
}
void nsIFrame::UpdateAnimationVisibility() {
auto* animationCollection =
AnimationCollection<CSSAnimation>::GetAnimationCollection(this);
auto* transitionCollection =
AnimationCollection<CSSTransition>::GetAnimationCollection(this);
if ((!animationCollection || animationCollection->mAnimations.IsEmpty()) &&
(!transitionCollection || transitionCollection->mAnimations.IsEmpty())) {
return;
}
bool hidden = AncestorHidesContent();
if (animationCollection) {
for (auto& animation : animationCollection->mAnimations) {
animation->SetHiddenByContentVisibility(hidden);
}
}
if (transitionCollection) {
for (auto& transition : transitionCollection->mAnimations) {
transition->SetHiddenByContentVisibility(hidden);
}
}
}
nsIFrame::PhysicalAxes nsIFrame::ShouldApplyOverflowClipping(
const nsStyleDisplay* aDisp) const {
MOZ_ASSERT(aDisp == StyleDisplay(), "Wrong display struct");

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

@ -4705,6 +4705,8 @@ class nsIFrame : public nsQueryFrame {
// Update mAllDescendantsAreInvisible flag for this frame and ancestors.
void UpdateVisibleDescendantsState();
void UpdateAnimationVisibility();
/**
* If this returns true, the frame it's called on should get the
* NS_FRAME_HAS_DIRTY_CHILDREN bit set on it by the caller; either directly

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

@ -1,12 +1,3 @@
[animation-display-lock.html]
[Animation events do not fire for a CSS animation running in a display locked subtree]
expected: FAIL
[Events and promises are handled normally for animations without an owning element]
expected: FAIL
[The finished promise does not resolve due to the normal passage of time for a CSS animation in a display locked subtree]
expected: FAIL
[The finished promise does not resolve due to the normal passage of time for a CSS transition in a display locked subtree]
expected: FAIL