In order to fix the failure of merge-timeline-offset-keyframes.html (and others), we have to do two things together: 1. Make sure we use the specified keyframe order when creating/merging keyframes. The later one overrides the previous ones, so we have to check the keyframes in the reverse order, for the keyfraems with timeline range names. 2. Fix the default composite for the generated 0% and 100% keyframes. Per spec, we use the default composite as their keyframe-specific composite, and the default composite is the corresponding computed value of animation-composition on element [1]. For the user-specified 0% and 100% keyframes, we still use auto because we don't resolve it when creating the animations. Note: It's unfortunate we have to resolve the composite of the generated keyframes earlier. This may have issues if someone changes the animation-composition property after creating the animations. I believe we will fix this in Bug 2037642 as well once we generate the missing keyframes on demand. [1] https://drafts.csswg.org/css-animations-2/#keyframes Differential Revision: https://phabricator.services.mozilla.com/D311207
704 lines
27 KiB
C++
704 lines
27 KiB
C++
/* 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 <algorithm> // std::stable_sort
|
||
|
||
#include "TimelineManager.h"
|
||
#include "mozilla/AnimationEventDispatcher.h"
|
||
#include "mozilla/AnimationUtils.h"
|
||
#include "mozilla/EffectCompositor.h"
|
||
#include "mozilla/ElementAnimationData.h"
|
||
#include "mozilla/ServoStyleSet.h"
|
||
#include "mozilla/TimelineCollection.h"
|
||
#include "mozilla/dom/AnimationEffect.h"
|
||
#include "mozilla/dom/Document.h"
|
||
#include "mozilla/dom/DocumentInlines.h"
|
||
#include "mozilla/dom/DocumentTimeline.h"
|
||
#include "mozilla/dom/ElementInlines.h"
|
||
#include "mozilla/dom/KeyframeEffect.h"
|
||
#include "mozilla/dom/MutationObservers.h"
|
||
#include "mozilla/dom/ScrollTimeline.h"
|
||
#include "mozilla/dom/ViewTimeline.h"
|
||
#include "nsDOMMutationObserver.h"
|
||
#include "nsIFrame.h"
|
||
#include "nsINode.h"
|
||
#include "nsLayoutUtils.h"
|
||
#include "nsPresContext.h"
|
||
#include "nsPresContextInlines.h"
|
||
#include "nsRFPService.h"
|
||
#include "nsStyleChangeList.h"
|
||
#include "nsTransitionManager.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::InactiveTimeline;
|
||
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,
|
||
const StyleAnimationComposition aComposition,
|
||
nsTArray<Keyframe>& aKeyframes) {
|
||
return aPresContext->StyleSet()->GetKeyframesForName(
|
||
aElement, *mComputedStyle, aName, aTimingFunction, aComposition,
|
||
aKeyframes);
|
||
}
|
||
void SetKeyframes(KeyframeEffect& aEffect, nsTArray<Keyframe>&& aKeyframes,
|
||
const dom::AnimationTimeline* aTimeline,
|
||
const dom::AnimationRange& aRange) {
|
||
aEffect.SetKeyframes(std::move(aKeyframes), mComputedStyle, aTimeline,
|
||
&aRange);
|
||
}
|
||
|
||
// 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 dom::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;
|
||
};
|
||
|
||
struct AnimationMatches {
|
||
bool operator()(const RefPtr<CSSAnimation>& aAnimation) {
|
||
return aAnimation.get() == mAnimation;
|
||
}
|
||
|
||
const CSSAnimation* mAnimation;
|
||
};
|
||
|
||
static void RemoveCorrespondingAnimation(
|
||
const nsAtom* aName, const CSSAnimation* aAnimation,
|
||
nsAnimationManager::TimelineNamesToAnimationMap&
|
||
aTimelineNamesToAnimationMap) {
|
||
auto result = aTimelineNamesToAnimationMap.Lookup(aName);
|
||
if (result) {
|
||
auto& l = result.Data();
|
||
auto foundIt =
|
||
std::find_if(l.cbegin(), l.cend(), AnimationMatches{aAnimation});
|
||
if (foundIt != l.cend()) {
|
||
l.RemoveElementAt(foundIt);
|
||
}
|
||
result.Remove();
|
||
}
|
||
#ifdef DEBUG
|
||
// One animation refers to one timeline, so if there is a duplication,
|
||
// something went wrong.
|
||
for (auto mapItr = aTimelineNamesToAnimationMap.Iter(); !mapItr.Done();
|
||
mapItr.Next()) {
|
||
auto& l = mapItr.Data();
|
||
auto foundIt =
|
||
std::find_if(l.cbegin(), l.cend(), AnimationMatches{aAnimation});
|
||
MOZ_ASSERT(foundIt == l.cend(), "Duplication animation entry");
|
||
}
|
||
#endif
|
||
}
|
||
|
||
static void UpdateOldAnimationPropertiesWithNew(
|
||
CSSAnimation& aOld, TimingParams&& aNewTiming,
|
||
nsTArray<Keyframe>&& aNewKeyframes, bool aNewIsStylePaused,
|
||
CSSAnimationProperties aOverriddenProperties,
|
||
ServoCSSAnimationBuilder& aBuilder, dom::AnimationTimeline* aTimeline,
|
||
const nsAtom* aTimelineName, dom::CompositeOperation aNewComposite,
|
||
dom::AnimationRange&& aTimelineRange,
|
||
nsAnimationManager::TimelineNamesToAnimationMap&
|
||
aTimelineNamesToAnimationMap) {
|
||
const auto* oldTimelineName = aOld.GetTimelineName();
|
||
const bool timelineReferenceChanged =
|
||
aOld.GetTimeline() != aTimeline || oldTimelineName != aTimelineName;
|
||
if (timelineReferenceChanged && oldTimelineName) {
|
||
RemoveCorrespondingAnimation(oldTimelineName, &aOld,
|
||
aTimelineNamesToAnimationMap);
|
||
}
|
||
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, aTimelineRange);
|
||
}
|
||
|
||
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) {
|
||
// See `UpdateNamedTimelineAnimation` as to why `SetTimeline` isn't used.
|
||
animationChanged =
|
||
animationChanged || aOld.SetTimelineNoUpdate(aTimeline, aTimelineName,
|
||
Animation::FromJS::No);
|
||
}
|
||
|
||
if (aOld.GetTimelineRange() != aTimelineRange) {
|
||
aOld.SetTimelineRange(std::move(aTimelineRange));
|
||
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);
|
||
}
|
||
|
||
if (timelineReferenceChanged && aTimelineName) {
|
||
auto& entries = aTimelineNamesToAnimationMap.LookupOrInsert(
|
||
aTimelineName, nsTArray<RefPtr<CSSAnimation>>{});
|
||
entries.AppendElement(&aOld);
|
||
}
|
||
}
|
||
|
||
static already_AddRefed<dom::AnimationTimeline> GetNamedProgressTimeline(
|
||
dom::Document* aDocument, const NonOwningAnimationTarget& aTarget,
|
||
const nsAtom* aName) {
|
||
auto* presContext = aDocument->GetPresContext();
|
||
const auto* timelineManager =
|
||
presContext ? presContext->TimelineManager() : nullptr;
|
||
// A named progress timeline is referenceable in animation-timeline by:
|
||
// 1. the declaring element itself
|
||
// 2. that element’s descendants
|
||
// https://drafts.csswg.org/scroll-animations-1/#timeline-scope
|
||
for (Element* e = aTarget.mElement->GetPseudoElement(aTarget.mPseudoRequest);
|
||
e; e = e->GetFlattenedTreeParentElement()) {
|
||
// 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.
|
||
const auto [element, pseudo] = AnimationUtils::GetElementPseudoPair(e);
|
||
if (auto* collection =
|
||
TimelineCollection<ScrollTimeline>::Get(element, pseudo)) {
|
||
if (RefPtr<ScrollTimeline> timeline = collection->Lookup(aName)) {
|
||
return timeline.forget();
|
||
}
|
||
}
|
||
|
||
if (auto* collection =
|
||
TimelineCollection<ViewTimeline>::Get(element, pseudo)) {
|
||
if (RefPtr<ViewTimeline> timeline = collection->Lookup(aName)) {
|
||
return timeline.forget();
|
||
}
|
||
}
|
||
|
||
if (!timelineManager) {
|
||
continue;
|
||
}
|
||
|
||
if (auto scopedTimeline = timelineManager->GetScopedTimeline(e, aName)) {
|
||
auto* result = scopedTimeline->take();
|
||
if (!result) {
|
||
// https://drafts.csswg.org/scroll-animations-1/#timeline-scoping
|
||
return MakeAndAddRef<InactiveTimeline>(aDocument);
|
||
}
|
||
return already_AddRefed{result};
|
||
}
|
||
}
|
||
|
||
// If we cannot find a matched scroll-timeline-name, this animation is not
|
||
// associated with a timeline.
|
||
// TODO(dshin): This is actually not spec compliant.. See
|
||
// https://github.com/w3c/csswg-drafts/issues/13955
|
||
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.
|
||
nsAtom* name = aStyleTimeline.AsTimeline().value.AsAtom();
|
||
if (name == nsGkAtoms::_empty) {
|
||
// `animation-timeline: none`.
|
||
return nullptr;
|
||
}
|
||
return GetNamedProgressTimeline(aPresContext->Document(), aTarget, name);
|
||
}
|
||
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;
|
||
}
|
||
|
||
static bool RefersToNamedTimeline(const CSSAnimation* aAnimation) {
|
||
return aAnimation->GetTimelineName();
|
||
}
|
||
|
||
// 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,
|
||
nsAnimationManager::TimelineNamesToAnimationMap&
|
||
aTimelineNamesToAnimationMap) {
|
||
MOZ_ASSERT(aPresContext);
|
||
|
||
nsAtom* animationName = aStyle.GetAnimationName(animIdx);
|
||
nsTArray<Keyframe> keyframes;
|
||
if (!aBuilder.BuildKeyframes(*aTarget.mElement, aPresContext, animationName,
|
||
aStyle.GetAnimationTimingFunction(animIdx),
|
||
aStyle.GetAnimationComposition(animIdx),
|
||
keyframes)) {
|
||
return nullptr;
|
||
}
|
||
|
||
const StyleAnimationDuration& duration = aStyle.GetAnimationDuration(animIdx);
|
||
TimingParams timing = TimingParamsFromCSSParams(
|
||
duration.IsAuto() ? Nothing() : Some(duration.AsTime().ToMilliseconds()),
|
||
aStyle.GetAnimationDelay(animIdx).ToMilliseconds(),
|
||
aStyle.GetAnimationIterationCount(animIdx),
|
||
aStyle.GetAnimationDirection(animIdx),
|
||
aStyle.GetAnimationFillMode(animIdx));
|
||
|
||
bool isStylePaused =
|
||
aStyle.GetAnimationPlayState(animIdx) == StyleAnimationPlayState::Paused;
|
||
|
||
const auto& styleTimeline = aStyle.GetTimeline(animIdx);
|
||
RefPtr<dom::AnimationTimeline> timeline =
|
||
GetTimeline(styleTimeline, aPresContext, aTarget);
|
||
auto timelineName = [&]() -> const nsAtom* {
|
||
if (!styleTimeline.IsTimeline()) {
|
||
return nullptr;
|
||
}
|
||
const auto* atom = styleTimeline.AsTimeline().value.AsAtom();
|
||
if (atom == nsGkAtoms::_empty) {
|
||
// This is actually `animation-timeline: none`.
|
||
return nullptr;
|
||
}
|
||
return atom;
|
||
}();
|
||
|
||
auto range = dom::AnimationRange{aStyle.GetAnimationRangeStart(animIdx),
|
||
aStyle.GetAnimationRangeEnd(animIdx)};
|
||
|
||
// 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);
|
||
|
||
const auto composition = StyleToDom(aStyle.GetAnimationComposition(animIdx));
|
||
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, timelineName,
|
||
composition, std::move(range), aTimelineNamesToAnimationMap);
|
||
// For now, only name-referenced timeline, or `none`, which is represented
|
||
// as IsTimeline with the empty atom, can result in no timeline.
|
||
MOZ_ASSERT_IF(timelineName && !timeline, styleTimeline.IsTimeline());
|
||
return oldAnim.forget();
|
||
}
|
||
|
||
KeyframeEffectParams effectOptions(composition);
|
||
auto effect = MakeRefPtr<dom::CSSAnimationKeyframeEffect>(
|
||
aPresContext->Document(),
|
||
OwningAnimationTarget(aTarget.mElement, aTarget.mPseudoRequest),
|
||
std::move(timing), effectOptions);
|
||
|
||
aBuilder.SetKeyframes(*effect, std::move(keyframes), timeline, range);
|
||
|
||
auto animation = MakeRefPtr<CSSAnimation>(
|
||
aPresContext->Document()->GetScopeObject(), animationName);
|
||
animation->SetOwningElement(
|
||
OwningElementRef(*aTarget.mElement, aTarget.mPseudoRequest));
|
||
|
||
animation->SetTimelineNoUpdate(timeline, timelineName, Animation::FromJS::No);
|
||
animation->SetEffectNoUpdate(effect);
|
||
animation->SetTimelineRangeNoUpdate(std::move(range));
|
||
|
||
if (isStylePaused) {
|
||
animation->PauseFromStyle();
|
||
} else {
|
||
animation->PlayFromStyle();
|
||
}
|
||
|
||
aBuilder.NotifyNewOrRemovedAnimation(*animation);
|
||
|
||
// For now, only `none` or name-referenced timeline can result in no timeline.
|
||
MOZ_ASSERT_IF(!timeline, styleTimeline.IsTimeline());
|
||
if (RefersToNamedTimeline(animation)) {
|
||
const auto* name = styleTimeline.AsTimeline().value.AsAtom();
|
||
auto& entries = aTimelineNamesToAnimationMap.LookupOrInsert(
|
||
name, nsTArray<RefPtr<CSSAnimation>>{});
|
||
entries.AppendElement(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::TimelineNamesToAnimationMap&
|
||
aTimelineNamesToAnimationMap) {
|
||
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, aTimelineNamesToAnimationMap);
|
||
if (!dest) {
|
||
continue;
|
||
}
|
||
|
||
dest->SetAnimationIndex(static_cast<uint64_t>(animIdx));
|
||
result.AppendElement(dest);
|
||
}
|
||
return result;
|
||
}
|
||
|
||
void nsAnimationManager::UpdateAnimations(
|
||
dom::Element* aElement, const PseudoStyleRequest& aPseudoRequest,
|
||
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, aPseudoRequest);
|
||
return;
|
||
}
|
||
|
||
NonOwningAnimationTarget target(aElement, aPseudoRequest);
|
||
ServoCSSAnimationBuilder builder(aComputedStyle);
|
||
|
||
DoUpdateAnimations(target, *aComputedStyle->StyleUIReset(), builder);
|
||
}
|
||
|
||
void nsAnimationManager::RemoveNamedTimelineAnimation(
|
||
const nsAtom* aName, mozilla::dom::CSSAnimation* aAnimation) {
|
||
RemoveCorrespondingAnimation(aName, aAnimation, mAnimationsWithNamedTimeline);
|
||
}
|
||
|
||
/**
|
||
* Try to update the named timeline associated with this animation.
|
||
* If such named timeline is not found, and if deferring is allowed,
|
||
* by passing a Some() aAnimationsWithDeferredUpdate, the animation
|
||
* is added to the provided hashset, to be updated once a frame.
|
||
* This follows the spec resolution made in
|
||
* https://github.com/w3c/csswg-drafts/issues/13963.
|
||
*/
|
||
static void UpdateNamedTimelineAnimation(
|
||
dom::Document* aDocument, CSSAnimation* aAnimation,
|
||
const nsAtom* aTimelineName,
|
||
Maybe<nsTHashSet<RefPtr<mozilla::dom::CSSAnimation>>&>
|
||
aAnimationsWithDeferredUpdate) {
|
||
if (aTimelineName != aAnimation->GetTimelineName()) {
|
||
return;
|
||
}
|
||
const auto target = aAnimation->GetTargetForAnimation();
|
||
const RefPtr<dom::AnimationTimeline> newTimeline =
|
||
GetNamedProgressTimeline(aDocument, target, aTimelineName);
|
||
const auto* oldTimeline = aAnimation->GetTimeline();
|
||
if (oldTimeline == newTimeline) {
|
||
return;
|
||
}
|
||
if (aAnimationsWithDeferredUpdate &&
|
||
(!newTimeline || newTimeline->IsInactiveTimeline())) {
|
||
// We know this animation is looking for a named animation - but it does not
|
||
// exist. One may become available later, so defer setting the new timeline
|
||
// (There may be more incoming changes).
|
||
aAnimationsWithDeferredUpdate->Insert(aAnimation);
|
||
return;
|
||
}
|
||
// No need to call `SetTimeline` and force compositor animation update -
|
||
// timeline changing shouldn't cause change in animation state or playback
|
||
// rate.
|
||
aAnimation->SetTimelineNoUpdate(newTimeline, aTimelineName,
|
||
Animation::FromJS::No);
|
||
}
|
||
|
||
#ifdef DEBUG
|
||
static void CheckNamedTimelineMap(
|
||
nsAnimationManager::TimelineNamesToAnimationMap&
|
||
aTimelineNamesToAnimationMap) {
|
||
for (const auto& key : aTimelineNamesToAnimationMap.Keys()) {
|
||
MOZ_ASSERT(key != nsGkAtoms::_empty);
|
||
}
|
||
}
|
||
#endif
|
||
|
||
void nsAnimationManager::UpdateNamedTimelineAnimations(
|
||
const nsTArray<RefPtr<const nsAtom>>& aChanged) {
|
||
auto* document = mPresContext->Document();
|
||
for (const auto& name : aChanged) {
|
||
auto entries = mAnimationsWithNamedTimeline.Lookup(name);
|
||
if (!entries) {
|
||
continue;
|
||
}
|
||
for (auto& animation : *entries) {
|
||
UpdateNamedTimelineAnimation(document, animation.get(), name.get(),
|
||
SomeRef(mAnimationsWithDeferredUpdate));
|
||
}
|
||
}
|
||
#ifdef DEBUG
|
||
CheckNamedTimelineMap(mAnimationsWithNamedTimeline);
|
||
#endif
|
||
}
|
||
|
||
void nsAnimationManager::UpdateAllNamedTimelineAnimations() {
|
||
auto* document = mPresContext->Document();
|
||
for (auto& entry : mAnimationsWithNamedTimeline) {
|
||
const auto& name = entry.GetKey();
|
||
for (auto& animation : entry.GetData()) {
|
||
UpdateNamedTimelineAnimation(document, animation.get(), name,
|
||
SomeRef(mAnimationsWithDeferredUpdate));
|
||
}
|
||
}
|
||
#ifdef DEBUG
|
||
CheckNamedTimelineMap(mAnimationsWithNamedTimeline);
|
||
#endif
|
||
}
|
||
|
||
void nsAnimationManager::UpdateDeferredTimelineChanges() {
|
||
if (mAnimationsWithDeferredUpdate.IsEmpty()) {
|
||
return;
|
||
}
|
||
auto* document = mPresContext->Document();
|
||
for (auto* animation : mAnimationsWithDeferredUpdate) {
|
||
if (!animation->GetTimelineName()) {
|
||
// May have switched to e.g. a document timeline.
|
||
// That's ok, just skip it.
|
||
continue;
|
||
}
|
||
UpdateNamedTimelineAnimation(document, animation,
|
||
animation->GetTimelineName(), Nothing{});
|
||
}
|
||
mAnimationsWithDeferredUpdate.Clear();
|
||
#ifdef DEBUG
|
||
CheckNamedTimelineMap(mAnimationsWithNamedTimeline);
|
||
#endif
|
||
}
|
||
|
||
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.mPseudoRequest);
|
||
if (!collection && aStyle.mAnimationNameCount == 1 &&
|
||
aStyle.mAnimations[0].GetName() == nsGkAtoms::_empty) {
|
||
return;
|
||
}
|
||
|
||
nsAutoAnimationMutationBatch mb(aTarget.mElement->OwnerDoc());
|
||
|
||
// Build the updated animations list. Even if we remove entries in
|
||
// `mAnimationsWithNamedTimeline` in this function later, entries are added
|
||
// when we build the list, breaking the symmetry. This is unfortunate, but
|
||
// unavoidable, because `newAmimations.Length()` does not necessarily equal to
|
||
// the length of animations defined in `aStyle`, e.g. when a referenced
|
||
// `animiation-name` does not exist.
|
||
//
|
||
// Old entries in `collection` may be reused. If they are, they are removed
|
||
// from `collection` and put into `newAnimations`.
|
||
OwningCSSAnimationPtrArray newAnimations =
|
||
BuildAnimations(mPresContext, aTarget, aStyle, aBuilder, collection,
|
||
mMaybeReferencedAnimations, mAnimationsWithNamedTimeline);
|
||
|
||
if (newAnimations.IsEmpty()) {
|
||
if (collection) {
|
||
for (const auto& animation : collection->mAnimations) {
|
||
if (!RefersToNamedTimeline(animation)) {
|
||
continue;
|
||
}
|
||
RemoveCorrespondingAnimation(animation->GetTimelineName(), animation,
|
||
mAnimationsWithNamedTimeline);
|
||
}
|
||
collection->Destroy();
|
||
}
|
||
return;
|
||
}
|
||
|
||
if (!collection) {
|
||
collection =
|
||
&aTarget.mElement->EnsureAnimationData().EnsureAnimationCollection(
|
||
*aTarget.mElement, aTarget.mPseudoRequest);
|
||
if (!collection->isInList()) {
|
||
AddElementCollection(collection);
|
||
}
|
||
}
|
||
collection->mAnimations.SwapElements(newAnimations);
|
||
|
||
// Cancel removed animations
|
||
for (size_t newAnimIdx = newAnimations.Length(); newAnimIdx-- != 0;) {
|
||
const auto& anim = newAnimations[newAnimIdx];
|
||
aBuilder.NotifyNewOrRemovedAnimation(*anim);
|
||
newAnimations[newAnimIdx]->CancelFromStyle(PostRestyleMode::IfNeeded);
|
||
if (RefersToNamedTimeline(anim)) {
|
||
RemoveCorrespondingAnimation(anim->GetTimelineName(), anim,
|
||
mAnimationsWithNamedTimeline);
|
||
}
|
||
}
|
||
}
|