... Because we are currently unconditionally re-creating the timeline even when setting animation-play-state, we end up setting a flag for auto-aligning the start time, and gCS returns style without animation cascade. Differential Revision: https://phabricator.services.mozilla.com/D317212
727 lines
28 KiB
C++
727 lines
28 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/StaticPrefs_layout.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/TimelineName.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::KeyframeEffect;
|
|
using mozilla::dom::MutationObservers;
|
|
using mozilla::dom::ScrollTimeline;
|
|
using mozilla::dom::UnresolvedTimeline;
|
|
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,
|
|
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();
|
|
// Note that we don't care about scoped names - we already know that
|
|
// we need to remove this exact animation.
|
|
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 dom::ScopedTimelineName& aTimelineName,
|
|
const StyleComputedTimingFunction& aNewTimingFunction,
|
|
dom::CompositeOperation aNewComposite, dom::AnimationRange&& aTimelineRange,
|
|
nsAnimationManager::TimelineNamesToAnimationMap&
|
|
aTimelineNamesToAnimationMap) {
|
|
const auto oldTimelineName = aOld.GetTimelineName();
|
|
const bool timelineReferenceChanged =
|
|
aOld.GetTimeline() != aTimeline ||
|
|
oldTimelineName.mName != aTimelineName.mName;
|
|
if (timelineReferenceChanged && oldTimelineName.mName) {
|
|
RemoveCorrespondingAnimation(oldTimelineName.mName, &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);
|
|
|
|
// The default timing function and default composite for CSS Keyframes
|
|
// processing.
|
|
if (auto* cssEffect =
|
|
oldKeyframeEffect->AsCSSAnimationKeyframeEffect()) {
|
|
cssEffect->SetDefaultTimingFunction(aNewTimingFunction);
|
|
cssEffect->SetDefaultComposite(aNewComposite);
|
|
}
|
|
}
|
|
|
|
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.
|
|
const bool timelineDidChange = aOld.SetTimelineNoUpdate(
|
|
aTimeline, aTimelineName, Animation::FromJS::No);
|
|
animationChanged = animationChanged || timelineDidChange;
|
|
}
|
|
|
|
if (aOld.GetTimelineRange() != aTimelineRange) {
|
|
aOld.SetTimelineRange(std::move(aTimelineRange), Animation::FromJS::No);
|
|
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.mName) {
|
|
auto& entries = aTimelineNamesToAnimationMap.LookupOrInsert(
|
|
aTimelineName.mName, nsTArray<RefPtr<CSSAnimation>>{});
|
|
entries.AppendElement(&aOld);
|
|
}
|
|
}
|
|
|
|
// https://drafts.csswg.org/scroll-animations-1/#timeline-scoping
|
|
static already_AddRefed<dom::AnimationTimeline> GetNamedProgressTimeline(
|
|
dom::Document* aDocument, const NonOwningAnimationTarget& aTarget,
|
|
const dom::ScopedTimelineName& aName) {
|
|
auto* presContext = aDocument->GetPresContext();
|
|
const auto* targetShadowRoot =
|
|
Servo_GetShadowRootForScoped(aTarget.mElement, aName.mCascadeLevel);
|
|
const auto* timelineManager =
|
|
presContext ? presContext->TimelineManager() : nullptr;
|
|
for (Element* e = aTarget.mElement->GetPseudoElement(aTarget.mPseudoRequest);
|
|
e; e = e->GetParentElementCrossingShadowRoot()) {
|
|
// Check ourselves first.
|
|
const auto [element, pseudo] = AnimationUtils::GetElementPseudoPair(e);
|
|
if (auto result = TimelineManager::GetNamedTimelineForThisElement(
|
|
element, pseudo, aName.mName, targetShadowRoot)) {
|
|
return result.forget();
|
|
}
|
|
// We're scoped, or have reached the top.
|
|
if (timelineManager &&
|
|
(timelineManager->TimelineNameScopedByElement(element, aName.mName) ||
|
|
e->IsRootElement())) {
|
|
return timelineManager->GetNamedTimelineInSubtree(
|
|
element, aName.mName, targetShadowRoot, aDocument);
|
|
}
|
|
// Continue the search with our parent.
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
static already_AddRefed<dom::AnimationTimeline> GetTimeline(
|
|
const StyleAnimationTimeline& aStyleTimeline, nsPresContext* aPresContext,
|
|
const NonOwningAnimationTarget& aTarget,
|
|
dom::AnimationTimeline* aOldTimeline) {
|
|
switch (aStyleTimeline.tag) {
|
|
case StyleAnimationTimeline::Tag::Timeline: {
|
|
// Check scroll-timeline-name property or view-timeline-property.
|
|
const auto& scopedName =
|
|
dom::ScopedTimelineName{aStyleTimeline.AsTimeline()};
|
|
if (scopedName.mName == nsGkAtoms::_empty) {
|
|
// `animation-timeline: none`.
|
|
return nullptr;
|
|
}
|
|
return GetNamedProgressTimeline(aPresContext->Document(), aTarget,
|
|
scopedName);
|
|
}
|
|
case StyleAnimationTimeline::Tag::Scroll: {
|
|
const auto& scroll = aStyleTimeline.AsScroll();
|
|
const bool reuseOldTimeline = [&]() {
|
|
const auto* scrollTimeline =
|
|
aOldTimeline ? aOldTimeline->AsScrollTimeline() : nullptr;
|
|
if (!scrollTimeline || scrollTimeline->IsViewTimeline()) {
|
|
return false;
|
|
}
|
|
return scrollTimeline->IsReusableAnonymousTimeline(scroll);
|
|
}();
|
|
if (reuseOldTimeline) {
|
|
// TODO(dshin): Refcount churn. Hopefully this doesn't happen often
|
|
// enough?
|
|
return do_AddRef(aOldTimeline);
|
|
}
|
|
return ScrollTimeline::MakeAnonymous(aPresContext->Document(), aTarget,
|
|
scroll.axis, scroll.scroller);
|
|
}
|
|
case StyleAnimationTimeline::Tag::View: {
|
|
const auto& view = aStyleTimeline.AsView();
|
|
const bool reuseOldTimeline = [&]() {
|
|
const auto* viewTimeline =
|
|
aOldTimeline ? aOldTimeline->AsViewTimeline() : nullptr;
|
|
if (!viewTimeline) {
|
|
return false;
|
|
}
|
|
return viewTimeline->IsReusableAnonymousTimeline(view);
|
|
}();
|
|
if (reuseOldTimeline) {
|
|
// TODO(dshin): Same potential issue as `scroll()` above.
|
|
return do_AddRef(aOldTimeline);
|
|
}
|
|
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().mName;
|
|
}
|
|
|
|
// 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);
|
|
const StyleComputedTimingFunction& timingFunction =
|
|
aStyle.GetAnimationTimingFunction(animIdx);
|
|
nsTArray<Keyframe> keyframes;
|
|
if (!aBuilder.BuildKeyframes(*aTarget.mElement, aPresContext, animationName,
|
|
timingFunction, 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;
|
|
|
|
// 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& styleTimeline = aStyle.GetTimeline(animIdx);
|
|
RefPtr<dom::AnimationTimeline> timeline =
|
|
GetTimeline(styleTimeline, aPresContext, aTarget,
|
|
oldAnim ? oldAnim->GetTimeline() : nullptr);
|
|
auto timelineName = [&]() -> dom::ScopedTimelineName {
|
|
if (!styleTimeline.IsTimeline()) {
|
|
return {};
|
|
}
|
|
const auto* atom = styleTimeline.AsTimeline().value.AsAtom();
|
|
if (atom == nsGkAtoms::_empty) {
|
|
// This is actually `animation-timeline: none`.
|
|
return {};
|
|
}
|
|
return dom::ScopedTimelineName{styleTimeline.AsTimeline()};
|
|
}();
|
|
|
|
auto range = dom::AnimationRange{aStyle.GetAnimationRangeStart(animIdx),
|
|
aStyle.GetAnimationRangeEnd(animIdx)};
|
|
|
|
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->PropertiesOverridenByJS(), aBuilder, timeline.get(),
|
|
timelineName, timingFunction, 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.mName && !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);
|
|
effect->SetDefaultTimingFunction(timingFunction);
|
|
effect->SetDefaultComposite(composition);
|
|
|
|
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), Animation::FromJS::No);
|
|
|
|
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 ||
|
|
(!StaticPrefs::layout_css_display_animations_enabled() &&
|
|
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.
|
|
// Stop (actually, destroy) any animations here: either there are no
|
|
// computed values, or display animations are disabled and this is a
|
|
// display:none root.
|
|
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) {
|
|
const auto scopedName = aAnimation->GetTimelineName();
|
|
if (aTimelineName != scopedName.mName) {
|
|
return;
|
|
}
|
|
const auto target = aAnimation->GetTargetForAnimation();
|
|
// Ok, we know at least one timeline by this name got updated, which may
|
|
// change our lookup result. Lookup by our scoped name again.
|
|
const RefPtr<dom::AnimationTimeline> newTimeline =
|
|
GetNamedProgressTimeline(aDocument, target, scopedName);
|
|
const auto* oldTimeline = aAnimation->GetTimeline();
|
|
if (oldTimeline == newTimeline) {
|
|
return;
|
|
}
|
|
if (aAnimationsWithDeferredUpdate &&
|
|
(!newTimeline || newTimeline->IsUnresolvedTimeline())) {
|
|
// 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, scopedName,
|
|
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().mName) {
|
|
// May have switched to e.g. a document timeline.
|
|
// That's ok, just skip it.
|
|
continue;
|
|
}
|
|
UpdateNamedTimelineAnimation(document, animation,
|
|
animation->GetTimelineName().mName, 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().mName,
|
|
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().mName, anim,
|
|
mAnimationsWithNamedTimeline);
|
|
}
|
|
}
|
|
}
|