summaryrefslogtreecommitdiffstats
path: root/layout/style/nsAnimationManager.cpp
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--layout/style/nsAnimationManager.cpp467
1 files changed, 467 insertions, 0 deletions
diff --git a/layout/style/nsAnimationManager.cpp b/layout/style/nsAnimationManager.cpp
new file mode 100644
index 0000000000..f03e00ff2a
--- /dev/null
+++ b/layout/style/nsAnimationManager.cpp
@@ -0,0 +1,467 @@
+/* -*- 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 "nsAnimationManager.h"
+#include "nsINode.h"
+#include "nsTransitionManager.h"
+
+#include "mozilla/AnimationEventDispatcher.h"
+#include "mozilla/AnimationUtils.h"
+#include "mozilla/EffectCompositor.h"
+#include "mozilla/ElementAnimationData.h"
+#include "mozilla/MemoryReporting.h"
+#include "mozilla/ServoStyleSet.h"
+#include "mozilla/TimelineCollection.h"
+#include "mozilla/dom/AnimationEffect.h"
+#include "mozilla/dom/DocumentTimeline.h"
+#include "mozilla/dom/KeyframeEffect.h"
+#include "mozilla/dom/MutationObservers.h"
+#include "mozilla/dom/ScrollTimeline.h"
+#include "mozilla/dom/ViewTimeline.h"
+
+#include "nsPresContext.h"
+#include "nsPresContextInlines.h"
+#include "nsStyleChangeList.h"
+#include "nsLayoutUtils.h"
+#include "nsIFrame.h"
+#include "mozilla/dom/Document.h"
+#include "nsDOMMutationObserver.h"
+#include "nsRFPService.h"
+#include <algorithm> // std::stable_sort
+#include <math.h>
+
+using namespace mozilla;
+using namespace mozilla::css;
+using mozilla::dom::Animation;
+using mozilla::dom::AnimationPlayState;
+using mozilla::dom::CSSAnimation;
+using mozilla::dom::Element;
+using mozilla::dom::KeyframeEffect;
+using mozilla::dom::MutationObservers;
+using mozilla::dom::ScrollTimeline;
+using mozilla::dom::ViewTimeline;
+
+////////////////////////// nsAnimationManager ////////////////////////////
+
+// Find the matching animation by |aName| in the old list
+// of animations and remove the matched animation from the list.
+static already_AddRefed<CSSAnimation> PopExistingAnimation(
+ const nsAtom* aName,
+ nsAnimationManager::CSSAnimationCollection* aCollection) {
+ if (!aCollection) {
+ return nullptr;
+ }
+
+ // Animations are stored in reverse order to how they appear in the
+ // animation-name property. However, we want to match animations beginning
+ // from the end of the animation-name list, so we iterate *forwards*
+ // through the collection.
+ for (size_t idx = 0, length = aCollection->mAnimations.Length();
+ idx != length; ++idx) {
+ CSSAnimation* cssAnim = aCollection->mAnimations[idx];
+ if (cssAnim->AnimationName() == aName) {
+ RefPtr<CSSAnimation> match = cssAnim;
+ aCollection->mAnimations.RemoveElementAt(idx);
+ return match.forget();
+ }
+ }
+
+ return nullptr;
+}
+
+class MOZ_STACK_CLASS ServoCSSAnimationBuilder final {
+ public:
+ explicit ServoCSSAnimationBuilder(const ComputedStyle* aComputedStyle)
+ : mComputedStyle(aComputedStyle) {
+ MOZ_ASSERT(aComputedStyle);
+ }
+
+ bool BuildKeyframes(const Element& aElement, nsPresContext* aPresContext,
+ nsAtom* aName,
+ const StyleComputedTimingFunction& aTimingFunction,
+ nsTArray<Keyframe>& aKeyframes) {
+ return aPresContext->StyleSet()->GetKeyframesForName(
+ aElement, *mComputedStyle, aName, aTimingFunction, aKeyframes);
+ }
+ void SetKeyframes(KeyframeEffect& aEffect, nsTArray<Keyframe>&& aKeyframes,
+ const dom::AnimationTimeline* aTimeline) {
+ aEffect.SetKeyframes(std::move(aKeyframes), mComputedStyle, aTimeline);
+ }
+
+ // Currently all the animation building code in this file is based on
+ // assumption that creating and removing animations should *not* trigger
+ // additional restyles since those changes will be handled within the same
+ // restyle.
+ //
+ // While that is true for the Gecko style backend, it is not true for the
+ // Servo style backend where we want restyles to be triggered so that we
+ // perform a second animation restyle where we will incorporate the changes
+ // arising from creating and removing animations.
+ //
+ // Fortunately, our attempts to avoid posting extra restyles as part of the
+ // processing here are imperfect and most of the time we happen to post
+ // them anyway. Occasionally, however, we don't. For example, we don't post
+ // a restyle when we create a new animation whose an animation index matches
+ // the default value it was given already (which is typically only true when
+ // the CSSAnimation we create is the first Animation created in a particular
+ // content process).
+ //
+ // As a result, when we are using the Servo backend, whenever we have an added
+ // or removed animation we need to explicitly trigger a restyle.
+ //
+ // This code should eventually disappear along with the Gecko style backend
+ // and we should simply call Play() / Pause() / Cancel() etc. which will
+ // post the required restyles.
+ void NotifyNewOrRemovedAnimation(const Animation& aAnimation) {
+ dom::AnimationEffect* effect = aAnimation.GetEffect();
+ if (!effect) {
+ return;
+ }
+
+ KeyframeEffect* keyframeEffect = effect->AsKeyframeEffect();
+ if (!keyframeEffect) {
+ return;
+ }
+
+ keyframeEffect->RequestRestyle(EffectCompositor::RestyleType::Standard);
+ }
+
+ private:
+ const ComputedStyle* mComputedStyle;
+};
+
+static void UpdateOldAnimationPropertiesWithNew(
+ CSSAnimation& aOld, TimingParams&& aNewTiming,
+ nsTArray<Keyframe>&& aNewKeyframes, bool aNewIsStylePaused,
+ CSSAnimationProperties aOverriddenProperties,
+ ServoCSSAnimationBuilder& aBuilder, dom::AnimationTimeline* aTimeline,
+ dom::CompositeOperation aNewComposite) {
+ bool animationChanged = false;
+
+ // Update the old from the new so we can keep the original object
+ // identity (and any expando properties attached to it).
+ if (aOld.GetEffect()) {
+ dom::AnimationEffect* oldEffect = aOld.GetEffect();
+
+ // Copy across the changes that are not overridden
+ TimingParams updatedTiming = oldEffect->SpecifiedTiming();
+ if (~aOverriddenProperties & CSSAnimationProperties::Duration) {
+ updatedTiming.SetDuration(aNewTiming.Duration());
+ }
+ if (~aOverriddenProperties & CSSAnimationProperties::IterationCount) {
+ updatedTiming.SetIterations(aNewTiming.Iterations());
+ }
+ if (~aOverriddenProperties & CSSAnimationProperties::Direction) {
+ updatedTiming.SetDirection(aNewTiming.Direction());
+ }
+ if (~aOverriddenProperties & CSSAnimationProperties::Delay) {
+ updatedTiming.SetDelay(aNewTiming.Delay());
+ }
+ if (~aOverriddenProperties & CSSAnimationProperties::FillMode) {
+ updatedTiming.SetFill(aNewTiming.Fill());
+ }
+
+ animationChanged = oldEffect->SpecifiedTiming() != updatedTiming;
+ oldEffect->SetSpecifiedTiming(std::move(updatedTiming));
+
+ if (KeyframeEffect* oldKeyframeEffect = oldEffect->AsKeyframeEffect()) {
+ if (~aOverriddenProperties & CSSAnimationProperties::Keyframes) {
+ aBuilder.SetKeyframes(*oldKeyframeEffect, std::move(aNewKeyframes),
+ aTimeline);
+ }
+
+ if (~aOverriddenProperties & CSSAnimationProperties::Composition) {
+ animationChanged = oldKeyframeEffect->Composite() != aNewComposite;
+ oldKeyframeEffect->SetCompositeFromStyle(aNewComposite);
+ }
+ }
+ }
+
+ // Checking pointers should be enough. If both are scroll-timeline, we reuse
+ // the scroll-timeline object if their scrollers and axes are the same.
+ if (aOld.GetTimeline() != aTimeline) {
+ aOld.SetTimeline(aTimeline);
+ animationChanged = true;
+ }
+
+ // Handle changes in play state. If the animation is idle, however,
+ // changes to animation-play-state should *not* restart it.
+ if (aOld.PlayState() != AnimationPlayState::Idle &&
+ ~aOverriddenProperties & CSSAnimationProperties::PlayState) {
+ bool wasPaused = aOld.PlayState() == AnimationPlayState::Paused;
+ if (!wasPaused && aNewIsStylePaused) {
+ aOld.PauseFromStyle();
+ animationChanged = true;
+ } else if (wasPaused && !aNewIsStylePaused) {
+ aOld.PlayFromStyle();
+ animationChanged = true;
+ }
+ }
+
+ // Updating the effect timing above might already have caused the
+ // animation to become irrelevant so only add a changed record if
+ // the animation is still relevant.
+ if (animationChanged && aOld.IsRelevant()) {
+ MutationObservers::NotifyAnimationChanged(&aOld);
+ }
+}
+
+static already_AddRefed<dom::AnimationTimeline> GetNamedProgressTimeline(
+ dom::Document* aDocument, const NonOwningAnimationTarget& aTarget,
+ const nsAtom* aName) {
+ // A named progress timeline is referenceable in animation-timeline by:
+ // 1. the declaring element itself
+ // 2. that element’s descendants
+ // 3. that element’s following siblings and their descendants
+ // https://drafts.csswg.org/scroll-animations-1/#timeline-scope
+ // FIXME: Bug 1823500. Reduce default scoping to ancestors only.
+ for (Element* curr = AnimationUtils::GetElementForRestyle(
+ aTarget.mElement, aTarget.mPseudoType);
+ curr; curr = curr->GetParentElement()) {
+ // If multiple elements have declared the same timeline name, the matching
+ // timeline is the one declared on the nearest element in tree order, which
+ // considers siblings closer than parents.
+ // Note: This is fine for parallel traversal because we update animations by
+ // SequentialTask.
+ for (Element* e = curr; e; e = e->GetPreviousElementSibling()) {
+ // In case of a name conflict on the same element, scroll progress
+ // timelines take precedence over view progress timelines.
+ const auto [element, pseudoType] =
+ AnimationUtils::GetElementPseudoPair(e);
+ if (auto* collection =
+ TimelineCollection<ScrollTimeline>::Get(element, pseudoType)) {
+ if (RefPtr<ScrollTimeline> timeline = collection->Lookup(aName)) {
+ return timeline.forget();
+ }
+ }
+
+ if (auto* collection =
+ TimelineCollection<ViewTimeline>::Get(element, pseudoType)) {
+ if (RefPtr<ViewTimeline> timeline = collection->Lookup(aName)) {
+ return timeline.forget();
+ }
+ }
+ }
+ }
+
+ // If we cannot find a matched scroll-timeline-name, this animation is not
+ // associated with a timeline.
+ // https://drafts.csswg.org/css-animations-2/#valdef-animation-timeline-custom-ident
+ return nullptr;
+}
+
+static already_AddRefed<dom::AnimationTimeline> GetTimeline(
+ const StyleAnimationTimeline& aStyleTimeline, nsPresContext* aPresContext,
+ const NonOwningAnimationTarget& aTarget) {
+ switch (aStyleTimeline.tag) {
+ case StyleAnimationTimeline::Tag::Timeline: {
+ // Check scroll-timeline-name property or view-timeline-property.
+ const nsAtom* name = aStyleTimeline.AsTimeline().AsAtom();
+ return name != nsGkAtoms::_empty
+ ? GetNamedProgressTimeline(aPresContext->Document(), aTarget,
+ name)
+ : nullptr;
+ }
+ case StyleAnimationTimeline::Tag::Scroll: {
+ const auto& scroll = aStyleTimeline.AsScroll();
+ return ScrollTimeline::MakeAnonymous(aPresContext->Document(), aTarget,
+ scroll.axis, scroll.scroller);
+ }
+ case StyleAnimationTimeline::Tag::View: {
+ const auto& view = aStyleTimeline.AsView();
+ return ViewTimeline::MakeAnonymous(aPresContext->Document(), aTarget,
+ view.axis, view.inset);
+ }
+ case StyleAnimationTimeline::Tag::Auto:
+ return do_AddRef(aTarget.mElement->OwnerDoc()->Timeline());
+ }
+ MOZ_ASSERT_UNREACHABLE("Unknown animation-timeline value?");
+ return nullptr;
+}
+
+// Returns a new animation set up with given StyleAnimation.
+// Or returns an existing animation matching StyleAnimation's name updated
+// with the new StyleAnimation.
+static already_AddRefed<CSSAnimation> BuildAnimation(
+ nsPresContext* aPresContext, const NonOwningAnimationTarget& aTarget,
+ const nsStyleUIReset& aStyle, uint32_t animIdx,
+ ServoCSSAnimationBuilder& aBuilder,
+ nsAnimationManager::CSSAnimationCollection* aCollection) {
+ MOZ_ASSERT(aPresContext);
+
+ nsAtom* animationName = aStyle.GetAnimationName(animIdx);
+ nsTArray<Keyframe> keyframes;
+ if (!aBuilder.BuildKeyframes(*aTarget.mElement, aPresContext, animationName,
+ aStyle.GetAnimationTimingFunction(animIdx),
+ keyframes)) {
+ return nullptr;
+ }
+
+ TimingParams timing = TimingParamsFromCSSParams(
+ aStyle.GetAnimationDuration(animIdx).ToMilliseconds(),
+ aStyle.GetAnimationDelay(animIdx).ToMilliseconds(),
+ aStyle.GetAnimationIterationCount(animIdx),
+ aStyle.GetAnimationDirection(animIdx),
+ aStyle.GetAnimationFillMode(animIdx));
+
+ bool isStylePaused =
+ aStyle.GetAnimationPlayState(animIdx) == StyleAnimationPlayState::Paused;
+
+ RefPtr<dom::AnimationTimeline> timeline =
+ GetTimeline(aStyle.GetTimeline(animIdx), aPresContext, aTarget);
+
+ // Find the matching animation with animation name in the old list
+ // of animations and remove the matched animation from the list.
+ RefPtr<CSSAnimation> oldAnim =
+ PopExistingAnimation(animationName, aCollection);
+
+ if (oldAnim) {
+ // Copy over the start times and (if still paused) pause starts
+ // for each animation (matching on name only) that was also in the
+ // old list of animations.
+ // This means that we honor dynamic changes, which isn't what the
+ // spec says to do, but WebKit seems to honor at least some of
+ // them. See
+ // http://lists.w3.org/Archives/Public/www-style/2011Apr/0079.html
+ // In order to honor what the spec said, we'd copy more data over.
+ UpdateOldAnimationPropertiesWithNew(
+ *oldAnim, std::move(timing), std::move(keyframes), isStylePaused,
+ oldAnim->GetOverriddenProperties(), aBuilder, timeline,
+ aStyle.GetAnimationComposition(animIdx));
+ return oldAnim.forget();
+ }
+
+ KeyframeEffectParams effectOptions(aStyle.GetAnimationComposition(animIdx));
+ RefPtr<KeyframeEffect> effect = new dom::CSSAnimationKeyframeEffect(
+ aPresContext->Document(),
+ OwningAnimationTarget(aTarget.mElement, aTarget.mPseudoType),
+ std::move(timing), effectOptions);
+
+ aBuilder.SetKeyframes(*effect, std::move(keyframes), timeline);
+
+ RefPtr<CSSAnimation> animation = new CSSAnimation(
+ aPresContext->Document()->GetScopeObject(), animationName);
+ animation->SetOwningElement(
+ OwningElementRef(*aTarget.mElement, aTarget.mPseudoType));
+
+ animation->SetTimelineNoUpdate(timeline);
+ animation->SetEffectNoUpdate(effect);
+
+ if (isStylePaused) {
+ animation->PauseFromStyle();
+ } else {
+ animation->PlayFromStyle();
+ }
+
+ aBuilder.NotifyNewOrRemovedAnimation(*animation);
+
+ return animation.forget();
+}
+
+static nsAnimationManager::OwningCSSAnimationPtrArray BuildAnimations(
+ nsPresContext* aPresContext, const NonOwningAnimationTarget& aTarget,
+ const nsStyleUIReset& aStyle, ServoCSSAnimationBuilder& aBuilder,
+ nsAnimationManager::CSSAnimationCollection* aCollection,
+ nsTHashSet<RefPtr<nsAtom>>& aReferencedAnimations) {
+ nsAnimationManager::OwningCSSAnimationPtrArray result;
+
+ for (size_t animIdx = aStyle.mAnimationNameCount; animIdx-- != 0;) {
+ nsAtom* name = aStyle.GetAnimationName(animIdx);
+ // CSS Animations whose animation-name does not match a @keyframes rule do
+ // not generate animation events. This includes when the animation-name is
+ // "none" which is represented by an empty name in the StyleAnimation.
+ // Since such animations neither affect style nor dispatch events, we do
+ // not generate a corresponding CSSAnimation for them.
+ if (name == nsGkAtoms::_empty) {
+ continue;
+ }
+
+ aReferencedAnimations.Insert(name);
+ RefPtr<CSSAnimation> dest = BuildAnimation(aPresContext, aTarget, aStyle,
+ animIdx, aBuilder, aCollection);
+ if (!dest) {
+ continue;
+ }
+
+ dest->SetAnimationIndex(static_cast<uint64_t>(animIdx));
+ result.AppendElement(dest);
+ }
+ return result;
+}
+
+void nsAnimationManager::UpdateAnimations(dom::Element* aElement,
+ PseudoStyleType aPseudoType,
+ const ComputedStyle* aComputedStyle) {
+ MOZ_ASSERT(mPresContext->IsDynamic(),
+ "Should not update animations for print or print preview");
+ MOZ_ASSERT(aElement->IsInComposedDoc(),
+ "Should not update animations that are not attached to the "
+ "document tree");
+
+ if (!aComputedStyle ||
+ aComputedStyle->StyleDisplay()->mDisplay == StyleDisplay::None) {
+ // If we are in a display:none subtree we will have no computed values.
+ // However, if we are on the root of display:none subtree, the computed
+ // values might not have been cleared yet.
+ // In either case, since CSS animations should not run in display:none
+ // subtrees we should stop (actually, destroy) any animations on this
+ // element here.
+ StopAnimationsForElement(aElement, aPseudoType);
+ return;
+ }
+
+ NonOwningAnimationTarget target(aElement, aPseudoType);
+ ServoCSSAnimationBuilder builder(aComputedStyle);
+
+ DoUpdateAnimations(target, *aComputedStyle->StyleUIReset(), builder);
+}
+
+void nsAnimationManager::DoUpdateAnimations(
+ const NonOwningAnimationTarget& aTarget, const nsStyleUIReset& aStyle,
+ ServoCSSAnimationBuilder& aBuilder) {
+ // Everything that causes our animation data to change triggers a
+ // style change, which in turn triggers a non-animation restyle.
+ // Likewise, when we initially construct frames, we're not in a
+ // style change, but also not in an animation restyle.
+
+ auto* collection =
+ CSSAnimationCollection::Get(aTarget.mElement, aTarget.mPseudoType);
+ if (!collection && aStyle.mAnimationNameCount == 1 &&
+ aStyle.mAnimations[0].GetName() == nsGkAtoms::_empty) {
+ return;
+ }
+
+ nsAutoAnimationMutationBatch mb(aTarget.mElement->OwnerDoc());
+
+ // Build the updated animations list, extracting matching animations from
+ // the existing collection as we go.
+ OwningCSSAnimationPtrArray newAnimations =
+ BuildAnimations(mPresContext, aTarget, aStyle, aBuilder, collection,
+ mMaybeReferencedAnimations);
+
+ if (newAnimations.IsEmpty()) {
+ if (collection) {
+ collection->Destroy();
+ }
+ return;
+ }
+
+ if (!collection) {
+ collection =
+ &aTarget.mElement->EnsureAnimationData().EnsureAnimationCollection(
+ *aTarget.mElement, aTarget.mPseudoType);
+ if (!collection->isInList()) {
+ AddElementCollection(collection);
+ }
+ }
+ collection->mAnimations.SwapElements(newAnimations);
+
+ // Cancel removed animations
+ for (size_t newAnimIdx = newAnimations.Length(); newAnimIdx-- != 0;) {
+ aBuilder.NotifyNewOrRemovedAnimation(*newAnimations[newAnimIdx]);
+ newAnimations[newAnimIdx]->CancelFromStyle(PostRestyleMode::IfNeeded);
+ }
+}