Bug 1879449 [wpt PR 44489] - [wptrunner] Add `infrastructure/expected-fail/` test...
[gecko.git] / layout / style / nsAnimationManager.cpp
blob978755cae2b59ef57d8a59bac582d4c3d4ec03b5
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "nsAnimationManager.h"
8 #include "nsINode.h"
9 #include "nsTransitionManager.h"
11 #include "mozilla/AnimationEventDispatcher.h"
12 #include "mozilla/AnimationUtils.h"
13 #include "mozilla/EffectCompositor.h"
14 #include "mozilla/ElementAnimationData.h"
15 #include "mozilla/MemoryReporting.h"
16 #include "mozilla/ServoStyleSet.h"
17 #include "mozilla/TimelineCollection.h"
18 #include "mozilla/dom/AnimationEffect.h"
19 #include "mozilla/dom/DocumentTimeline.h"
20 #include "mozilla/dom/KeyframeEffect.h"
21 #include "mozilla/dom/MutationObservers.h"
22 #include "mozilla/dom/ScrollTimeline.h"
23 #include "mozilla/dom/ViewTimeline.h"
25 #include "nsPresContext.h"
26 #include "nsPresContextInlines.h"
27 #include "nsStyleChangeList.h"
28 #include "nsLayoutUtils.h"
29 #include "nsIFrame.h"
30 #include "mozilla/dom/Document.h"
31 #include "nsDOMMutationObserver.h"
32 #include "nsRFPService.h"
33 #include <algorithm> // std::stable_sort
34 #include <math.h>
36 using namespace mozilla;
37 using namespace mozilla::css;
38 using mozilla::dom::Animation;
39 using mozilla::dom::AnimationPlayState;
40 using mozilla::dom::CSSAnimation;
41 using mozilla::dom::Element;
42 using mozilla::dom::KeyframeEffect;
43 using mozilla::dom::MutationObservers;
44 using mozilla::dom::ScrollTimeline;
45 using mozilla::dom::ViewTimeline;
47 ////////////////////////// nsAnimationManager ////////////////////////////
49 // Find the matching animation by |aName| in the old list
50 // of animations and remove the matched animation from the list.
51 static already_AddRefed<CSSAnimation> PopExistingAnimation(
52 const nsAtom* aName,
53 nsAnimationManager::CSSAnimationCollection* aCollection) {
54 if (!aCollection) {
55 return nullptr;
58 // Animations are stored in reverse order to how they appear in the
59 // animation-name property. However, we want to match animations beginning
60 // from the end of the animation-name list, so we iterate *forwards*
61 // through the collection.
62 for (size_t idx = 0, length = aCollection->mAnimations.Length();
63 idx != length; ++idx) {
64 CSSAnimation* cssAnim = aCollection->mAnimations[idx];
65 if (cssAnim->AnimationName() == aName) {
66 RefPtr<CSSAnimation> match = cssAnim;
67 aCollection->mAnimations.RemoveElementAt(idx);
68 return match.forget();
72 return nullptr;
75 class MOZ_STACK_CLASS ServoCSSAnimationBuilder final {
76 public:
77 explicit ServoCSSAnimationBuilder(const ComputedStyle* aComputedStyle)
78 : mComputedStyle(aComputedStyle) {
79 MOZ_ASSERT(aComputedStyle);
82 bool BuildKeyframes(const Element& aElement, nsPresContext* aPresContext,
83 nsAtom* aName,
84 const StyleComputedTimingFunction& aTimingFunction,
85 nsTArray<Keyframe>& aKeyframes) {
86 return aPresContext->StyleSet()->GetKeyframesForName(
87 aElement, *mComputedStyle, aName, aTimingFunction, aKeyframes);
89 void SetKeyframes(KeyframeEffect& aEffect, nsTArray<Keyframe>&& aKeyframes,
90 const dom::AnimationTimeline* aTimeline) {
91 aEffect.SetKeyframes(std::move(aKeyframes), mComputedStyle, aTimeline);
94 // Currently all the animation building code in this file is based on
95 // assumption that creating and removing animations should *not* trigger
96 // additional restyles since those changes will be handled within the same
97 // restyle.
99 // While that is true for the Gecko style backend, it is not true for the
100 // Servo style backend where we want restyles to be triggered so that we
101 // perform a second animation restyle where we will incorporate the changes
102 // arising from creating and removing animations.
104 // Fortunately, our attempts to avoid posting extra restyles as part of the
105 // processing here are imperfect and most of the time we happen to post
106 // them anyway. Occasionally, however, we don't. For example, we don't post
107 // a restyle when we create a new animation whose an animation index matches
108 // the default value it was given already (which is typically only true when
109 // the CSSAnimation we create is the first Animation created in a particular
110 // content process).
112 // As a result, when we are using the Servo backend, whenever we have an added
113 // or removed animation we need to explicitly trigger a restyle.
115 // This code should eventually disappear along with the Gecko style backend
116 // and we should simply call Play() / Pause() / Cancel() etc. which will
117 // post the required restyles.
118 void NotifyNewOrRemovedAnimation(const Animation& aAnimation) {
119 dom::AnimationEffect* effect = aAnimation.GetEffect();
120 if (!effect) {
121 return;
124 KeyframeEffect* keyframeEffect = effect->AsKeyframeEffect();
125 if (!keyframeEffect) {
126 return;
129 keyframeEffect->RequestRestyle(EffectCompositor::RestyleType::Standard);
132 private:
133 const ComputedStyle* mComputedStyle;
136 static void UpdateOldAnimationPropertiesWithNew(
137 CSSAnimation& aOld, TimingParams&& aNewTiming,
138 nsTArray<Keyframe>&& aNewKeyframes, bool aNewIsStylePaused,
139 CSSAnimationProperties aOverriddenProperties,
140 ServoCSSAnimationBuilder& aBuilder, dom::AnimationTimeline* aTimeline,
141 dom::CompositeOperation aNewComposite) {
142 bool animationChanged = false;
144 // Update the old from the new so we can keep the original object
145 // identity (and any expando properties attached to it).
146 if (aOld.GetEffect()) {
147 dom::AnimationEffect* oldEffect = aOld.GetEffect();
149 // Copy across the changes that are not overridden
150 TimingParams updatedTiming = oldEffect->SpecifiedTiming();
151 if (~aOverriddenProperties & CSSAnimationProperties::Duration) {
152 updatedTiming.SetDuration(aNewTiming.Duration());
154 if (~aOverriddenProperties & CSSAnimationProperties::IterationCount) {
155 updatedTiming.SetIterations(aNewTiming.Iterations());
157 if (~aOverriddenProperties & CSSAnimationProperties::Direction) {
158 updatedTiming.SetDirection(aNewTiming.Direction());
160 if (~aOverriddenProperties & CSSAnimationProperties::Delay) {
161 updatedTiming.SetDelay(aNewTiming.Delay());
163 if (~aOverriddenProperties & CSSAnimationProperties::FillMode) {
164 updatedTiming.SetFill(aNewTiming.Fill());
167 animationChanged = oldEffect->SpecifiedTiming() != updatedTiming;
168 oldEffect->SetSpecifiedTiming(std::move(updatedTiming));
170 if (KeyframeEffect* oldKeyframeEffect = oldEffect->AsKeyframeEffect()) {
171 if (~aOverriddenProperties & CSSAnimationProperties::Keyframes) {
172 aBuilder.SetKeyframes(*oldKeyframeEffect, std::move(aNewKeyframes),
173 aTimeline);
176 if (~aOverriddenProperties & CSSAnimationProperties::Composition) {
177 animationChanged = oldKeyframeEffect->Composite() != aNewComposite;
178 oldKeyframeEffect->SetCompositeFromStyle(aNewComposite);
183 // Checking pointers should be enough. If both are scroll-timeline, we reuse
184 // the scroll-timeline object if their scrollers and axes are the same.
185 if (aOld.GetTimeline() != aTimeline) {
186 aOld.SetTimeline(aTimeline);
187 animationChanged = true;
190 // Handle changes in play state. If the animation is idle, however,
191 // changes to animation-play-state should *not* restart it.
192 if (aOld.PlayState() != AnimationPlayState::Idle &&
193 ~aOverriddenProperties & CSSAnimationProperties::PlayState) {
194 bool wasPaused = aOld.PlayState() == AnimationPlayState::Paused;
195 if (!wasPaused && aNewIsStylePaused) {
196 aOld.PauseFromStyle();
197 animationChanged = true;
198 } else if (wasPaused && !aNewIsStylePaused) {
199 aOld.PlayFromStyle();
200 animationChanged = true;
204 // Updating the effect timing above might already have caused the
205 // animation to become irrelevant so only add a changed record if
206 // the animation is still relevant.
207 if (animationChanged && aOld.IsRelevant()) {
208 MutationObservers::NotifyAnimationChanged(&aOld);
212 static already_AddRefed<dom::AnimationTimeline> GetNamedProgressTimeline(
213 dom::Document* aDocument, const NonOwningAnimationTarget& aTarget,
214 nsAtom* aName) {
215 // A named progress timeline is referenceable in animation-timeline by:
216 // 1. the declaring element itself
217 // 2. that element’s descendants
218 // 3. that element’s following siblings and their descendants
219 // https://drafts.csswg.org/scroll-animations-1/#timeline-scope
220 // FIXME: Bug 1823500. Reduce default scoping to ancestors only.
221 for (Element* curr = AnimationUtils::GetElementForRestyle(
222 aTarget.mElement, aTarget.mPseudoType);
223 curr; curr = curr->GetParentElement()) {
224 // If multiple elements have declared the same timeline name, the matching
225 // timeline is the one declared on the nearest element in tree order, which
226 // considers siblings closer than parents.
227 // Note: This is fine for parallel traversal because we update animations by
228 // SequentialTask.
229 for (Element* e = curr; e; e = e->GetPreviousElementSibling()) {
230 // In case of a name conflict on the same element, scroll progress
231 // timelines take precedence over view progress timelines.
232 const auto [element, pseudoType] =
233 AnimationUtils::GetElementPseudoPair(e);
234 if (auto* collection =
235 TimelineCollection<ScrollTimeline>::Get(element, pseudoType)) {
236 if (RefPtr<ScrollTimeline> timeline = collection->Lookup(aName)) {
237 return timeline.forget();
241 if (auto* collection =
242 TimelineCollection<ViewTimeline>::Get(element, pseudoType)) {
243 if (RefPtr<ViewTimeline> timeline = collection->Lookup(aName)) {
244 return timeline.forget();
250 // If we cannot find a matched scroll-timeline-name, this animation is not
251 // associated with a timeline.
252 // https://drafts.csswg.org/css-animations-2/#valdef-animation-timeline-custom-ident
253 return nullptr;
256 static already_AddRefed<dom::AnimationTimeline> GetTimeline(
257 const StyleAnimationTimeline& aStyleTimeline, nsPresContext* aPresContext,
258 const NonOwningAnimationTarget& aTarget) {
259 switch (aStyleTimeline.tag) {
260 case StyleAnimationTimeline::Tag::Timeline: {
261 // Check scroll-timeline-name property or view-timeline-property.
262 nsAtom* name = aStyleTimeline.AsTimeline().AsAtom();
263 return name != nsGkAtoms::_empty
264 ? GetNamedProgressTimeline(aPresContext->Document(), aTarget,
265 name)
266 : nullptr;
268 case StyleAnimationTimeline::Tag::Scroll: {
269 const auto& scroll = aStyleTimeline.AsScroll();
270 return ScrollTimeline::MakeAnonymous(aPresContext->Document(), aTarget,
271 scroll.axis, scroll.scroller);
273 case StyleAnimationTimeline::Tag::View: {
274 const auto& view = aStyleTimeline.AsView();
275 return ViewTimeline::MakeAnonymous(aPresContext->Document(), aTarget,
276 view.axis, view.inset);
278 case StyleAnimationTimeline::Tag::Auto:
279 return do_AddRef(aTarget.mElement->OwnerDoc()->Timeline());
281 MOZ_ASSERT_UNREACHABLE("Unknown animation-timeline value?");
282 return nullptr;
285 // Returns a new animation set up with given StyleAnimation.
286 // Or returns an existing animation matching StyleAnimation's name updated
287 // with the new StyleAnimation.
288 static already_AddRefed<CSSAnimation> BuildAnimation(
289 nsPresContext* aPresContext, const NonOwningAnimationTarget& aTarget,
290 const nsStyleUIReset& aStyle, uint32_t animIdx,
291 ServoCSSAnimationBuilder& aBuilder,
292 nsAnimationManager::CSSAnimationCollection* aCollection) {
293 MOZ_ASSERT(aPresContext);
295 nsAtom* animationName = aStyle.GetAnimationName(animIdx);
296 nsTArray<Keyframe> keyframes;
297 if (!aBuilder.BuildKeyframes(*aTarget.mElement, aPresContext, animationName,
298 aStyle.GetAnimationTimingFunction(animIdx),
299 keyframes)) {
300 return nullptr;
303 TimingParams timing = TimingParamsFromCSSParams(
304 aStyle.GetAnimationDuration(animIdx).ToMilliseconds(),
305 aStyle.GetAnimationDelay(animIdx).ToMilliseconds(),
306 aStyle.GetAnimationIterationCount(animIdx),
307 aStyle.GetAnimationDirection(animIdx),
308 aStyle.GetAnimationFillMode(animIdx));
310 bool isStylePaused =
311 aStyle.GetAnimationPlayState(animIdx) == StyleAnimationPlayState::Paused;
313 RefPtr<dom::AnimationTimeline> timeline =
314 GetTimeline(aStyle.GetTimeline(animIdx), aPresContext, aTarget);
316 // Find the matching animation with animation name in the old list
317 // of animations and remove the matched animation from the list.
318 RefPtr<CSSAnimation> oldAnim =
319 PopExistingAnimation(animationName, aCollection);
321 const auto composition = StyleToDom(aStyle.GetAnimationComposition(animIdx));
322 if (oldAnim) {
323 // Copy over the start times and (if still paused) pause starts
324 // for each animation (matching on name only) that was also in the
325 // old list of animations.
326 // This means that we honor dynamic changes, which isn't what the
327 // spec says to do, but WebKit seems to honor at least some of
328 // them. See
329 // http://lists.w3.org/Archives/Public/www-style/2011Apr/0079.html
330 // In order to honor what the spec said, we'd copy more data over.
331 UpdateOldAnimationPropertiesWithNew(
332 *oldAnim, std::move(timing), std::move(keyframes), isStylePaused,
333 oldAnim->GetOverriddenProperties(), aBuilder, timeline, composition);
334 return oldAnim.forget();
337 KeyframeEffectParams effectOptions(composition);
338 RefPtr<KeyframeEffect> effect = new dom::CSSAnimationKeyframeEffect(
339 aPresContext->Document(),
340 OwningAnimationTarget(aTarget.mElement, aTarget.mPseudoType),
341 std::move(timing), effectOptions);
343 aBuilder.SetKeyframes(*effect, std::move(keyframes), timeline);
345 RefPtr<CSSAnimation> animation = new CSSAnimation(
346 aPresContext->Document()->GetScopeObject(), animationName);
347 animation->SetOwningElement(
348 OwningElementRef(*aTarget.mElement, aTarget.mPseudoType));
350 animation->SetTimelineNoUpdate(timeline);
351 animation->SetEffectNoUpdate(effect);
353 if (isStylePaused) {
354 animation->PauseFromStyle();
355 } else {
356 animation->PlayFromStyle();
359 aBuilder.NotifyNewOrRemovedAnimation(*animation);
361 return animation.forget();
364 static nsAnimationManager::OwningCSSAnimationPtrArray BuildAnimations(
365 nsPresContext* aPresContext, const NonOwningAnimationTarget& aTarget,
366 const nsStyleUIReset& aStyle, ServoCSSAnimationBuilder& aBuilder,
367 nsAnimationManager::CSSAnimationCollection* aCollection,
368 nsTHashSet<RefPtr<nsAtom>>& aReferencedAnimations) {
369 nsAnimationManager::OwningCSSAnimationPtrArray result;
371 for (size_t animIdx = aStyle.mAnimationNameCount; animIdx-- != 0;) {
372 nsAtom* name = aStyle.GetAnimationName(animIdx);
373 // CSS Animations whose animation-name does not match a @keyframes rule do
374 // not generate animation events. This includes when the animation-name is
375 // "none" which is represented by an empty name in the StyleAnimation.
376 // Since such animations neither affect style nor dispatch events, we do
377 // not generate a corresponding CSSAnimation for them.
378 if (name == nsGkAtoms::_empty) {
379 continue;
382 aReferencedAnimations.Insert(name);
383 RefPtr<CSSAnimation> dest = BuildAnimation(aPresContext, aTarget, aStyle,
384 animIdx, aBuilder, aCollection);
385 if (!dest) {
386 continue;
389 dest->SetAnimationIndex(static_cast<uint64_t>(animIdx));
390 result.AppendElement(dest);
392 return result;
395 void nsAnimationManager::UpdateAnimations(dom::Element* aElement,
396 PseudoStyleType aPseudoType,
397 const ComputedStyle* aComputedStyle) {
398 MOZ_ASSERT(mPresContext->IsDynamic(),
399 "Should not update animations for print or print preview");
400 MOZ_ASSERT(aElement->IsInComposedDoc(),
401 "Should not update animations that are not attached to the "
402 "document tree");
404 if (!aComputedStyle ||
405 aComputedStyle->StyleDisplay()->mDisplay == StyleDisplay::None) {
406 // If we are in a display:none subtree we will have no computed values.
407 // However, if we are on the root of display:none subtree, the computed
408 // values might not have been cleared yet.
409 // In either case, since CSS animations should not run in display:none
410 // subtrees we should stop (actually, destroy) any animations on this
411 // element here.
412 StopAnimationsForElement(aElement, aPseudoType);
413 return;
416 NonOwningAnimationTarget target(aElement, aPseudoType);
417 ServoCSSAnimationBuilder builder(aComputedStyle);
419 DoUpdateAnimations(target, *aComputedStyle->StyleUIReset(), builder);
422 void nsAnimationManager::DoUpdateAnimations(
423 const NonOwningAnimationTarget& aTarget, const nsStyleUIReset& aStyle,
424 ServoCSSAnimationBuilder& aBuilder) {
425 // Everything that causes our animation data to change triggers a
426 // style change, which in turn triggers a non-animation restyle.
427 // Likewise, when we initially construct frames, we're not in a
428 // style change, but also not in an animation restyle.
430 auto* collection =
431 CSSAnimationCollection::Get(aTarget.mElement, aTarget.mPseudoType);
432 if (!collection && aStyle.mAnimationNameCount == 1 &&
433 aStyle.mAnimations[0].GetName() == nsGkAtoms::_empty) {
434 return;
437 nsAutoAnimationMutationBatch mb(aTarget.mElement->OwnerDoc());
439 // Build the updated animations list, extracting matching animations from
440 // the existing collection as we go.
441 OwningCSSAnimationPtrArray newAnimations =
442 BuildAnimations(mPresContext, aTarget, aStyle, aBuilder, collection,
443 mMaybeReferencedAnimations);
445 if (newAnimations.IsEmpty()) {
446 if (collection) {
447 collection->Destroy();
449 return;
452 if (!collection) {
453 collection =
454 &aTarget.mElement->EnsureAnimationData().EnsureAnimationCollection(
455 *aTarget.mElement, aTarget.mPseudoType);
456 if (!collection->isInList()) {
457 AddElementCollection(collection);
460 collection->mAnimations.SwapElements(newAnimations);
462 // Cancel removed animations
463 for (size_t newAnimIdx = newAnimations.Length(); newAnimIdx-- != 0;) {
464 aBuilder.NotifyNewOrRemovedAnimation(*newAnimations[newAnimIdx]);
465 newAnimations[newAnimIdx]->CancelFromStyle(PostRestyleMode::IfNeeded);