From eee4796cb14e5fdc4a387ed470a1658785749062 Mon Sep 17 00:00:00 2001 From: Hiroyuki Ikezoe Date: Tue, 5 May 2020 10:01:33 +0000 Subject: [PATCH] Bug 1634943 - Split CSSTransition class into a new file in dom/animation/. r=boris The motivation here is that we will want to call CSSTransition specific functions, e.g. updating the start value of a given CSSTransition with the latest value of the CSSTransition on the compositor, from somewhere not in layout/style. Unfotunately nsTransitionManager.h is not exposed and we will never want to expose it since it's purely for layout/style stuff. Differential Revision: https://phabricator.services.mozilla.com/D73571 --- dom/animation/CSSTransition.cpp | 338 +++++++++++++++++++++++++++ dom/animation/CSSTransition.h | 245 +++++++++++++++++++ dom/animation/moz.build | 2 + layout/style/AnimationCollection.cpp | 6 +- layout/style/nsTransitionManager.cpp | 333 -------------------------- layout/style/nsTransitionManager.h | 241 +------------------ 6 files changed, 589 insertions(+), 576 deletions(-) create mode 100644 dom/animation/CSSTransition.cpp create mode 100644 dom/animation/CSSTransition.h diff --git a/dom/animation/CSSTransition.cpp b/dom/animation/CSSTransition.cpp new file mode 100644 index 000000000000..823c0e14d2f6 --- /dev/null +++ b/dom/animation/CSSTransition.cpp @@ -0,0 +1,338 @@ +/* -*- 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 "CSSTransition.h" + +#include "mozilla/AnimationEventDispatcher.h" +#include "mozilla/dom/CSSTransitionBinding.h" +#include "mozilla/dom/KeyframeEffectBinding.h" +#include "mozilla/dom/KeyframeEffect.h" +#include "mozilla/TimeStamp.h" +#include "nsPresContext.h" + +namespace mozilla { +namespace dom { + +JSObject* CSSTransition::WrapObject(JSContext* aCx, + JS::Handle aGivenProto) { + return dom::CSSTransition_Binding::Wrap(aCx, this, aGivenProto); +} + +void CSSTransition::GetTransitionProperty(nsString& aRetVal) const { + MOZ_ASSERT(eCSSProperty_UNKNOWN != mTransitionProperty, + "Transition Property should be initialized"); + aRetVal = + NS_ConvertUTF8toUTF16(nsCSSProps::GetStringValue(mTransitionProperty)); +} + +AnimationPlayState CSSTransition::PlayStateFromJS() const { + FlushUnanimatedStyle(); + return Animation::PlayStateFromJS(); +} + +bool CSSTransition::PendingFromJS() const { + // Transitions don't become pending again after they start running but, if + // while the transition is still pending, style is updated in such a way + // that the transition will be canceled, we need to report false here. + // Hence we need to flush, but only when we're pending. + if (Pending()) { + FlushUnanimatedStyle(); + } + return Animation::PendingFromJS(); +} + +void CSSTransition::PlayFromJS(ErrorResult& aRv) { + FlushUnanimatedStyle(); + Animation::PlayFromJS(aRv); +} + +void CSSTransition::UpdateTiming(SeekFlag aSeekFlag, + SyncNotifyFlag aSyncNotifyFlag) { + if (mNeedsNewAnimationIndexWhenRun && + PlayState() != AnimationPlayState::Idle) { + mAnimationIndex = sNextAnimationIndex++; + mNeedsNewAnimationIndexWhenRun = false; + } + + Animation::UpdateTiming(aSeekFlag, aSyncNotifyFlag); +} + +void CSSTransition::QueueEvents(const StickyTimeDuration& aActiveTime) { + if (!mOwningElement.IsSet()) { + return; + } + + nsPresContext* presContext = mOwningElement.GetPresContext(); + if (!presContext) { + return; + } + + static constexpr StickyTimeDuration zeroDuration = StickyTimeDuration(); + + TransitionPhase currentPhase; + StickyTimeDuration intervalStartTime; + StickyTimeDuration intervalEndTime; + + if (!mEffect) { + currentPhase = GetAnimationPhaseWithoutEffect(*this); + } else { + ComputedTiming computedTiming = mEffect->GetComputedTiming(); + + currentPhase = static_cast(computedTiming.mPhase); + intervalStartTime = IntervalStartTime(computedTiming.mActiveDuration); + intervalEndTime = IntervalEndTime(computedTiming.mActiveDuration); + } + + if (mPendingState != PendingState::NotPending && + (mPreviousTransitionPhase == TransitionPhase::Idle || + mPreviousTransitionPhase == TransitionPhase::Pending)) { + currentPhase = TransitionPhase::Pending; + } + + if (currentPhase == mPreviousTransitionPhase) { + return; + } + + // TimeStamps to use for ordering the events when they are dispatched. We + // use a TimeStamp so we can compare events produced by different elements, + // perhaps even with different timelines. + // The zero timestamp is for transitionrun events where we ignore the delay + // for the purpose of ordering events. + TimeStamp zeroTimeStamp = AnimationTimeToTimeStamp(zeroDuration); + TimeStamp startTimeStamp = ElapsedTimeToTimeStamp(intervalStartTime); + TimeStamp endTimeStamp = ElapsedTimeToTimeStamp(intervalEndTime); + + AutoTArray events; + + auto appendTransitionEvent = [&](EventMessage aMessage, + const StickyTimeDuration& aElapsedTime, + const TimeStamp& aScheduledEventTimeStamp) { + double elapsedTime = aElapsedTime.ToSeconds(); + if (aMessage == eTransitionCancel) { + // 0 is an inappropriate value for this callsite. What we need to do is + // use a single random value for all increasing times reportable. + // That is to say, whenever elapsedTime goes negative (because an + // animation restarts, something rewinds the animation, or otherwise) + // a new random value for the mix-in must be generated. + elapsedTime = + nsRFPService::ReduceTimePrecisionAsSecsRFPOnly(elapsedTime, 0); + } + events.AppendElement(AnimationEventInfo( + TransitionProperty(), mOwningElement.Target(), aMessage, elapsedTime, + aScheduledEventTimeStamp, this)); + }; + + // Handle cancel events first + if ((mPreviousTransitionPhase != TransitionPhase::Idle && + mPreviousTransitionPhase != TransitionPhase::After) && + currentPhase == TransitionPhase::Idle) { + appendTransitionEvent(eTransitionCancel, aActiveTime, + GetTimelineCurrentTimeAsTimeStamp()); + } + + // All other events + switch (mPreviousTransitionPhase) { + case TransitionPhase::Idle: + if (currentPhase == TransitionPhase::Pending || + currentPhase == TransitionPhase::Before) { + appendTransitionEvent(eTransitionRun, intervalStartTime, zeroTimeStamp); + } else if (currentPhase == TransitionPhase::Active) { + appendTransitionEvent(eTransitionRun, intervalStartTime, zeroTimeStamp); + appendTransitionEvent(eTransitionStart, intervalStartTime, + startTimeStamp); + } else if (currentPhase == TransitionPhase::After) { + appendTransitionEvent(eTransitionRun, intervalStartTime, zeroTimeStamp); + appendTransitionEvent(eTransitionStart, intervalStartTime, + startTimeStamp); + appendTransitionEvent(eTransitionEnd, intervalEndTime, endTimeStamp); + } + break; + + case TransitionPhase::Pending: + case TransitionPhase::Before: + if (currentPhase == TransitionPhase::Active) { + appendTransitionEvent(eTransitionStart, intervalStartTime, + startTimeStamp); + } else if (currentPhase == TransitionPhase::After) { + appendTransitionEvent(eTransitionStart, intervalStartTime, + startTimeStamp); + appendTransitionEvent(eTransitionEnd, intervalEndTime, endTimeStamp); + } + break; + + case TransitionPhase::Active: + if (currentPhase == TransitionPhase::After) { + appendTransitionEvent(eTransitionEnd, intervalEndTime, endTimeStamp); + } else if (currentPhase == TransitionPhase::Before) { + appendTransitionEvent(eTransitionEnd, intervalStartTime, + startTimeStamp); + } + break; + + case TransitionPhase::After: + if (currentPhase == TransitionPhase::Active) { + appendTransitionEvent(eTransitionStart, intervalEndTime, + startTimeStamp); + } else if (currentPhase == TransitionPhase::Before) { + appendTransitionEvent(eTransitionStart, intervalEndTime, + startTimeStamp); + appendTransitionEvent(eTransitionEnd, intervalStartTime, endTimeStamp); + } + break; + } + mPreviousTransitionPhase = currentPhase; + + if (!events.IsEmpty()) { + presContext->AnimationEventDispatcher()->QueueEvents(std::move(events)); + } +} + +void CSSTransition::Tick() { + Animation::Tick(); + QueueEvents(); +} + +nsCSSPropertyID CSSTransition::TransitionProperty() const { + MOZ_ASSERT(eCSSProperty_UNKNOWN != mTransitionProperty, + "Transition property should be initialized"); + return mTransitionProperty; +} + +AnimationValue CSSTransition::ToValue() const { + MOZ_ASSERT(!mTransitionToValue.IsNull(), + "Transition ToValue should be initialized"); + return mTransitionToValue; +} + +bool CSSTransition::HasLowerCompositeOrderThan( + const CSSTransition& aOther) const { + MOZ_ASSERT(IsTiedToMarkup() && aOther.IsTiedToMarkup(), + "Should only be called for CSS transitions that are sorted " + "as CSS transitions (i.e. tied to CSS markup)"); + + // 0. Object-equality case + if (&aOther == this) { + return false; + } + + // 1. Sort by document order + if (!mOwningElement.Equals(aOther.mOwningElement)) { + return mOwningElement.LessThan( + const_cast(this)->CachedChildIndexRef(), + aOther.mOwningElement, + const_cast(&aOther)->CachedChildIndexRef()); + } + + // 2. (Same element and pseudo): Sort by transition generation + if (mAnimationIndex != aOther.mAnimationIndex) { + return mAnimationIndex < aOther.mAnimationIndex; + } + + // 3. (Same transition generation): Sort by transition property + return nsCSSProps::GetStringValue(TransitionProperty()) < + nsCSSProps::GetStringValue(aOther.TransitionProperty()); +} + +/* static */ +Nullable CSSTransition::GetCurrentTimeAt( + const AnimationTimeline& aTimeline, const TimeStamp& aBaseTime, + const TimeDuration& aStartTime, double aPlaybackRate) { + Nullable result; + + Nullable timelineTime = aTimeline.ToTimelineTime(aBaseTime); + if (!timelineTime.IsNull()) { + result.SetValue( + (timelineTime.Value() - aStartTime).MultDouble(aPlaybackRate)); + } + + return result; +} + +double CSSTransition::CurrentValuePortion() const { + if (!GetEffect()) { + return 0.0; + } + + // Transitions use a fill mode of 'backwards' so GetComputedTiming will + // never return a null time progress due to being *before* the animation + // interval. However, it might be possible that we're behind on flushing + // causing us to get called *after* the animation interval. So, just in + // case, we override the fill mode to 'both' to ensure the progress + // is never null. + TimingParams timingToUse = GetEffect()->SpecifiedTiming(); + timingToUse.SetFill(dom::FillMode::Both); + ComputedTiming computedTiming = GetEffect()->GetComputedTiming(&timingToUse); + + if (computedTiming.mProgress.IsNull()) { + return 0.0; + } + + // 'transition-timing-function' corresponds to the effect timing while + // the transition keyframes have a linear timing function so we can ignore + // them for the purposes of calculating the value portion. + return computedTiming.mProgress.Value(); +} + +void CSSTransition::UpdateStartValueFromReplacedTransition() { + MOZ_ASSERT(mEffect && mEffect->AsKeyframeEffect() && + mEffect->AsKeyframeEffect()->HasAnimationOfPropertySet( + nsCSSPropertyIDSet::CompositorAnimatables()), + "Should be called for compositor-runnable transitions"); + + MOZ_ASSERT(mTimeline, + "Should have a timeline if we are replacing transition start " + "values"); + + if (!mReplacedTransition) { + return; + } + + ComputedTiming computedTiming = AnimationEffect::GetComputedTimingAt( + CSSTransition::GetCurrentTimeAt(*mTimeline, TimeStamp::Now(), + mReplacedTransition->mStartTime, + mReplacedTransition->mPlaybackRate), + mReplacedTransition->mTiming, mReplacedTransition->mPlaybackRate); + + if (!computedTiming.mProgress.IsNull()) { + double valuePosition = ComputedTimingFunction::GetPortion( + mReplacedTransition->mTimingFunction, computedTiming.mProgress.Value(), + computedTiming.mBeforeFlag); + + const AnimationValue& replacedFrom = mReplacedTransition->mFromValue; + const AnimationValue& replacedTo = mReplacedTransition->mToValue; + AnimationValue startValue; + startValue.mServo = + Servo_AnimationValues_Interpolate(replacedFrom.mServo, + replacedTo.mServo, valuePosition) + .Consume(); + + mEffect->AsKeyframeEffect()->ReplaceTransitionStartValue( + std::move(startValue)); + } + + mReplacedTransition.reset(); +} + +void CSSTransition::SetEffectFromStyle(dom::AnimationEffect* aEffect) { + Animation::SetEffectNoUpdate(aEffect); + + // Initialize transition property and to value. + // + // Typically this should only be called with a KeyframeEffect representing + // a simple transition, but just to be sure we check the effect has the + // expected shape first. + const KeyframeEffect* keyframeEffect = aEffect->AsKeyframeEffect(); + if (MOZ_LIKELY(keyframeEffect && keyframeEffect->Properties().Length() == 1 && + keyframeEffect->Properties()[0].mSegments.Length() == 1)) { + mTransitionProperty = keyframeEffect->Properties()[0].mProperty; + mTransitionToValue = keyframeEffect->Properties()[0].mSegments[0].mToValue; + } else { + MOZ_ASSERT_UNREACHABLE("Transition effect has unexpected shape"); + } +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/animation/CSSTransition.h b/dom/animation/CSSTransition.h new file mode 100644 index 000000000000..b831378bf851 --- /dev/null +++ b/dom/animation/CSSTransition.h @@ -0,0 +1,245 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_CSSTransition_h +#define mozilla_dom_CSSTransition_h + +#include "mozilla/dom/Animation.h" +#include "mozilla/StyleAnimationValue.h" +#include "AnimationCommon.h" + +class nsIGlobalObject; + +namespace mozilla { +namespace dom { + +class CSSTransition final : public Animation { + public: + explicit CSSTransition(nsIGlobalObject* aGlobal) + : dom::Animation(aGlobal), + mPreviousTransitionPhase(TransitionPhase::Idle), + mNeedsNewAnimationIndexWhenRun(false), + mTransitionProperty(eCSSProperty_UNKNOWN) {} + + JSObject* WrapObject(JSContext* aCx, + JS::Handle aGivenProto) override; + + CSSTransition* AsCSSTransition() override { return this; } + const CSSTransition* AsCSSTransition() const override { return this; } + + // CSSTransition interface + void GetTransitionProperty(nsString& aRetVal) const; + + // Animation interface overrides + AnimationPlayState PlayStateFromJS() const override; + bool PendingFromJS() const override; + void PlayFromJS(ErrorResult& aRv) override; + + // A variant of Play() that avoids posting style updates since this method + // is expected to be called whilst already updating style. + void PlayFromStyle() { + ErrorResult rv; + PlayNoUpdate(rv, Animation::LimitBehavior::Continue); + // play() should not throw when LimitBehavior is Continue + MOZ_ASSERT(!rv.Failed(), "Unexpected exception playing transition"); + } + + void CancelFromStyle(PostRestyleMode aPostRestyle) { + // The animation index to use for compositing will be established when + // this transition next transitions out of the idle state but we still + // update it now so that the sort order of this transition remains + // defined until that moment. + // + // See longer explanation in CSSAnimation::CancelFromStyle. + mAnimationIndex = sNextAnimationIndex++; + mNeedsNewAnimationIndexWhenRun = true; + + Animation::Cancel(aPostRestyle); + + // It is important we do this *after* calling Cancel(). + // This is because Cancel() will end up posting a restyle and + // that restyle should target the *transitions* level of the cascade. + // However, once we clear the owning element, CascadeLevel() will begin + // returning CascadeLevel::Animations. + mOwningElement = OwningElementRef(); + } + + void SetEffectFromStyle(AnimationEffect* aEffect); + + void Tick() override; + + nsCSSPropertyID TransitionProperty() const; + AnimationValue ToValue() const; + + bool HasLowerCompositeOrderThan(const CSSTransition& aOther) const; + EffectCompositor::CascadeLevel CascadeLevel() const override { + return IsTiedToMarkup() ? EffectCompositor::CascadeLevel::Transitions + : EffectCompositor::CascadeLevel::Animations; + } + + void SetCreationSequence(uint64_t aIndex) { + MOZ_ASSERT(IsTiedToMarkup()); + mAnimationIndex = aIndex; + } + + // Sets the owning element which is used for determining the composite + // oder of CSSTransition objects generated from CSS markup. + // + // @see mOwningElement + void SetOwningElement(const OwningElementRef& aElement) { + mOwningElement = aElement; + } + // True for transitions that are generated from CSS markup and continue to + // reflect changes to that markup. + bool IsTiedToMarkup() const { return mOwningElement.IsSet(); } + + // Return the animation current time based on a given TimeStamp, a given + // start time and a given playbackRate on a given timeline. This is useful + // when we estimate the current animated value running on the compositor + // because the animation on the compositor may be running ahead while + // main-thread is busy. + static Nullable GetCurrentTimeAt( + const AnimationTimeline& aTimeline, const TimeStamp& aBaseTime, + const TimeDuration& aStartTime, double aPlaybackRate); + + void MaybeQueueCancelEvent(const StickyTimeDuration& aActiveTime) override { + QueueEvents(aActiveTime); + } + + // Compute the portion of the *value* space that we should be through + // at the current time. (The input to the transition timing function + // has time units, the output has value units.) + double CurrentValuePortion() const; + + const AnimationValue& StartForReversingTest() const { + return mStartForReversingTest; + } + double ReversePortion() const { return mReversePortion; } + + void SetReverseParameters(AnimationValue&& aStartForReversingTest, + double aReversePortion) { + mStartForReversingTest = std::move(aStartForReversingTest); + mReversePortion = aReversePortion; + } + + struct ReplacedTransitionProperties { + TimeDuration mStartTime; + double mPlaybackRate; + TimingParams mTiming; + Maybe mTimingFunction; + AnimationValue mFromValue, mToValue; + }; + void SetReplacedTransition( + ReplacedTransitionProperties&& aReplacedTransition) { + mReplacedTransition.emplace(std::move(aReplacedTransition)); + } + + // For a new transition interrupting an existing transition on the + // compositor, update the start value to match the value of the replaced + // transitions at the current time. + void UpdateStartValueFromReplacedTransition(); + + protected: + virtual ~CSSTransition() { + MOZ_ASSERT(!mOwningElement.IsSet(), + "Owning element should be cleared " + "before a CSS transition is destroyed"); + } + + // Animation overrides + void UpdateTiming(SeekFlag aSeekFlag, + SyncNotifyFlag aSyncNotifyFlag) override; + + void QueueEvents(const StickyTimeDuration& activeTime = StickyTimeDuration()); + + enum class TransitionPhase; + + // The (pseudo-)element whose computed transition-property refers to this + // transition (if any). + // + // This is used for determining the relative composite order of transitions + // generated from CSS markup. + // + // Typically this will be the same as the target element of the keyframe + // effect associated with this transition. However, it can differ in the + // following circumstances: + // + // a) If script removes or replaces the effect of this transition, + // b) If this transition is cancelled (e.g. by updating the + // transition-property or removing the owning element from the document), + // c) If this object is generated from script using the CSSTransition + // constructor. + // + // For (b) and (c) the owning element will return !IsSet(). + OwningElementRef mOwningElement; + + // The 'transition phase' used to determine which transition events need + // to be queued on this tick. + // See: https://drafts.csswg.org/css-transitions-2/#transition-phase + enum class TransitionPhase { + Idle = static_cast(ComputedTiming::AnimationPhase::Idle), + Before = static_cast(ComputedTiming::AnimationPhase::Before), + Active = static_cast(ComputedTiming::AnimationPhase::Active), + After = static_cast(ComputedTiming::AnimationPhase::After), + Pending + }; + TransitionPhase mPreviousTransitionPhase; + + // When true, indicates that when this transition next leaves the idle state, + // its animation index should be updated. + bool mNeedsNewAnimationIndexWhenRun; + + // Store the transition property and to-value here since we need that + // information in order to determine if there is an existing transition + // for a given style change. We can't store that information on the + // effect however since it can be replaced using the Web Animations API. + nsCSSPropertyID mTransitionProperty; + AnimationValue mTransitionToValue; + + // This is the start value to be used for a check for whether a + // transition is being reversed. Normally the same as + // mEffect->mProperties[0].mSegments[0].mFromValue, except when this + // transition started as the reversal of another in-progress transition + // or when the effect has been mutated using the Web Animations API. + // + // Needed so we can handle two reverses in a row. + AnimationValue mStartForReversingTest; + + // Likewise, the portion (in value space) of the "full" reversed + // transition that we're actually covering. For example, if a :hover + // effect has a transition that moves the element 10px to the right + // (by changing 'left' from 0px to 10px), and the mouse moves in to + // the element (starting the transition) but then moves out after the + // transition has advanced 4px, the second transition (from 10px/4px + // to 0px) will have mReversePortion of 0.4. (If the mouse then moves + // in again when the transition is back to 2px, the mReversePortion + // for the third transition (from 0px/2px to 10px) will be 0.8. + double mReversePortion = 1.0; + + Maybe mReplacedTransition; +}; + +} // namespace dom + +template <> +struct AnimationTypeTraits { + static nsAtom* ElementPropertyAtom() { + return nsGkAtoms::transitionsProperty; + } + static nsAtom* BeforePropertyAtom() { + return nsGkAtoms::transitionsOfBeforeProperty; + } + static nsAtom* AfterPropertyAtom() { + return nsGkAtoms::transitionsOfAfterProperty; + } + static nsAtom* MarkerPropertyAtom() { + return nsGkAtoms::transitionsOfMarkerProperty; + } +}; + +} // namespace mozilla + +#endif // mozilla_dom_CSSTransition_h diff --git a/dom/animation/moz.build b/dom/animation/moz.build index 8e3d5ba55cba..d4bf6f1636be 100644 --- a/dom/animation/moz.build +++ b/dom/animation/moz.build @@ -15,6 +15,7 @@ EXPORTS.mozilla.dom += [ 'AnimationEffect.h', 'AnimationTimeline.h', 'CSSPseudoElement.h', + 'CSSTransition.h', 'DocumentTimeline.h', 'KeyframeEffect.h', ] @@ -48,6 +49,7 @@ UNIFIED_SOURCES += [ 'AnimationUtils.cpp', 'ComputedTimingFunction.cpp', 'CSSPseudoElement.cpp', + 'CSSTransition.cpp', 'DocumentTimeline.cpp', 'EffectCompositor.cpp', 'EffectSet.cpp', diff --git a/layout/style/AnimationCollection.cpp b/layout/style/AnimationCollection.cpp index a5669d039e12..ca1b44ba7c12 100644 --- a/layout/style/AnimationCollection.cpp +++ b/layout/style/AnimationCollection.cpp @@ -7,9 +7,9 @@ #include "mozilla/AnimationCollection.h" #include "mozilla/RestyleManager.h" -#include "nsAnimationManager.h" // For dom::CSSAnimation -#include "nsDOMMutationObserver.h" // For nsAutoAnimationMutationBatch -#include "nsTransitionManager.h" // For dom::CSSTransition +#include "nsAnimationManager.h" // For dom::CSSAnimation +#include "nsDOMMutationObserver.h" // For nsAutoAnimationMutationBatch +#include "mozilla/dom/CSSTransition.h" // For dom::CSSTransition namespace mozilla { diff --git a/layout/style/nsTransitionManager.cpp b/layout/style/nsTransitionManager.cpp index 01b432594e86..775c768ab496 100644 --- a/layout/style/nsTransitionManager.cpp +++ b/layout/style/nsTransitionManager.cpp @@ -8,18 +8,12 @@ #include "nsTransitionManager.h" #include "mozilla/dom/Document.h" -#include "mozilla/dom/KeyframeEffectBinding.h" #include "nsAnimationManager.h" -#include "mozilla/dom/CSSTransitionBinding.h" #include "nsIContent.h" #include "mozilla/ComputedStyle.h" #include "mozilla/MemoryReporting.h" -#include "mozilla/TimeStamp.h" -#include "nsRefreshDriver.h" #include "nsCSSPropertyIDSet.h" -#include "mozilla/AnimationEventDispatcher.h" -#include "mozilla/EffectCompositor.h" #include "mozilla/EffectSet.h" #include "mozilla/EventDispatcher.h" #include "mozilla/ServoBindings.h" @@ -35,342 +29,15 @@ #include "nsRFPService.h" #include "nsStyleChangeList.h" #include "mozilla/RestyleManager.h" -#include "nsDOMMutationObserver.h" -using mozilla::TimeDuration; -using mozilla::TimeStamp; using mozilla::dom::Animation; -using mozilla::dom::AnimationPlayState; using mozilla::dom::CSSTransition; using mozilla::dom::DocumentTimeline; using mozilla::dom::KeyframeEffect; -using mozilla::dom::Nullable; using namespace mozilla; using namespace mozilla::css; -////////////////////////// CSSTransition //////////////////////////// - -JSObject* CSSTransition::WrapObject(JSContext* aCx, - JS::Handle aGivenProto) { - return dom::CSSTransition_Binding::Wrap(aCx, this, aGivenProto); -} - -void CSSTransition::GetTransitionProperty(nsString& aRetVal) const { - MOZ_ASSERT(eCSSProperty_UNKNOWN != mTransitionProperty, - "Transition Property should be initialized"); - aRetVal = - NS_ConvertUTF8toUTF16(nsCSSProps::GetStringValue(mTransitionProperty)); -} - -AnimationPlayState CSSTransition::PlayStateFromJS() const { - FlushUnanimatedStyle(); - return Animation::PlayStateFromJS(); -} - -bool CSSTransition::PendingFromJS() const { - // Transitions don't become pending again after they start running but, if - // while the transition is still pending, style is updated in such a way - // that the transition will be canceled, we need to report false here. - // Hence we need to flush, but only when we're pending. - if (Pending()) { - FlushUnanimatedStyle(); - } - return Animation::PendingFromJS(); -} - -void CSSTransition::PlayFromJS(ErrorResult& aRv) { - FlushUnanimatedStyle(); - Animation::PlayFromJS(aRv); -} - -void CSSTransition::UpdateTiming(SeekFlag aSeekFlag, - SyncNotifyFlag aSyncNotifyFlag) { - if (mNeedsNewAnimationIndexWhenRun && - PlayState() != AnimationPlayState::Idle) { - mAnimationIndex = sNextAnimationIndex++; - mNeedsNewAnimationIndexWhenRun = false; - } - - Animation::UpdateTiming(aSeekFlag, aSyncNotifyFlag); -} - -void CSSTransition::QueueEvents(const StickyTimeDuration& aActiveTime) { - if (!mOwningElement.IsSet()) { - return; - } - - nsPresContext* presContext = mOwningElement.GetPresContext(); - if (!presContext) { - return; - } - - static constexpr StickyTimeDuration zeroDuration = StickyTimeDuration(); - - TransitionPhase currentPhase; - StickyTimeDuration intervalStartTime; - StickyTimeDuration intervalEndTime; - - if (!mEffect) { - currentPhase = GetAnimationPhaseWithoutEffect(*this); - } else { - ComputedTiming computedTiming = mEffect->GetComputedTiming(); - - currentPhase = static_cast(computedTiming.mPhase); - intervalStartTime = IntervalStartTime(computedTiming.mActiveDuration); - intervalEndTime = IntervalEndTime(computedTiming.mActiveDuration); - } - - if (mPendingState != PendingState::NotPending && - (mPreviousTransitionPhase == TransitionPhase::Idle || - mPreviousTransitionPhase == TransitionPhase::Pending)) { - currentPhase = TransitionPhase::Pending; - } - - if (currentPhase == mPreviousTransitionPhase) { - return; - } - - // TimeStamps to use for ordering the events when they are dispatched. We - // use a TimeStamp so we can compare events produced by different elements, - // perhaps even with different timelines. - // The zero timestamp is for transitionrun events where we ignore the delay - // for the purpose of ordering events. - TimeStamp zeroTimeStamp = AnimationTimeToTimeStamp(zeroDuration); - TimeStamp startTimeStamp = ElapsedTimeToTimeStamp(intervalStartTime); - TimeStamp endTimeStamp = ElapsedTimeToTimeStamp(intervalEndTime); - - AutoTArray events; - - auto appendTransitionEvent = [&](EventMessage aMessage, - const StickyTimeDuration& aElapsedTime, - const TimeStamp& aScheduledEventTimeStamp) { - double elapsedTime = aElapsedTime.ToSeconds(); - if (aMessage == eTransitionCancel) { - // 0 is an inappropriate value for this callsite. What we need to do is - // use a single random value for all increasing times reportable. - // That is to say, whenever elapsedTime goes negative (because an - // animation restarts, something rewinds the animation, or otherwise) - // a new random value for the mix-in must be generated. - elapsedTime = - nsRFPService::ReduceTimePrecisionAsSecsRFPOnly(elapsedTime, 0); - } - events.AppendElement(AnimationEventInfo( - TransitionProperty(), mOwningElement.Target(), aMessage, elapsedTime, - aScheduledEventTimeStamp, this)); - }; - - // Handle cancel events first - if ((mPreviousTransitionPhase != TransitionPhase::Idle && - mPreviousTransitionPhase != TransitionPhase::After) && - currentPhase == TransitionPhase::Idle) { - appendTransitionEvent(eTransitionCancel, aActiveTime, - GetTimelineCurrentTimeAsTimeStamp()); - } - - // All other events - switch (mPreviousTransitionPhase) { - case TransitionPhase::Idle: - if (currentPhase == TransitionPhase::Pending || - currentPhase == TransitionPhase::Before) { - appendTransitionEvent(eTransitionRun, intervalStartTime, zeroTimeStamp); - } else if (currentPhase == TransitionPhase::Active) { - appendTransitionEvent(eTransitionRun, intervalStartTime, zeroTimeStamp); - appendTransitionEvent(eTransitionStart, intervalStartTime, - startTimeStamp); - } else if (currentPhase == TransitionPhase::After) { - appendTransitionEvent(eTransitionRun, intervalStartTime, zeroTimeStamp); - appendTransitionEvent(eTransitionStart, intervalStartTime, - startTimeStamp); - appendTransitionEvent(eTransitionEnd, intervalEndTime, endTimeStamp); - } - break; - - case TransitionPhase::Pending: - case TransitionPhase::Before: - if (currentPhase == TransitionPhase::Active) { - appendTransitionEvent(eTransitionStart, intervalStartTime, - startTimeStamp); - } else if (currentPhase == TransitionPhase::After) { - appendTransitionEvent(eTransitionStart, intervalStartTime, - startTimeStamp); - appendTransitionEvent(eTransitionEnd, intervalEndTime, endTimeStamp); - } - break; - - case TransitionPhase::Active: - if (currentPhase == TransitionPhase::After) { - appendTransitionEvent(eTransitionEnd, intervalEndTime, endTimeStamp); - } else if (currentPhase == TransitionPhase::Before) { - appendTransitionEvent(eTransitionEnd, intervalStartTime, - startTimeStamp); - } - break; - - case TransitionPhase::After: - if (currentPhase == TransitionPhase::Active) { - appendTransitionEvent(eTransitionStart, intervalEndTime, - startTimeStamp); - } else if (currentPhase == TransitionPhase::Before) { - appendTransitionEvent(eTransitionStart, intervalEndTime, - startTimeStamp); - appendTransitionEvent(eTransitionEnd, intervalStartTime, endTimeStamp); - } - break; - } - mPreviousTransitionPhase = currentPhase; - - if (!events.IsEmpty()) { - presContext->AnimationEventDispatcher()->QueueEvents(std::move(events)); - } -} - -void CSSTransition::Tick() { - Animation::Tick(); - QueueEvents(); -} - -nsCSSPropertyID CSSTransition::TransitionProperty() const { - MOZ_ASSERT(eCSSProperty_UNKNOWN != mTransitionProperty, - "Transition property should be initialized"); - return mTransitionProperty; -} - -AnimationValue CSSTransition::ToValue() const { - MOZ_ASSERT(!mTransitionToValue.IsNull(), - "Transition ToValue should be initialized"); - return mTransitionToValue; -} - -bool CSSTransition::HasLowerCompositeOrderThan( - const CSSTransition& aOther) const { - MOZ_ASSERT(IsTiedToMarkup() && aOther.IsTiedToMarkup(), - "Should only be called for CSS transitions that are sorted " - "as CSS transitions (i.e. tied to CSS markup)"); - - // 0. Object-equality case - if (&aOther == this) { - return false; - } - - // 1. Sort by document order - if (!mOwningElement.Equals(aOther.mOwningElement)) { - return mOwningElement.LessThan( - const_cast(this)->CachedChildIndexRef(), - aOther.mOwningElement, - const_cast(&aOther)->CachedChildIndexRef()); - } - - // 2. (Same element and pseudo): Sort by transition generation - if (mAnimationIndex != aOther.mAnimationIndex) { - return mAnimationIndex < aOther.mAnimationIndex; - } - - // 3. (Same transition generation): Sort by transition property - return nsCSSProps::GetStringValue(TransitionProperty()) < - nsCSSProps::GetStringValue(aOther.TransitionProperty()); -} - -/* static */ -Nullable CSSTransition::GetCurrentTimeAt( - const AnimationTimeline& aTimeline, const TimeStamp& aBaseTime, - const TimeDuration& aStartTime, double aPlaybackRate) { - Nullable result; - - Nullable timelineTime = aTimeline.ToTimelineTime(aBaseTime); - if (!timelineTime.IsNull()) { - result.SetValue( - (timelineTime.Value() - aStartTime).MultDouble(aPlaybackRate)); - } - - return result; -} - -double CSSTransition::CurrentValuePortion() const { - if (!GetEffect()) { - return 0.0; - } - - // Transitions use a fill mode of 'backwards' so GetComputedTiming will - // never return a null time progress due to being *before* the animation - // interval. However, it might be possible that we're behind on flushing - // causing us to get called *after* the animation interval. So, just in - // case, we override the fill mode to 'both' to ensure the progress - // is never null. - TimingParams timingToUse = GetEffect()->SpecifiedTiming(); - timingToUse.SetFill(dom::FillMode::Both); - ComputedTiming computedTiming = GetEffect()->GetComputedTiming(&timingToUse); - - if (computedTiming.mProgress.IsNull()) { - return 0.0; - } - - // 'transition-timing-function' corresponds to the effect timing while - // the transition keyframes have a linear timing function so we can ignore - // them for the purposes of calculating the value portion. - return computedTiming.mProgress.Value(); -} - -void CSSTransition::UpdateStartValueFromReplacedTransition() { - MOZ_ASSERT(mEffect && mEffect->AsKeyframeEffect() && - mEffect->AsKeyframeEffect()->HasAnimationOfPropertySet( - nsCSSPropertyIDSet::CompositorAnimatables()), - "Should be called for compositor-runnable transitions"); - - MOZ_ASSERT(mTimeline, - "Should have a timeline if we are replacing transition start " - "values"); - - if (!mReplacedTransition) { - return; - } - - ComputedTiming computedTiming = AnimationEffect::GetComputedTimingAt( - CSSTransition::GetCurrentTimeAt(*mTimeline, TimeStamp::Now(), - mReplacedTransition->mStartTime, - mReplacedTransition->mPlaybackRate), - mReplacedTransition->mTiming, mReplacedTransition->mPlaybackRate); - - if (!computedTiming.mProgress.IsNull()) { - double valuePosition = ComputedTimingFunction::GetPortion( - mReplacedTransition->mTimingFunction, computedTiming.mProgress.Value(), - computedTiming.mBeforeFlag); - - const AnimationValue& replacedFrom = mReplacedTransition->mFromValue; - const AnimationValue& replacedTo = mReplacedTransition->mToValue; - AnimationValue startValue; - startValue.mServo = - Servo_AnimationValues_Interpolate(replacedFrom.mServo, - replacedTo.mServo, valuePosition) - .Consume(); - - mEffect->AsKeyframeEffect()->ReplaceTransitionStartValue( - std::move(startValue)); - } - - mReplacedTransition.reset(); -} - -void CSSTransition::SetEffectFromStyle(dom::AnimationEffect* aEffect) { - Animation::SetEffectNoUpdate(aEffect); - - // Initialize transition property and to value. - // - // Typically this should only be called with a KeyframeEffect representing - // a simple transition, but just to be sure we check the effect has the - // expected shape first. - const KeyframeEffect* keyframeEffect = aEffect->AsKeyframeEffect(); - if (MOZ_LIKELY(keyframeEffect && keyframeEffect->Properties().Length() == 1 && - keyframeEffect->Properties()[0].mSegments.Length() == 1)) { - mTransitionProperty = keyframeEffect->Properties()[0].mProperty; - mTransitionToValue = keyframeEffect->Properties()[0].mSegments[0].mToValue; - } else { - MOZ_ASSERT_UNREACHABLE("Transition effect has unexpected shape"); - } -} - -////////////////////////// nsTransitionManager //////////////////////////// - static inline bool ExtractNonDiscreteComputedValue( nsCSSPropertyID aProperty, const ComputedStyle& aComputedStyle, AnimationValue& aAnimationValue) { diff --git a/layout/style/nsTransitionManager.h b/layout/style/nsTransitionManager.h index 915a0d540aa2..180bd6b8c2dc 100644 --- a/layout/style/nsTransitionManager.h +++ b/layout/style/nsTransitionManager.h @@ -9,255 +9,16 @@ #ifndef nsTransitionManager_h_ #define nsTransitionManager_h_ -#include "mozilla/ComputedTiming.h" -#include "mozilla/EffectCompositor.h" // For EffectCompositor::CascadeLevel -#include "mozilla/dom/Animation.h" -#include "mozilla/dom/KeyframeEffect.h" +#include "mozilla/dom/CSSTransition.h" #include "AnimationCommon.h" #include "nsISupportsImpl.h" -class nsIGlobalObject; class nsPresContext; class nsCSSPropertyIDSet; namespace mozilla { class ComputedStyle; enum class PseudoStyleType : uint8_t; -struct Keyframe; -struct StyleTransition; -} // namespace mozilla - -/***************************************************************************** - * Per-Element data * - *****************************************************************************/ - -namespace mozilla { -namespace dom { - -class CSSTransition final : public Animation { - public: - explicit CSSTransition(nsIGlobalObject* aGlobal) - : dom::Animation(aGlobal), - mPreviousTransitionPhase(TransitionPhase::Idle), - mNeedsNewAnimationIndexWhenRun(false), - mTransitionProperty(eCSSProperty_UNKNOWN) {} - - JSObject* WrapObject(JSContext* aCx, - JS::Handle aGivenProto) override; - - CSSTransition* AsCSSTransition() override { return this; } - const CSSTransition* AsCSSTransition() const override { return this; } - - // CSSTransition interface - void GetTransitionProperty(nsString& aRetVal) const; - - // Animation interface overrides - AnimationPlayState PlayStateFromJS() const override; - bool PendingFromJS() const override; - void PlayFromJS(ErrorResult& aRv) override; - - // A variant of Play() that avoids posting style updates since this method - // is expected to be called whilst already updating style. - void PlayFromStyle() { - ErrorResult rv; - PlayNoUpdate(rv, Animation::LimitBehavior::Continue); - // play() should not throw when LimitBehavior is Continue - MOZ_ASSERT(!rv.Failed(), "Unexpected exception playing transition"); - } - - void CancelFromStyle(PostRestyleMode aPostRestyle) { - // The animation index to use for compositing will be established when - // this transition next transitions out of the idle state but we still - // update it now so that the sort order of this transition remains - // defined until that moment. - // - // See longer explanation in CSSAnimation::CancelFromStyle. - mAnimationIndex = sNextAnimationIndex++; - mNeedsNewAnimationIndexWhenRun = true; - - Animation::Cancel(aPostRestyle); - - // It is important we do this *after* calling Cancel(). - // This is because Cancel() will end up posting a restyle and - // that restyle should target the *transitions* level of the cascade. - // However, once we clear the owning element, CascadeLevel() will begin - // returning CascadeLevel::Animations. - mOwningElement = OwningElementRef(); - } - - void SetEffectFromStyle(AnimationEffect* aEffect); - - void Tick() override; - - nsCSSPropertyID TransitionProperty() const; - AnimationValue ToValue() const; - - bool HasLowerCompositeOrderThan(const CSSTransition& aOther) const; - EffectCompositor::CascadeLevel CascadeLevel() const override { - return IsTiedToMarkup() ? EffectCompositor::CascadeLevel::Transitions - : EffectCompositor::CascadeLevel::Animations; - } - - void SetCreationSequence(uint64_t aIndex) { - MOZ_ASSERT(IsTiedToMarkup()); - mAnimationIndex = aIndex; - } - - // Sets the owning element which is used for determining the composite - // oder of CSSTransition objects generated from CSS markup. - // - // @see mOwningElement - void SetOwningElement(const OwningElementRef& aElement) { - mOwningElement = aElement; - } - // True for transitions that are generated from CSS markup and continue to - // reflect changes to that markup. - bool IsTiedToMarkup() const { return mOwningElement.IsSet(); } - - // Return the animation current time based on a given TimeStamp, a given - // start time and a given playbackRate on a given timeline. This is useful - // when we estimate the current animated value running on the compositor - // because the animation on the compositor may be running ahead while - // main-thread is busy. - static Nullable GetCurrentTimeAt( - const AnimationTimeline& aTimeline, const TimeStamp& aBaseTime, - const TimeDuration& aStartTime, double aPlaybackRate); - - void MaybeQueueCancelEvent(const StickyTimeDuration& aActiveTime) override { - QueueEvents(aActiveTime); - } - - // Compute the portion of the *value* space that we should be through - // at the current time. (The input to the transition timing function - // has time units, the output has value units.) - double CurrentValuePortion() const; - - const AnimationValue& StartForReversingTest() const { - return mStartForReversingTest; - } - double ReversePortion() const { return mReversePortion; } - - void SetReverseParameters(AnimationValue&& aStartForReversingTest, - double aReversePortion) { - mStartForReversingTest = std::move(aStartForReversingTest); - mReversePortion = aReversePortion; - } - - struct ReplacedTransitionProperties { - TimeDuration mStartTime; - double mPlaybackRate; - TimingParams mTiming; - Maybe mTimingFunction; - AnimationValue mFromValue, mToValue; - }; - void SetReplacedTransition( - ReplacedTransitionProperties&& aReplacedTransition) { - mReplacedTransition.emplace(std::move(aReplacedTransition)); - } - - // For a new transition interrupting an existing transition on the - // compositor, update the start value to match the value of the replaced - // transitions at the current time. - void UpdateStartValueFromReplacedTransition(); - - protected: - virtual ~CSSTransition() { - MOZ_ASSERT(!mOwningElement.IsSet(), - "Owning element should be cleared " - "before a CSS transition is destroyed"); - } - - // Animation overrides - void UpdateTiming(SeekFlag aSeekFlag, - SyncNotifyFlag aSyncNotifyFlag) override; - - void QueueEvents(const StickyTimeDuration& activeTime = StickyTimeDuration()); - - enum class TransitionPhase; - - // The (pseudo-)element whose computed transition-property refers to this - // transition (if any). - // - // This is used for determining the relative composite order of transitions - // generated from CSS markup. - // - // Typically this will be the same as the target element of the keyframe - // effect associated with this transition. However, it can differ in the - // following circumstances: - // - // a) If script removes or replaces the effect of this transition, - // b) If this transition is cancelled (e.g. by updating the - // transition-property or removing the owning element from the document), - // c) If this object is generated from script using the CSSTransition - // constructor. - // - // For (b) and (c) the owning element will return !IsSet(). - OwningElementRef mOwningElement; - - // The 'transition phase' used to determine which transition events need - // to be queued on this tick. - // See: https://drafts.csswg.org/css-transitions-2/#transition-phase - enum class TransitionPhase { - Idle = static_cast(ComputedTiming::AnimationPhase::Idle), - Before = static_cast(ComputedTiming::AnimationPhase::Before), - Active = static_cast(ComputedTiming::AnimationPhase::Active), - After = static_cast(ComputedTiming::AnimationPhase::After), - Pending - }; - TransitionPhase mPreviousTransitionPhase; - - // When true, indicates that when this transition next leaves the idle state, - // its animation index should be updated. - bool mNeedsNewAnimationIndexWhenRun; - - // Store the transition property and to-value here since we need that - // information in order to determine if there is an existing transition - // for a given style change. We can't store that information on the - // effect however since it can be replaced using the Web Animations API. - nsCSSPropertyID mTransitionProperty; - AnimationValue mTransitionToValue; - - // This is the start value to be used for a check for whether a - // transition is being reversed. Normally the same as - // mEffect->mProperties[0].mSegments[0].mFromValue, except when this - // transition started as the reversal of another in-progress transition - // or when the effect has been mutated using the Web Animations API. - // - // Needed so we can handle two reverses in a row. - AnimationValue mStartForReversingTest; - - // Likewise, the portion (in value space) of the "full" reversed - // transition that we're actually covering. For example, if a :hover - // effect has a transition that moves the element 10px to the right - // (by changing 'left' from 0px to 10px), and the mouse moves in to - // the element (starting the transition) but then moves out after the - // transition has advanced 4px, the second transition (from 10px/4px - // to 0px) will have mReversePortion of 0.4. (If the mouse then moves - // in again when the transition is back to 2px, the mReversePortion - // for the third transition (from 0px/2px to 10px) will be 0.8. - double mReversePortion = 1.0; - - Maybe mReplacedTransition; -}; - -} // namespace dom - -template <> -struct AnimationTypeTraits { - static nsAtom* ElementPropertyAtom() { - return nsGkAtoms::transitionsProperty; - } - static nsAtom* BeforePropertyAtom() { - return nsGkAtoms::transitionsOfBeforeProperty; - } - static nsAtom* AfterPropertyAtom() { - return nsGkAtoms::transitionsOfAfterProperty; - } - static nsAtom* MarkerPropertyAtom() { - return nsGkAtoms::transitionsOfMarkerProperty; - } -}; - } // namespace mozilla class nsTransitionManager final