Bug 1771282 - Introduce ScrollTimelineAnimationTracker. r=hiro

It's possible to change the timeline if the animation is in pending. So
we still need an animation tracker to track the scroll-linked
animations. Besides, per the spec, we should keep this animation in
pending if its timeline is inactive. So in this patch, we always put the
scroll-linked animations into ScrollTimelineAnimationTracker, and if we
change the timeline but the animation is still in pending, we move the
animation into the correct animation tracker if needed.

Using two different animation trackers because we would like to trigger
scroll-linked animations after frame construction and reflow,
and don't want to ensure the paint is scheduled.

Note:
1. All tests in scroll-timeline-dynamic.tentative.html are failed. We
   will fix them in Bug 1774275.
2. Drop `animation-duration: infinite` from
   progress-based-animation-animation-longhand-properties.tentative.html,
   because infinite is not defined in animation-duration in [css-animations-1].

Differential Revision: https://phabricator.services.mozilla.com/D159650
This commit is contained in:
Boris Chiou 2022-10-31 23:25:17 +00:00
Родитель 512ea500ee
Коммит 722dd73447
14 изменённых файлов: 303 добавлений и 36 удалений

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

@ -29,6 +29,7 @@
#include "nsThreadUtils.h" // For nsRunnableMethod and nsRevocableEventPtr
#include "nsTransitionManager.h" // For CSSTransition
#include "PendingAnimationTracker.h" // For PendingAnimationTracker
#include "ScrollTimelineAnimationTracker.h"
namespace mozilla::dom {
@ -267,6 +268,9 @@ void Animation::SetTimelineNoUpdate(AnimationTimeline* aTimeline) {
if (!aTimeline) {
MaybeQueueCancelEvent(activeTime);
}
UpdatePendingAnimationTracker(oldTimeline, aTimeline);
UpdateTiming(SeekFlag::NoSeek, SyncNotifyFlag::Async);
}
@ -904,12 +908,9 @@ void Animation::TriggerNow() {
}
// If we don't have an active timeline we can't trigger the animation.
// For non monotonically increasing timelines, we call this function in Play()
// and Pause() immediately,
//
// For monotonically increasing timelines, this is a test-only method that we
// don't expect to be used in conjunction with animations without an active
// timeline so generate a warning if we do find ourselves in that situation.
// However, this is a test-only method that we don't expect to be used in
// conjunction with animations without an active timeline so generate
// a warning if we do find ourselves in that situation.
if (!mTimeline || mTimeline->GetCurrentTimeAsDuration().IsNull()) {
NS_WARNING("Failed to trigger an animation with an active timeline");
return;
@ -918,6 +919,31 @@ void Animation::TriggerNow() {
FinishPendingAt(mTimeline->GetCurrentTimeAsDuration().Value());
}
bool Animation::TryTriggerNowForFiniteTimeline() {
// Normally we expect the play state to be pending but when an animation
// is cancelled and its rendered document can't be reached, we can end up
// with the animation still in a pending player tracker even after it is
// no longer pending.
if (!Pending()) {
return true;
}
MOZ_ASSERT(mTimeline && !mTimeline->IsMonotonicallyIncreasing());
// It's possible that the primary frame or the scrollable frame is not ready
// when setting up this animation. So we don't finish pending right now. In
// this case, the timeline is inactive so it is still pending. The caller
// should handle this case by trying this later once the scrollable frame is
// ready.
const auto currentTime = mTimeline->GetCurrentTimeAsDuration();
if (currentTime.IsNull()) {
return false;
}
FinishPendingAt(currentTime.Value());
return true;
}
Nullable<TimeDuration> Animation::GetCurrentOrPendingStartTime() const {
Nullable<TimeDuration> result;
@ -1442,10 +1468,14 @@ void Animation::PlayNoUpdate(ErrorResult& aRv, LimitBehavior aLimitBehavior) {
// animations if it applies.
mSyncWithGeometricAnimations = false;
// If the animation use finite timeline, e.g. scroll timeline, we don't use
// pending animation tracker. Instead, we let it play immediately.
if (HasFiniteTimeline()) {
TriggerNow();
// Always schedule a task even if we would like to let this animation
// immedidately ready, per spec.
// https://drafts.csswg.org/web-animations/#playing-an-animation-section
if (Document* doc = GetRenderedDocument()) {
doc->GetOrCreateScrollTimelineAnimationTracker()->AddPending(*this);
} // else: we fail to track this animation, so let the scroll frame to
// trigger it when ticking.
} else {
if (Document* doc = GetRenderedDocument()) {
PendingAnimationTracker* tracker =
@ -1504,10 +1534,14 @@ void Animation::Pause(ErrorResult& aRv) {
mPendingState = PendingState::PausePending;
// If the animation use finite timeline, e.g. scroll timeline, we don't use
// pending animation tracker. Instead, we let it pause immediately.
if (HasFiniteTimeline()) {
TriggerNow();
// Always schedule a task even if we would like to let this animation
// immedidately ready, per spec.
// https://drafts.csswg.org/web-animations/#playing-an-animation-section
if (Document* doc = GetRenderedDocument()) {
doc->GetOrCreateScrollTimelineAnimationTracker()->AddPending(*this);
} // else: we fail to track this animation, so let the scroll frame to
// trigger it when ticking.
} else {
if (Document* doc = GetRenderedDocument()) {
PendingAnimationTracker* tracker =
@ -1804,10 +1838,13 @@ bool Animation::IsPossiblyOrphanedPendingAnimation() const {
// * We started playing but our timeline became inactive.
// In this case the pending animation tracker will drop us from its hashmap
// when we have been painted.
// * When we started playing we couldn't find a PendingAnimationTracker to
// register with (perhaps the effect had no document) so we simply
// set mPendingState in PlayNoUpdate and relied on this method to catch us
// on the next tick.
// * When we started playing we couldn't find a
// PendingAnimationTracker/ScrollTimelineAnimationTracker to register with
// (perhaps the effect had no document) so we may
// 1. simply set mPendingState in PlayNoUpdate and relied on this method to
// catch us on the next tick, or
// 2. rely on the scroll frame to tick this animation and catch us in this
// method.
// If we're not pending we're ok.
if (mPendingState == PendingState::NotPending) {
@ -1862,6 +1899,50 @@ Document* Animation::GetTimelineDocument() const {
return mTimeline ? mTimeline->GetDocument() : nullptr;
}
void Animation::UpdatePendingAnimationTracker(AnimationTimeline* aOldTimeline,
AnimationTimeline* aNewTimeline) {
// If we are still in pending, we may have to move this animation into the
// correct animation tracker.
Document* doc = GetRenderedDocument();
if (!doc || !Pending()) {
return;
}
const bool fromFiniteTimeline =
aOldTimeline && !aOldTimeline->IsMonotonicallyIncreasing();
const bool toFiniteTimeline =
aNewTimeline && !aNewTimeline->IsMonotonicallyIncreasing();
if (fromFiniteTimeline == toFiniteTimeline) {
return;
}
const bool isPlayPending = mPendingState == PendingState::PlayPending;
if (toFiniteTimeline) {
// From null/document-timeline to scroll-timeline
if (auto* tracker = doc->GetPendingAnimationTracker()) {
if (isPlayPending) {
tracker->RemovePlayPending(*this);
} else {
tracker->RemovePausePending(*this);
}
}
doc->GetOrCreateScrollTimelineAnimationTracker()->AddPending(*this);
} else {
// From scroll-timeline to null/document-timeline
if (auto* tracker = doc->GetScrollTimelineAnimationTracker()) {
tracker->RemovePending(*this);
}
auto* tracker = doc->GetOrCreatePendingAnimationTracker();
if (isPlayPending) {
tracker->AddPlayPending(*this);
} else {
tracker->AddPausePending(*this);
}
}
}
class AsyncFinishNotification : public MicroTaskRunnable {
public:
explicit AsyncFinishNotification(Animation* aAnimation)

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

@ -226,11 +226,6 @@ class Animation : public DOMEventTargetHelper,
*/
void TriggerOnNextTick(const Nullable<TimeDuration>& aReadyTime);
/**
* For the non-monotonically increasing timeline (e.g. ScrollTimeline), this
* is used when playing or pausing because we don't put the animations into
* PendingAnimationTracker, and we would like to use the current scroll
* position as the ready time.
*
* For the monotonically increasing timeline, we use this only for testing:
* Start or pause a pending animation using the current timeline time. This
* is used to support existing tests that expect animations to begin
@ -242,6 +237,13 @@ class Animation : public DOMEventTargetHelper,
* added to.
*/
void TriggerNow();
/**
* For the non-monotonically increasing timeline (e.g. ScrollTimeline), we try
* to trigger it in ScrollTimelineAnimationTracker by this method. This uses
* the current scroll position as the ready time. Return true if we don't need
* to trigger it or we trigger it successfully.
*/
bool TryTriggerNowForFiniteTimeline();
/**
* When TriggerOnNextTick is called, we store the ready time but we don't
* apply it until the next tick. In the meantime, GetStartTime() will return
@ -595,6 +597,9 @@ class Animation : public DOMEventTargetHelper,
return mTimeline && !mTimeline->IsMonotonicallyIncreasing();
}
void UpdatePendingAnimationTracker(AnimationTimeline* aOldTimeline,
AnimationTimeline* aNewTimeline);
RefPtr<AnimationTimeline> mTimeline;
RefPtr<AnimationEffect> mEffect;
// The beginning of the delay period.

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

@ -64,8 +64,8 @@ void PendingAnimationTracker::TriggerPendingAnimationsOnNextTick(
}
MOZ_ASSERT(timeline->IsMonotonicallyIncreasing(),
"Don't put non-MonotoniciallyIncreasing timeline into "
"PendingAnimationTracker");
"The non-monotonicially-increasing timeline should be in "
"ScrollTimelineAnimationTracker");
// When the timeline's refresh driver is under test control, its values
// have no correspondance to wallclock times so we shouldn't try to

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

@ -4,8 +4,8 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
#ifndef mozilla_dom_PendingAnimationTracker_h
#define mozilla_dom_PendingAnimationTracker_h
#ifndef mozilla_PendingAnimationTracker_h
#define mozilla_PendingAnimationTracker_h
#include "mozilla/dom/Animation.h"
#include "mozilla/TypedEnumBits.h"
@ -20,6 +20,10 @@ namespace dom {
class Document;
}
/**
* Handle the pending animations which use document-timeline or null-timeline
* while playing or pausing.
*/
class PendingAnimationTracker final {
public:
explicit PendingAnimationTracker(dom::Document* aDocument);
@ -106,4 +110,4 @@ MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(PendingAnimationTracker::CheckState)
} // namespace mozilla
#endif // mozilla_dom_PendingAnimationTracker_h
#endif // mozilla_PendingAnimationTracker_h

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

@ -0,0 +1,51 @@
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "ScrollTimelineAnimationTracker.h"
#include "mozilla/dom/Document.h"
namespace mozilla {
NS_IMPL_CYCLE_COLLECTION(ScrollTimelineAnimationTracker, mPendingSet, mDocument)
NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(ScrollTimelineAnimationTracker, AddRef)
NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(ScrollTimelineAnimationTracker, Release)
void ScrollTimelineAnimationTracker::TriggerPendingAnimations() {
for (auto iter = mPendingSet.begin(), end = mPendingSet.end(); iter != end;
++iter) {
dom::Animation* animation = *iter;
MOZ_ASSERT(animation->GetTimeline() &&
!animation->GetTimeline()->IsMonotonicallyIncreasing());
// FIXME: Trigger now may not be correct because the spec says:
// If a user agent determines that animation is immediately ready, it may
// schedule the task (i.e. ResumeAt()) as a microtask such that it runs at
// the next microtask checkpoint, but it must not perform the task
// synchronously.
// Note: So, for now, we put the animation into the tracker, and trigger
// them immediately until the frames are ready. Using TriggerOnNextTick()
// for scroll-linked animations may have issues because we don't tick if
// no one does scroll.
if (!animation->TryTriggerNowForFiniteTimeline()) {
// Note: We keep this animation pending even if its timeline is always
// inactive. It's pretty hard to tell its future status, for example, it's
// possible that the scroll container is in display:none subtree but the
// animating element isn't the subtree, then we need to keep tracking the
// situation until the scroll container gets framed. so in general we make
// this animation be pending (i.e. not ready) if its scroll-timeline is
// inactive, and this also matches the current spec definition.
continue;
}
// Note: Remove() is legitimately called once per entry during the loop.
mPendingSet.Remove(iter);
}
}
} // namespace mozilla

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

@ -0,0 +1,58 @@
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
#ifndef mozilla_ScrollTimelineAnimationTracker_h
#define mozilla_ScrollTimelineAnimationTracker_h
#include "mozilla/dom/Animation.h"
#include "nsCycleCollectionParticipant.h"
#include "nsTHashSet.h"
namespace mozilla {
namespace dom {
class Document;
}
/**
* Handle the pending animations which use scroll timeline while playing or
* pausing.
*/
class ScrollTimelineAnimationTracker final {
public:
explicit ScrollTimelineAnimationTracker(dom::Document* aDocument)
: mDocument(aDocument) {}
NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(
ScrollTimelineAnimationTracker)
NS_DECL_CYCLE_COLLECTION_NATIVE_CLASS(ScrollTimelineAnimationTracker)
void AddPending(dom::Animation& aAnimation) {
mPendingSet.Insert(&aAnimation);
}
void RemovePending(dom::Animation& aAnimation) {
mPendingSet.Remove(&aAnimation);
}
bool HasPendingAnimations() const { return mPendingSet.Count() > 0; }
bool IsWaiting(const dom::Animation& aAnimation) const {
return mPendingSet.Contains(const_cast<dom::Animation*>(&aAnimation));
}
void TriggerPendingAnimations();
private:
~ScrollTimelineAnimationTracker() = default;
nsTHashSet<nsRefPtrHashKey<dom::Animation>> mPendingSet;
RefPtr<dom::Document> mDocument;
};
} // namespace mozilla
#endif // mozilla_ScrollTimelineAnimationTracker_h

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

@ -38,6 +38,7 @@ EXPORTS.mozilla += [
"PendingAnimationTracker.h",
"PostRestyleMode.h",
"PseudoElementHashEntry.h",
"ScrollTimelineAnimationTracker.h",
"TimingParams.h",
]
@ -58,6 +59,7 @@ UNIFIED_SOURCES += [
"KeyframeUtils.cpp",
"PendingAnimationTracker.cpp",
"ScrollTimeline.cpp",
"ScrollTimelineAnimationTracker.cpp",
"TimingParams.cpp",
]

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

@ -100,6 +100,7 @@
#include "mozilla/RelativeTo.h"
#include "mozilla/RestyleManager.h"
#include "mozilla/ReverseIterator.h"
#include "mozilla/ScrollTimelineAnimationTracker.h"
#include "mozilla/SMILAnimationController.h"
#include "mozilla/SMILTimeContainer.h"
#include "mozilla/ScopeExit.h"
@ -2500,6 +2501,7 @@ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INTERNAL(Document)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mCachedEncoder)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocumentTimeline)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPendingAnimationTracker)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mScrollTimelineAnimationTracker)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTemplateContentsOwner)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mChildrenCollection)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mImages);
@ -2625,6 +2627,7 @@ NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(Document)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mCachedEncoder)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocumentTimeline)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mPendingAnimationTracker)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mScrollTimelineAnimationTracker)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mTemplateContentsOwner)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mChildrenCollection)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mImages);
@ -9274,6 +9277,15 @@ PendingAnimationTracker* Document::GetOrCreatePendingAnimationTracker() {
return mPendingAnimationTracker;
}
ScrollTimelineAnimationTracker*
Document::GetOrCreateScrollTimelineAnimationTracker() {
if (!mScrollTimelineAnimationTracker) {
mScrollTimelineAnimationTracker = new ScrollTimelineAnimationTracker(this);
}
return mScrollTimelineAnimationTracker;
}
/**
* Retrieve the "direction" property of the document.
*

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

@ -204,6 +204,7 @@ struct LangGroupFontPrefs;
class PendingAnimationTracker;
class PermissionDelegateHandler;
class PresShell;
class ScrollTimelineAnimationTracker;
class ServoStyleSet;
enum class StyleOrigin : uint8_t;
class SMILAnimationController;
@ -2765,6 +2766,19 @@ class Document : public nsINode,
// will never be nullptr.
PendingAnimationTracker* GetOrCreatePendingAnimationTracker();
// Gets the tracker for scroll-linked animations that are waiting to start.
// Returns nullptr if there is no scroll-linked animation tracker for this
// document which will be the case if there have never been any scroll-linked
// animations in the document.
ScrollTimelineAnimationTracker* GetScrollTimelineAnimationTracker() {
return mScrollTimelineAnimationTracker;
}
// Gets the tracker for scroll-linked animations that are waiting to start and
// creates it if it doesn't already exist. As a result, the return value
// will never be nullptr.
ScrollTimelineAnimationTracker* GetOrCreateScrollTimelineAnimationTracker();
/**
* Prevents user initiated events from being dispatched to the document and
* subdocuments.
@ -5140,6 +5154,10 @@ class Document : public nsINode,
// nullptr until GetOrCreatePendingAnimationTracker is called.
RefPtr<PendingAnimationTracker> mPendingAnimationTracker;
// Tracker for scroll-linked animations that are waiting to start.
// nullptr until GetOrCreateScrollTimelineAnimationTracker is called.
RefPtr<ScrollTimelineAnimationTracker> mScrollTimelineAnimationTracker;
// A document "without a browsing context" that owns the content of
// HTMLTemplateElement.
RefPtr<Document> mTemplateContentsOwner;

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

@ -193,6 +193,7 @@
#include "mozilla/layers/APZPublicUtils.h"
#include "mozilla/ProfilerLabels.h"
#include "mozilla/ProfilerMarkers.h"
#include "mozilla/ScrollTimelineAnimationTracker.h"
#include "mozilla/ScrollTypes.h"
#include "mozilla/ServoBindings.h"
#include "mozilla/ServoStyleSet.h"
@ -4241,6 +4242,14 @@ static inline void AssertFrameTreeIsSane(const PresShell& aPresShell) {
#endif
}
static void TriggerPendingScrollTimelineAnimations(Document* aDocument) {
auto* tracker = aDocument->GetScrollTimelineAnimationTracker();
if (!tracker || !tracker->HasPendingAnimations()) {
return;
}
tracker->TriggerPendingAnimations();
}
void PresShell::DoFlushPendingNotifications(mozilla::ChangesToFlush aFlush) {
// FIXME(emilio, bug 1530177): Turn into a release assert when bug 1530188 and
// bug 1530190 are fixed.
@ -4414,6 +4423,17 @@ void PresShell::DoFlushPendingNotifications(mozilla::ChangesToFlush aFlush) {
FlushPendingScrollResnap();
if (MOZ_LIKELY(!mIsDestroying)) {
// Try to trigger pending scroll-linked animations after we flush
// style and layout (if any). If we try to trigger them after flushing
// style but the frame tree is not ready, we will check them again after
// we flush layout because the requirement to trigger scroll-linked
// animations is that the associated scroll containers are ready (i.e. the
// scroll-timeline is active), and this depends on the readiness of the
// scrollable frame and the primary frame of the scroll container.
TriggerPendingScrollTimelineAnimations(mDocument);
}
if (flushType >= FlushType::Layout) {
if (!mIsDestroying) {
viewManager->UpdateWidgetGeometry();

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

@ -1,12 +1,16 @@
[scroll-timeline-dynamic.tentative.html]
expected:
if (os == "android") and fission: [OK, TIMEOUT, CRASH]
[Switching between document and scroll timelines [immediate\]]
expected: FAIL
[Switching between document and scroll timelines [scroll\]]
expected: FAIL
[Switching pending animation from document to scroll timelines [immediate\]]
expected: FAIL
[Switching pending animation from document to scroll timelines [scroll\]]
expected: FAIL
[Changing computed value of animation-timeline changes effective timeline [immediate\]]
expected: FAIL

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

@ -83,14 +83,6 @@ promise_test(async t => {
assert_equals(getComputedStyle(target).translate, '100px');
}, 'animation-duration: 0s');
promise_test(async t => {
let [target, scroller] = createTargetAndScroller(t);
target.style.animation = 'infinite linear anim scroll(nearest)';
await scrollTop(scroller, 25); // [0, 100].
assert_equals(getComputedStyle(target).translate, '100px');
}, 'animation-duration: infinite');
// ------------------------------
// Test animation-iteration-count

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

@ -1,4 +1,5 @@
<!DOCTYPE html>
<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1">
<link rel="help" src="https://drafts.csswg.org/scroll-animations-1/#scroll-timeline-axis">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>

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

@ -106,6 +106,25 @@
await assert_width(element, '120px');
}, 'Switching between document and scroll timelines');
dynamic_rule_test(async (t, assert_width) => {
let element = insertElement();
// Flush style and create the animation with play pending.
getComputedStyle(element).animation;
let anim = element.getAnimations()[0];
assert_true(anim.pending, "The animation is in play pending");
// Switch to scroll timeline for a pending animation.
scroller1.style.scrollTimelineName = 'timeline';
element.style.animationTimeline = 'timeline';
await anim.ready;
assert_false(anim.pending, "The animation is not pending");
await assert_width(element, '120px');
}, 'Switching pending animation from document to scroll timelines');
dynamic_rule_test(async (t, assert_width) => {
let element = insertElement();