summaryrefslogtreecommitdiffstats
path: root/layout/style/nsTransitionManager.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'layout/style/nsTransitionManager.cpp')
-rw-r--r--layout/style/nsTransitionManager.cpp1047
1 files changed, 1047 insertions, 0 deletions
diff --git a/layout/style/nsTransitionManager.cpp b/layout/style/nsTransitionManager.cpp
new file mode 100644
index 000000000..4a1a5b7ad
--- /dev/null
+++ b/layout/style/nsTransitionManager.cpp
@@ -0,0 +1,1047 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set shiftwidth=2 tabstop=8 autoindent cindent expandtab: */
+/* 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/. */
+
+/* Code to start and animate CSS transitions. */
+
+#include "nsTransitionManager.h"
+#include "nsAnimationManager.h"
+#include "mozilla/dom/CSSTransitionBinding.h"
+
+#include "nsIContent.h"
+#include "nsStyleContext.h"
+#include "mozilla/MemoryReporting.h"
+#include "mozilla/TimeStamp.h"
+#include "nsRefreshDriver.h"
+#include "nsRuleProcessorData.h"
+#include "nsRuleWalker.h"
+#include "nsCSSPropertyIDSet.h"
+#include "mozilla/EffectCompositor.h"
+#include "mozilla/EffectSet.h"
+#include "mozilla/EventDispatcher.h"
+#include "mozilla/StyleAnimationValue.h"
+#include "mozilla/dom/DocumentTimeline.h"
+#include "mozilla/dom/Element.h"
+#include "nsIFrame.h"
+#include "Layers.h"
+#include "FrameLayerBuilder.h"
+#include "nsCSSProps.h"
+#include "nsCSSPseudoElements.h"
+#include "nsDisplayList.h"
+#include "nsStyleChangeList.h"
+#include "nsStyleSet.h"
+#include "mozilla/RestyleManagerHandle.h"
+#include "mozilla/RestyleManagerHandleInlines.h"
+#include "nsDOMMutationObserver.h"
+
+using mozilla::TimeStamp;
+using mozilla::TimeDuration;
+using mozilla::dom::Animation;
+using mozilla::dom::AnimationPlayState;
+using mozilla::dom::CSSTransition;
+using mozilla::dom::KeyframeEffectReadOnly;
+
+using namespace mozilla;
+using namespace mozilla::css;
+
+typedef mozilla::ComputedTiming::AnimationPhase AnimationPhase;
+
+namespace {
+struct TransitionEventParams {
+ EventMessage mMessage;
+ StickyTimeDuration mElapsedTime;
+ TimeStamp mTimeStamp;
+};
+} // anonymous namespace
+
+double
+ElementPropertyTransition::CurrentValuePortion() const
+{
+ MOZ_ASSERT(!GetLocalTime().IsNull(),
+ "Getting the value portion of an animation that's not being "
+ "sampled");
+
+ // 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 = SpecifiedTiming();
+ timingToUse.mFill = dom::FillMode::Both;
+ ComputedTiming computedTiming = GetComputedTiming(&timingToUse);
+
+ MOZ_ASSERT(!computedTiming.mProgress.IsNull(),
+ "Got a null progress for a fill mode of 'both'");
+ MOZ_ASSERT(mKeyframes.Length() == 2,
+ "Should have two animation keyframes for a transition");
+ return ComputedTimingFunction::GetPortion(mKeyframes[0].mTimingFunction,
+ computedTiming.mProgress.Value(),
+ computedTiming.mBeforeFlag);
+}
+
+void
+ElementPropertyTransition::UpdateStartValueFromReplacedTransition()
+{
+ if (!mReplacedTransition) {
+ return;
+ }
+ MOZ_ASSERT(nsCSSProps::PropHasFlags(TransitionProperty(),
+ CSS_PROPERTY_CAN_ANIMATE_ON_COMPOSITOR),
+ "The transition property should be able to be run on the "
+ "compositor");
+ MOZ_ASSERT(mTarget && mTarget->mElement->OwnerDoc(),
+ "We should have a valid document at this moment");
+
+ dom::DocumentTimeline* timeline = mTarget->mElement->OwnerDoc()->Timeline();
+ ComputedTiming computedTiming = GetComputedTimingAt(
+ dom::CSSTransition::GetCurrentTimeAt(*timeline,
+ 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);
+ StyleAnimationValue startValue;
+ if (StyleAnimationValue::Interpolate(mProperties[0].mProperty,
+ mReplacedTransition->mFromValue,
+ mReplacedTransition->mToValue,
+ valuePosition, startValue)) {
+ MOZ_ASSERT(mProperties.Length() == 1 &&
+ mProperties[0].mSegments.Length() == 1,
+ "The transition should have one property and one segment");
+ nsCSSValue cssValue;
+ DebugOnly<bool> uncomputeResult =
+ StyleAnimationValue::UncomputeValue(mProperties[0].mProperty,
+ startValue,
+ cssValue);
+ mProperties[0].mSegments[0].mFromValue = Move(startValue);
+ MOZ_ASSERT(uncomputeResult, "UncomputeValue should not fail");
+ MOZ_ASSERT(mKeyframes.Length() == 2,
+ "Transitions should have exactly two animation keyframes");
+ MOZ_ASSERT(mKeyframes[0].mPropertyValues.Length() == 1,
+ "Transitions should have exactly one property in their first "
+ "frame");
+ mKeyframes[0].mPropertyValues[0].mValue = cssValue;
+ }
+ }
+
+ mReplacedTransition.reset();
+}
+
+////////////////////////// CSSTransition ////////////////////////////
+
+JSObject*
+CSSTransition::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
+{
+ return dom::CSSTransitionBinding::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
+{
+ FlushStyle();
+ return Animation::PlayStateFromJS();
+}
+
+void
+CSSTransition::PlayFromJS(ErrorResult& aRv)
+{
+ FlushStyle();
+ 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()
+{
+ if (!mEffect ||
+ !mOwningElement.IsSet()) {
+ return;
+ }
+
+ dom::Element* owningElement;
+ CSSPseudoElementType owningPseudoType;
+ mOwningElement.GetElement(owningElement, owningPseudoType);
+ MOZ_ASSERT(owningElement, "Owning element should be set");
+
+ nsPresContext* presContext = mOwningElement.GetRenderedPresContext();
+ if (!presContext) {
+ return;
+ }
+
+ ComputedTiming computedTiming = mEffect->GetComputedTiming();
+ const StickyTimeDuration zeroDuration;
+ StickyTimeDuration intervalStartTime =
+ std::max(std::min(StickyTimeDuration(-mEffect->SpecifiedTiming().mDelay),
+ computedTiming.mActiveDuration), zeroDuration);
+ StickyTimeDuration intervalEndTime =
+ std::max(std::min((EffectEnd() - mEffect->SpecifiedTiming().mDelay),
+ computedTiming.mActiveDuration), zeroDuration);
+
+ // 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 startTimeStamp = ElapsedTimeToTimeStamp(intervalStartTime);
+ TimeStamp endTimeStamp = ElapsedTimeToTimeStamp(intervalEndTime);
+
+ TransitionPhase currentPhase;
+ if (mPendingState != PendingState::NotPending &&
+ (mPreviousTransitionPhase == TransitionPhase::Idle ||
+ mPreviousTransitionPhase == TransitionPhase::Pending))
+ {
+ currentPhase = TransitionPhase::Pending;
+ } else {
+ currentPhase = static_cast<TransitionPhase>(computedTiming.mPhase);
+ }
+
+ AutoTArray<TransitionEventParams, 3> events;
+ switch (mPreviousTransitionPhase) {
+ case TransitionPhase::Idle:
+ if (currentPhase == TransitionPhase::After) {
+ events.AppendElement(TransitionEventParams{ eTransitionEnd,
+ intervalEndTime,
+ endTimeStamp });
+ }
+ break;
+
+ case TransitionPhase::Pending:
+ case TransitionPhase::Before:
+ if (currentPhase == TransitionPhase::After) {
+ events.AppendElement(TransitionEventParams{ eTransitionEnd,
+ intervalEndTime,
+ endTimeStamp });
+ }
+ break;
+
+ case TransitionPhase::Active:
+ if (currentPhase == TransitionPhase::After) {
+ events.AppendElement(TransitionEventParams{ eTransitionEnd,
+ intervalEndTime,
+ endTimeStamp });
+ } else if (currentPhase == TransitionPhase::Before) {
+ events.AppendElement(TransitionEventParams{ eTransitionEnd,
+ intervalStartTime,
+ startTimeStamp });
+ }
+ break;
+
+ case TransitionPhase::After:
+ if (currentPhase == TransitionPhase::Before) {
+ events.AppendElement(TransitionEventParams{ eTransitionEnd,
+ intervalStartTime,
+ endTimeStamp });
+ }
+ break;
+ }
+ mPreviousTransitionPhase = currentPhase;
+
+ nsTransitionManager* manager = presContext->TransitionManager();
+ for (const TransitionEventParams& evt : events) {
+ manager->QueueEvent(TransitionEventInfo(owningElement, owningPseudoType,
+ evt.mMessage,
+ TransitionProperty(),
+ evt.mElapsedTime,
+ evt.mTimeStamp,
+ this));
+ }
+}
+
+void
+CSSTransition::Tick()
+{
+ Animation::Tick();
+ QueueEvents();
+}
+
+nsCSSPropertyID
+CSSTransition::TransitionProperty() const
+{
+ MOZ_ASSERT(eCSSProperty_UNKNOWN != mTransitionProperty,
+ "Transition property should be initialized");
+ return mTransitionProperty;
+}
+
+StyleAnimationValue
+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(aOther.mOwningElement);
+ }
+
+ // 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<TimeDuration>
+CSSTransition::GetCurrentTimeAt(const DocumentTimeline& aTimeline,
+ const TimeStamp& aBaseTime,
+ const TimeDuration& aStartTime,
+ double aPlaybackRate)
+{
+ Nullable<TimeDuration> result;
+
+ Nullable<TimeDuration> timelineTime = aTimeline.ToTimelineTime(aBaseTime);
+ if (!timelineTime.IsNull()) {
+ result.SetValue((timelineTime.Value() - aStartTime)
+ .MultDouble(aPlaybackRate));
+ }
+
+ return result;
+}
+
+void
+CSSTransition::SetEffectFromStyle(AnimationEffectReadOnly* aEffect)
+{
+ Animation::SetEffectNoUpdate(aEffect);
+
+ // Initialize transition property.
+ ElementPropertyTransition* pt = aEffect ? aEffect->AsTransition() : nullptr;
+ if (eCSSProperty_UNKNOWN == mTransitionProperty && pt) {
+ mTransitionProperty = pt->TransitionProperty();
+ mTransitionToValue = pt->ToValue();
+ }
+}
+
+////////////////////////// nsTransitionManager ////////////////////////////
+
+NS_IMPL_CYCLE_COLLECTION(nsTransitionManager, mEventDispatcher)
+
+NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(nsTransitionManager, AddRef)
+NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(nsTransitionManager, Release)
+
+static inline bool
+ExtractNonDiscreteComputedValue(nsCSSPropertyID aProperty,
+ nsStyleContext* aStyleContext,
+ StyleAnimationValue& aComputedValue)
+{
+ return (nsCSSProps::kAnimTypeTable[aProperty] != eStyleAnimType_Discrete ||
+ aProperty == eCSSProperty_visibility) &&
+ StyleAnimationValue::ExtractComputedValue(aProperty, aStyleContext,
+ aComputedValue);
+}
+
+void
+nsTransitionManager::StyleContextChanged(dom::Element *aElement,
+ nsStyleContext *aOldStyleContext,
+ RefPtr<nsStyleContext>* aNewStyleContext /* inout */)
+{
+ nsStyleContext* newStyleContext = *aNewStyleContext;
+
+ NS_PRECONDITION(aOldStyleContext->GetPseudo() == newStyleContext->GetPseudo(),
+ "pseudo type mismatch");
+
+ if (mInAnimationOnlyStyleUpdate) {
+ // If we're doing an animation-only style update, return, since the
+ // purpose of an animation-only style update is to update only the
+ // animation styles so that we don't consider style changes
+ // resulting from changes in the animation time for starting a
+ // transition.
+ return;
+ }
+
+ if (!mPresContext->IsDynamic()) {
+ // For print or print preview, ignore transitions.
+ return;
+ }
+
+ if (aOldStyleContext->HasPseudoElementData() !=
+ newStyleContext->HasPseudoElementData()) {
+ // If the old style context and new style context differ in terms of
+ // whether they're inside ::first-letter, ::first-line, or similar,
+ // bail. We can't hit this codepath for normal style changes
+ // involving moving frames around the boundaries of these
+ // pseudo-elements since we don't call StyleContextChanged from
+ // ReparentStyleContext. However, we can hit this codepath during
+ // the handling of transitions that start across reframes.
+ //
+ // While there isn't an easy *perfect* way to handle this case, err
+ // on the side of missing some transitions that we ought to have
+ // rather than having bogus transitions that we shouldn't.
+ //
+ // We could consider changing this handling, although it's worth
+ // thinking about whether the code below could do anything weird in
+ // this case.
+ return;
+ }
+
+ // NOTE: Things in this function (and ConsiderInitiatingTransition)
+ // should never call PeekStyleData because we don't preserve gotten
+ // structs across reframes.
+
+ // Return sooner (before the startedAny check below) for the most
+ // common case: no transitions specified or running.
+ const nsStyleDisplay *disp = newStyleContext->StyleDisplay();
+ CSSPseudoElementType pseudoType = newStyleContext->GetPseudoType();
+ if (pseudoType != CSSPseudoElementType::NotPseudo) {
+ if (pseudoType != CSSPseudoElementType::before &&
+ pseudoType != CSSPseudoElementType::after) {
+ return;
+ }
+
+ NS_ASSERTION((pseudoType == CSSPseudoElementType::before &&
+ aElement->IsGeneratedContentContainerForBefore()) ||
+ (pseudoType == CSSPseudoElementType::after &&
+ aElement->IsGeneratedContentContainerForAfter()),
+ "Unexpected aElement coming through");
+
+ // Else the element we want to use from now on is the element the
+ // :before or :after is attached to.
+ aElement = aElement->GetParent()->AsElement();
+ }
+
+ CSSTransitionCollection* collection =
+ CSSTransitionCollection::GetAnimationCollection(aElement, pseudoType);
+ if (!collection &&
+ disp->mTransitionPropertyCount == 1 &&
+ disp->mTransitions[0].GetCombinedDuration() <= 0.0f) {
+ return;
+ }
+
+ MOZ_ASSERT(mPresContext->RestyleManager()->IsGecko(),
+ "ServoRestyleManager should not use nsTransitionManager "
+ "for transitions");
+ if (collection &&
+ collection->mCheckGeneration ==
+ mPresContext->RestyleManager()->AsGecko()->GetAnimationGeneration()) {
+ // When we start a new transition, we immediately post a restyle.
+ // If the animation generation on the collection is current, that
+ // means *this* is that restyle, since we bump the animation
+ // generation on the restyle manager whenever there's a real style
+ // change (i.e., one where mInAnimationOnlyStyleUpdate isn't true,
+ // which causes us to return above). Thus we shouldn't do anything.
+ return;
+ }
+ if (newStyleContext->GetParent() &&
+ newStyleContext->GetParent()->HasPseudoElementData()) {
+ // Ignore transitions on things that inherit properties from
+ // pseudo-elements.
+ // FIXME (Bug 522599): Add tests for this.
+ return;
+ }
+
+ NS_WARNING_ASSERTION(
+ !mPresContext->EffectCompositor()->HasThrottledStyleUpdates(),
+ "throttled animations not up to date");
+
+ // Compute what the css-transitions spec calls the "after-change
+ // style", which is the new style without any data from transitions,
+ // but still inheriting from data that contains transitions that are
+ // not stopping or starting right now.
+ RefPtr<nsStyleContext> afterChangeStyle;
+ if (collection) {
+ MOZ_ASSERT(mPresContext->StyleSet()->IsGecko(),
+ "ServoStyleSets should not use nsTransitionManager "
+ "for transitions");
+ nsStyleSet* styleSet = mPresContext->StyleSet()->AsGecko();
+ afterChangeStyle =
+ styleSet->ResolveStyleWithoutAnimation(aElement, newStyleContext,
+ eRestyle_CSSTransitions);
+ } else {
+ afterChangeStyle = newStyleContext;
+ }
+
+ nsAutoAnimationMutationBatch mb(aElement->OwnerDoc());
+
+ DebugOnly<bool> startedAny = false;
+ // We don't have to update transitions if display:none, although we will
+ // cancel them after restyling.
+ if (!afterChangeStyle->IsInDisplayNoneSubtree()) {
+ startedAny = UpdateTransitions(disp, aElement, collection,
+ aOldStyleContext, afterChangeStyle);
+ }
+
+ MOZ_ASSERT(!startedAny || collection,
+ "must have element transitions if we started any transitions");
+
+ EffectCompositor::CascadeLevel cascadeLevel =
+ EffectCompositor::CascadeLevel::Transitions;
+
+ if (collection) {
+ collection->UpdateCheckGeneration(mPresContext);
+ mPresContext->EffectCompositor()->MaybeUpdateAnimationRule(aElement,
+ pseudoType,
+ cascadeLevel,
+ newStyleContext);
+ }
+
+ // We want to replace the new style context with the after-change style.
+ *aNewStyleContext = afterChangeStyle;
+ if (collection) {
+ // Since we have transition styles, we have to undo this replacement.
+ // The check of collection->mCheckGeneration against the restyle
+ // manager's GetAnimationGeneration() will ensure that we don't go
+ // through the rest of this function again when we do.
+ mPresContext->EffectCompositor()->PostRestyleForAnimation(aElement,
+ pseudoType,
+ cascadeLevel);
+ }
+}
+
+bool
+nsTransitionManager::UpdateTransitions(
+ const nsStyleDisplay* aDisp,
+ dom::Element* aElement,
+ CSSTransitionCollection*& aElementTransitions,
+ nsStyleContext* aOldStyleContext,
+ nsStyleContext* aNewStyleContext)
+{
+ MOZ_ASSERT(aDisp, "Null nsStyleDisplay");
+ MOZ_ASSERT(!aElementTransitions ||
+ aElementTransitions->mElement == aElement, "Element mismatch");
+
+ // Per http://lists.w3.org/Archives/Public/www-style/2009Aug/0109.html
+ // I'll consider only the transitions from the number of items in
+ // 'transition-property' on down, and later ones will override earlier
+ // ones (tracked using |whichStarted|).
+ bool startedAny = false;
+ nsCSSPropertyIDSet whichStarted;
+ for (uint32_t i = aDisp->mTransitionPropertyCount; i-- != 0; ) {
+ const StyleTransition& t = aDisp->mTransitions[i];
+ // Check the combined duration (combination of delay and duration)
+ // first, since it defaults to zero, which means we can ignore the
+ // transition.
+ if (t.GetCombinedDuration() > 0.0f) {
+ // We might have something to transition. See if any of the
+ // properties in question changed and are animatable.
+ // FIXME: Would be good to find a way to share code between this
+ // interpretation of transition-property and the one below.
+ nsCSSPropertyID property = t.GetProperty();
+ if (property == eCSSPropertyExtra_no_properties ||
+ property == eCSSPropertyExtra_variable ||
+ property == eCSSProperty_UNKNOWN) {
+ // Nothing to do, but need to exclude this from cases below.
+ } else if (property == eCSSPropertyExtra_all_properties) {
+ for (nsCSSPropertyID p = nsCSSPropertyID(0);
+ p < eCSSProperty_COUNT_no_shorthands;
+ p = nsCSSPropertyID(p + 1)) {
+ ConsiderInitiatingTransition(p, t, aElement, aElementTransitions,
+ aOldStyleContext, aNewStyleContext,
+ &startedAny, &whichStarted);
+ }
+ } else if (nsCSSProps::IsShorthand(property)) {
+ CSSPROPS_FOR_SHORTHAND_SUBPROPERTIES(subprop, property,
+ CSSEnabledState::eForAllContent)
+ {
+ ConsiderInitiatingTransition(*subprop, t, aElement,
+ aElementTransitions,
+ aOldStyleContext, aNewStyleContext,
+ &startedAny, &whichStarted);
+ }
+ } else {
+ ConsiderInitiatingTransition(property, t, aElement, aElementTransitions,
+ aOldStyleContext, aNewStyleContext,
+ &startedAny, &whichStarted);
+ }
+ }
+ }
+
+ // Stop any transitions for properties that are no longer in
+ // 'transition-property', including finished transitions.
+ // Also stop any transitions (and remove any finished transitions)
+ // for properties that just changed (and are still in the set of
+ // properties to transition), but for which we didn't just start the
+ // transition. This can happen delay and duration are both zero, or
+ // because the new value is not interpolable.
+ // Note that we also do the latter set of work in
+ // nsTransitionManager::PruneCompletedTransitions.
+ if (aElementTransitions) {
+ bool checkProperties =
+ aDisp->mTransitions[0].GetProperty() != eCSSPropertyExtra_all_properties;
+ nsCSSPropertyIDSet allTransitionProperties;
+ if (checkProperties) {
+ for (uint32_t i = aDisp->mTransitionPropertyCount; i-- != 0; ) {
+ const StyleTransition& t = aDisp->mTransitions[i];
+ // FIXME: Would be good to find a way to share code between this
+ // interpretation of transition-property and the one above.
+ nsCSSPropertyID property = t.GetProperty();
+ if (property == eCSSPropertyExtra_no_properties ||
+ property == eCSSPropertyExtra_variable ||
+ property == eCSSProperty_UNKNOWN) {
+ // Nothing to do, but need to exclude this from cases below.
+ } else if (property == eCSSPropertyExtra_all_properties) {
+ for (nsCSSPropertyID p = nsCSSPropertyID(0);
+ p < eCSSProperty_COUNT_no_shorthands;
+ p = nsCSSPropertyID(p + 1)) {
+ allTransitionProperties.AddProperty(p);
+ }
+ } else if (nsCSSProps::IsShorthand(property)) {
+ CSSPROPS_FOR_SHORTHAND_SUBPROPERTIES(
+ subprop, property, CSSEnabledState::eForAllContent) {
+ allTransitionProperties.AddProperty(*subprop);
+ }
+ } else {
+ allTransitionProperties.AddProperty(property);
+ }
+ }
+ }
+
+ OwningCSSTransitionPtrArray& animations = aElementTransitions->mAnimations;
+ size_t i = animations.Length();
+ MOZ_ASSERT(i != 0, "empty transitions list?");
+ StyleAnimationValue currentValue;
+ do {
+ --i;
+ CSSTransition* anim = animations[i];
+ // properties no longer in 'transition-property'
+ if ((checkProperties &&
+ !allTransitionProperties.HasProperty(anim->TransitionProperty())) ||
+ // properties whose computed values changed but for which we
+ // did not start a new transition (because delay and
+ // duration are both zero, or because the new value is not
+ // interpolable); a new transition would have anim->ToValue()
+ // matching currentValue
+ !ExtractNonDiscreteComputedValue(anim->TransitionProperty(),
+ aNewStyleContext, currentValue) ||
+ currentValue != anim->ToValue()) {
+ // stop the transition
+ if (anim->HasCurrentEffect()) {
+ EffectSet* effectSet =
+ EffectSet::GetEffectSet(aElement,
+ aNewStyleContext->GetPseudoType());
+ if (effectSet) {
+ effectSet->UpdateAnimationGeneration(mPresContext);
+ }
+ }
+ anim->CancelFromStyle();
+ animations.RemoveElementAt(i);
+ }
+ } while (i != 0);
+
+ if (animations.IsEmpty()) {
+ aElementTransitions->Destroy();
+ aElementTransitions = nullptr;
+ }
+ }
+
+ return startedAny;
+}
+
+void
+nsTransitionManager::ConsiderInitiatingTransition(
+ nsCSSPropertyID aProperty,
+ const StyleTransition& aTransition,
+ dom::Element* aElement,
+ CSSTransitionCollection*& aElementTransitions,
+ nsStyleContext* aOldStyleContext,
+ nsStyleContext* aNewStyleContext,
+ bool* aStartedAny,
+ nsCSSPropertyIDSet* aWhichStarted)
+{
+ // IsShorthand itself will assert if aProperty is not a property.
+ MOZ_ASSERT(!nsCSSProps::IsShorthand(aProperty),
+ "property out of range");
+ NS_ASSERTION(!aElementTransitions ||
+ aElementTransitions->mElement == aElement, "Element mismatch");
+
+ // Ignore disabled properties. We can arrive here if the transition-property
+ // is 'all' and the disabled property has a default value which derives value
+ // from another property, e.g. color.
+ if (!nsCSSProps::IsEnabled(aProperty, CSSEnabledState::eForAllContent)) {
+ return;
+ }
+
+ if (aWhichStarted->HasProperty(aProperty)) {
+ // A later item in transition-property already started a
+ // transition for this property, so we ignore this one.
+ // See comment above and
+ // http://lists.w3.org/Archives/Public/www-style/2009Aug/0109.html .
+ return;
+ }
+
+ if (nsCSSProps::kAnimTypeTable[aProperty] == eStyleAnimType_None) {
+ return;
+ }
+
+ dom::DocumentTimeline* timeline = aElement->OwnerDoc()->Timeline();
+
+ StyleAnimationValue startValue, endValue, dummyValue;
+ bool haveValues =
+ ExtractNonDiscreteComputedValue(aProperty, aOldStyleContext, startValue) &&
+ ExtractNonDiscreteComputedValue(aProperty, aNewStyleContext, endValue);
+
+ bool haveChange = startValue != endValue;
+
+ bool shouldAnimate =
+ haveValues &&
+ haveChange &&
+ // Check that we can interpolate between these values
+ // (If this is ever a performance problem, we could add a
+ // CanInterpolate method, but it seems fine for now.)
+ StyleAnimationValue::Interpolate(aProperty, startValue, endValue,
+ 0.5, dummyValue);
+
+ bool haveCurrentTransition = false;
+ size_t currentIndex = nsTArray<ElementPropertyTransition>::NoIndex;
+ const ElementPropertyTransition *oldPT = nullptr;
+ if (aElementTransitions) {
+ OwningCSSTransitionPtrArray& animations = aElementTransitions->mAnimations;
+ for (size_t i = 0, i_end = animations.Length(); i < i_end; ++i) {
+ if (animations[i]->TransitionProperty() == aProperty) {
+ haveCurrentTransition = true;
+ currentIndex = i;
+ oldPT = animations[i]->GetEffect()
+ ? animations[i]->GetEffect()->AsTransition()
+ : nullptr;
+ break;
+ }
+ }
+ }
+
+ // If we got a style change that changed the value to the endpoint
+ // of the currently running transition, we don't want to interrupt
+ // its timing function.
+ // This needs to be before the !shouldAnimate && haveCurrentTransition
+ // case below because we might be close enough to the end of the
+ // transition that the current value rounds to the final value. In
+ // this case, we'll end up with shouldAnimate as false (because
+ // there's no value change), but we need to return early here rather
+ // than cancel the running transition because shouldAnimate is false!
+ //
+ // Likewise, if we got a style change that changed the value to the
+ // endpoint of our finished transition, we also don't want to start
+ // a new transition for the reasons described in
+ // https://lists.w3.org/Archives/Public/www-style/2015Jan/0444.html .
+ if (haveCurrentTransition && haveValues &&
+ aElementTransitions->mAnimations[currentIndex]->ToValue() == endValue) {
+ // GetAnimationRule already called RestyleForAnimation.
+ return;
+ }
+
+ if (!shouldAnimate) {
+ if (haveCurrentTransition) {
+ // We're in the middle of a transition, and just got a non-transition
+ // style change to something that we can't animate. This might happen
+ // because we got a non-transition style change changing to the current
+ // in-progress value (which is particularly easy to cause when we're
+ // currently in the 'transition-delay'). It also might happen because we
+ // just got a style change to a value that can't be interpolated.
+ OwningCSSTransitionPtrArray& animations =
+ aElementTransitions->mAnimations;
+ animations[currentIndex]->CancelFromStyle();
+ oldPT = nullptr; // Clear pointer so it doesn't dangle
+ animations.RemoveElementAt(currentIndex);
+ EffectSet* effectSet =
+ EffectSet::GetEffectSet(aElement, aNewStyleContext->GetPseudoType());
+ if (effectSet) {
+ effectSet->UpdateAnimationGeneration(mPresContext);
+ }
+
+ if (animations.IsEmpty()) {
+ aElementTransitions->Destroy();
+ // |aElementTransitions| is now a dangling pointer!
+ aElementTransitions = nullptr;
+ }
+ // GetAnimationRule already called RestyleForAnimation.
+ }
+ return;
+ }
+
+ const nsTimingFunction &tf = aTransition.GetTimingFunction();
+ float delay = aTransition.GetDelay();
+ float duration = aTransition.GetDuration();
+ if (duration < 0.0) {
+ // The spec says a negative duration is treated as zero.
+ duration = 0.0;
+ }
+
+ StyleAnimationValue startForReversingTest = startValue;
+ double reversePortion = 1.0;
+
+ // If the new transition reverses an existing one, we'll need to
+ // handle the timing differently.
+ // FIXME: Move mStartForReversingTest, mReversePortion to CSSTransition,
+ // and set the timing function on transitions as an effect-level
+ // easing (rather than keyframe-level easing). (Bug 1292001)
+ if (haveCurrentTransition &&
+ aElementTransitions->mAnimations[currentIndex]->HasCurrentEffect() &&
+ oldPT &&
+ oldPT->mStartForReversingTest == endValue) {
+ // Compute the appropriate negative transition-delay such that right
+ // now we'd end up at the current position.
+ double valuePortion =
+ oldPT->CurrentValuePortion() * oldPT->mReversePortion +
+ (1.0 - oldPT->mReversePortion);
+ // A timing function with negative y1 (or y2!) might make
+ // valuePortion negative. In this case, we still want to apply our
+ // reversing logic based on relative distances, not make duration
+ // negative.
+ if (valuePortion < 0.0) {
+ valuePortion = -valuePortion;
+ }
+ // A timing function with y2 (or y1!) greater than one might
+ // advance past its terminal value. It's probably a good idea to
+ // clamp valuePortion to be at most one to preserve the invariant
+ // that a transition will complete within at most its specified
+ // time.
+ if (valuePortion > 1.0) {
+ valuePortion = 1.0;
+ }
+
+ // Negative delays are essentially part of the transition
+ // function, so reduce them along with the duration, but don't
+ // reduce positive delays.
+ if (delay < 0.0f) {
+ delay *= valuePortion;
+ }
+
+ duration *= valuePortion;
+
+ startForReversingTest = oldPT->ToValue();
+ reversePortion = valuePortion;
+ }
+
+ TimingParams timing;
+ timing.mDuration.emplace(StickyTimeDuration::FromMilliseconds(duration));
+ timing.mDelay = TimeDuration::FromMilliseconds(delay);
+ timing.mIterations = 1.0;
+ timing.mDirection = dom::PlaybackDirection::Normal;
+ timing.mFill = dom::FillMode::Backwards;
+
+ // aElement is non-null here, so we emplace it directly.
+ Maybe<OwningAnimationTarget> target;
+ target.emplace(aElement, aNewStyleContext->GetPseudoType());
+ KeyframeEffectParams effectOptions;
+ RefPtr<ElementPropertyTransition> pt =
+ new ElementPropertyTransition(aElement->OwnerDoc(), target, timing,
+ startForReversingTest, reversePortion,
+ effectOptions);
+
+ pt->SetKeyframes(GetTransitionKeyframes(aNewStyleContext, aProperty,
+ Move(startValue), Move(endValue), tf),
+ aNewStyleContext);
+
+ MOZ_ASSERT(mPresContext->RestyleManager()->IsGecko(),
+ "ServoRestyleManager should not use nsTransitionManager "
+ "for transitions");
+
+ RefPtr<CSSTransition> animation =
+ new CSSTransition(mPresContext->Document()->GetScopeObject());
+ animation->SetOwningElement(
+ OwningElementRef(*aElement, aNewStyleContext->GetPseudoType()));
+ animation->SetTimelineNoUpdate(timeline);
+ animation->SetCreationSequence(
+ mPresContext->RestyleManager()->AsGecko()->GetAnimationGeneration());
+ animation->SetEffectFromStyle(pt);
+ animation->PlayFromStyle();
+
+ if (!aElementTransitions) {
+ bool createdCollection = false;
+ aElementTransitions =
+ CSSTransitionCollection::GetOrCreateAnimationCollection(
+ aElement, aNewStyleContext->GetPseudoType(), &createdCollection);
+ if (!aElementTransitions) {
+ MOZ_ASSERT(!createdCollection, "outparam should agree with return value");
+ NS_WARNING("allocating collection failed");
+ return;
+ }
+
+ if (createdCollection) {
+ AddElementCollection(aElementTransitions);
+ }
+ }
+
+ OwningCSSTransitionPtrArray& animations = aElementTransitions->mAnimations;
+#ifdef DEBUG
+ for (size_t i = 0, i_end = animations.Length(); i < i_end; ++i) {
+ MOZ_ASSERT(
+ i == currentIndex || animations[i]->TransitionProperty() != aProperty,
+ "duplicate transitions for property");
+ }
+#endif
+ if (haveCurrentTransition) {
+ // If this new transition is replacing an existing transition that is running
+ // on the compositor, we store select parameters from the replaced transition
+ // so that later, once all scripts have run, we can update the start value
+ // of the transition using TimeStamp::Now(). This allows us to avoid a
+ // large jump when starting a new transition when the main thread lags behind
+ // the compositor.
+ if (oldPT &&
+ oldPT->IsCurrent() &&
+ oldPT->IsRunningOnCompositor() &&
+ !oldPT->GetAnimation()->GetStartTime().IsNull() &&
+ timeline == oldPT->GetAnimation()->GetTimeline()) {
+ const AnimationPropertySegment& segment =
+ oldPT->Properties()[0].mSegments[0];
+ pt->mReplacedTransition.emplace(
+ ElementPropertyTransition::ReplacedTransitionProperties({
+ oldPT->GetAnimation()->GetStartTime().Value(),
+ oldPT->GetAnimation()->PlaybackRate(),
+ oldPT->SpecifiedTiming(),
+ segment.mTimingFunction,
+ segment.mFromValue,
+ segment.mToValue
+ })
+ );
+ }
+ animations[currentIndex]->CancelFromStyle();
+ oldPT = nullptr; // Clear pointer so it doesn't dangle
+ animations[currentIndex] = animation;
+ } else {
+ if (!animations.AppendElement(animation)) {
+ NS_WARNING("out of memory");
+ return;
+ }
+ }
+
+ EffectSet* effectSet =
+ EffectSet::GetEffectSet(aElement, aNewStyleContext->GetPseudoType());
+ if (effectSet) {
+ effectSet->UpdateAnimationGeneration(mPresContext);
+ }
+
+ *aStartedAny = true;
+ aWhichStarted->AddProperty(aProperty);
+}
+
+static Keyframe&
+AppendKeyframe(double aOffset, nsCSSPropertyID aProperty,
+ StyleAnimationValue&& aValue, nsTArray<Keyframe>& aKeyframes)
+{
+ Keyframe& frame = *aKeyframes.AppendElement();
+ frame.mOffset.emplace(aOffset);
+ PropertyValuePair& pv = *frame.mPropertyValues.AppendElement();
+ pv.mProperty = aProperty;
+ DebugOnly<bool> uncomputeResult =
+ StyleAnimationValue::UncomputeValue(aProperty, Move(aValue), pv.mValue);
+ MOZ_ASSERT(uncomputeResult,
+ "Unable to get specified value from computed value");
+ return frame;
+}
+
+nsTArray<Keyframe>
+nsTransitionManager::GetTransitionKeyframes(
+ nsStyleContext* aStyleContext,
+ nsCSSPropertyID aProperty,
+ StyleAnimationValue&& aStartValue,
+ StyleAnimationValue&& aEndValue,
+ const nsTimingFunction& aTimingFunction)
+{
+ nsTArray<Keyframe> keyframes(2);
+
+ Keyframe& fromFrame = AppendKeyframe(0.0, aProperty, Move(aStartValue),
+ keyframes);
+ if (aTimingFunction.mType != nsTimingFunction::Type::Linear) {
+ fromFrame.mTimingFunction.emplace();
+ fromFrame.mTimingFunction->Init(aTimingFunction);
+ }
+
+ AppendKeyframe(1.0, aProperty, Move(aEndValue), keyframes);
+
+ return keyframes;
+}
+
+void
+nsTransitionManager::PruneCompletedTransitions(mozilla::dom::Element* aElement,
+ CSSPseudoElementType aPseudoType,
+ nsStyleContext* aNewStyleContext)
+{
+ CSSTransitionCollection* collection =
+ CSSTransitionCollection::GetAnimationCollection(aElement, aPseudoType);
+ if (!collection) {
+ return;
+ }
+
+ // Remove any finished transitions whose style doesn't match the new
+ // style.
+ // This is similar to some of the work that happens near the end of
+ // nsTransitionManager::StyleContextChanged.
+ // FIXME (bug 1158431): Really, we should also cancel running
+ // transitions whose destination doesn't match as well.
+ OwningCSSTransitionPtrArray& animations = collection->mAnimations;
+ size_t i = animations.Length();
+ MOZ_ASSERT(i != 0, "empty transitions list?");
+ do {
+ --i;
+ CSSTransition* anim = animations[i];
+
+ if (anim->HasCurrentEffect()) {
+ continue;
+ }
+
+ // Since effect is a finished transition, we know it didn't
+ // influence style.
+ StyleAnimationValue currentValue;
+ if (!ExtractNonDiscreteComputedValue(anim->TransitionProperty(),
+ aNewStyleContext, currentValue) ||
+ currentValue != anim->ToValue()) {
+ anim->CancelFromStyle();
+ animations.RemoveElementAt(i);
+ }
+ } while (i != 0);
+
+ if (collection->mAnimations.IsEmpty()) {
+ collection->Destroy();
+ // |collection| is now a dangling pointer!
+ collection = nullptr;
+ }
+}
+
+void
+nsTransitionManager::StopTransitionsForElement(
+ mozilla::dom::Element* aElement,
+ mozilla::CSSPseudoElementType aPseudoType)
+{
+ MOZ_ASSERT(aElement);
+ CSSTransitionCollection* collection =
+ CSSTransitionCollection::GetAnimationCollection(aElement, aPseudoType);
+ if (!collection) {
+ return;
+ }
+
+ nsAutoAnimationMutationBatch mb(aElement->OwnerDoc());
+ collection->Destroy();
+}