gecko-dev/layout/style/nsTransitionManager.cpp

1157 строки
44 KiB
C++

/* -*- 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 "nsContentUtils.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/ServoBindings.h"
#include "mozilla/ServoComputedValuesWithParent.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/RestyleManager.h"
#include "mozilla/RestyleManagerInlines.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;
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);
MOZ_ASSERT(mProperties.Length() == 1 &&
mProperties[0].mSegments.Length() == 1,
"The transition should have one property and one segment");
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");
const AnimationValue& replacedFrom = mReplacedTransition->mFromValue;
const AnimationValue& replacedTo = mReplacedTransition->mToValue;
AnimationValue startValue;
if (mDocument->IsStyledByServo()) {
startValue.mServo =
Servo_AnimationValues_Interpolate(replacedFrom.mServo,
replacedTo.mServo,
valuePosition).Consume();
if (startValue.mServo) {
mKeyframes[0].mPropertyValues[0].mServoDeclarationBlock =
Servo_AnimationValue_Uncompute(startValue.mServo).Consume();
mProperties[0].mSegments[0].mFromValue = Move(startValue);
}
} else if (StyleAnimationValue::Interpolate(mProperties[0].mProperty,
replacedFrom.mGecko,
replacedTo.mGecko,
valuePosition,
startValue.mGecko)) {
nsCSSValue cssValue;
DebugOnly<bool> uncomputeResult =
StyleAnimationValue::UncomputeValue(mProperties[0].mProperty,
startValue.mGecko,
cssValue);
MOZ_ASSERT(uncomputeResult, "UncomputeValue should not fail");
mKeyframes[0].mPropertyValues[0].mValue = cssValue;
mProperties[0].mSegments[0].mFromValue = Move(startValue);
}
}
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(StickyTimeDuration aActiveTime)
{
if (!mOwningElement.IsSet()) {
return;
}
dom::Element* owningElement;
CSSPseudoElementType owningPseudoType;
mOwningElement.GetElement(owningElement, owningPseudoType);
MOZ_ASSERT(owningElement, "Owning element should be set");
nsPresContext* presContext =
nsContentUtils::GetContextForContent(owningElement);
if (!presContext) {
return;
}
const StickyTimeDuration zeroDuration = StickyTimeDuration();
TransitionPhase currentPhase;
StickyTimeDuration intervalStartTime;
StickyTimeDuration intervalEndTime;
if (!mEffect) {
currentPhase = GetAnimationPhaseWithoutEffect<TransitionPhase>(*this);
} else {
ComputedTiming computedTiming = mEffect->GetComputedTiming();
currentPhase = static_cast<TransitionPhase>(computedTiming.mPhase);
intervalStartTime =
std::max(std::min(StickyTimeDuration(-mEffect->SpecifiedTiming().mDelay),
computedTiming.mActiveDuration), zeroDuration);
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 zeroTimeStamp = AnimationTimeToTimeStamp(zeroDuration);
TimeStamp startTimeStamp = ElapsedTimeToTimeStamp(intervalStartTime);
TimeStamp endTimeStamp = ElapsedTimeToTimeStamp(intervalEndTime);
if (mPendingState != PendingState::NotPending &&
(mPreviousTransitionPhase == TransitionPhase::Idle ||
mPreviousTransitionPhase == TransitionPhase::Pending))
{
currentPhase = TransitionPhase::Pending;
}
AutoTArray<TransitionEventParams, 3> events;
// Handle cancel events first
if ((mPreviousTransitionPhase != TransitionPhase::Idle &&
mPreviousTransitionPhase != TransitionPhase::After) &&
currentPhase == TransitionPhase::Idle) {
TimeStamp activeTimeStamp = ElapsedTimeToTimeStamp(aActiveTime);
events.AppendElement(TransitionEventParams{ eTransitionCancel,
aActiveTime,
activeTimeStamp });
}
// All other events
switch (mPreviousTransitionPhase) {
case TransitionPhase::Idle:
if (currentPhase == TransitionPhase::Pending ||
currentPhase == TransitionPhase::Before) {
events.AppendElement(TransitionEventParams{ eTransitionRun,
intervalStartTime,
zeroTimeStamp });
} else if (currentPhase == TransitionPhase::Active) {
events.AppendElement(TransitionEventParams{ eTransitionRun,
intervalStartTime,
zeroTimeStamp });
events.AppendElement(TransitionEventParams{ eTransitionStart,
intervalStartTime,
startTimeStamp });
} else if (currentPhase == TransitionPhase::After) {
events.AppendElement(TransitionEventParams{ eTransitionRun,
intervalStartTime,
zeroTimeStamp });
events.AppendElement(TransitionEventParams{ eTransitionStart,
intervalStartTime,
startTimeStamp });
events.AppendElement(TransitionEventParams{ eTransitionEnd,
intervalEndTime,
endTimeStamp });
}
break;
case TransitionPhase::Pending:
case TransitionPhase::Before:
if (currentPhase == TransitionPhase::Active) {
events.AppendElement(TransitionEventParams{ eTransitionStart,
intervalStartTime,
startTimeStamp });
} else if (currentPhase == TransitionPhase::After) {
events.AppendElement(TransitionEventParams{ eTransitionStart,
intervalStartTime,
startTimeStamp });
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::Active) {
events.AppendElement(TransitionEventParams{ eTransitionStart,
intervalEndTime,
startTimeStamp });
} else if (currentPhase == TransitionPhase::Before) {
events.AppendElement(TransitionEventParams{ eTransitionStart,
intervalEndTime,
startTimeStamp });
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;
}
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(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 dom::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(dom::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,
AnimationValue& aAnimationValue)
{
return (nsCSSProps::kAnimTypeTable[aProperty] != eStyleAnimType_Discrete ||
aProperty == eCSSProperty_visibility) &&
StyleAnimationValue::ExtractComputedValue(aProperty, aStyleContext,
aAnimationValue.mGecko);
}
static inline bool
ExtractNonDiscreteComputedValue(nsCSSPropertyID aProperty,
const ServoComputedValuesWithParent&
aComputedStyle,
AnimationValue& aAnimationValue)
{
if (Servo_Property_IsDiscreteAnimatable(aProperty) &&
aProperty != eCSSProperty_visibility) {
return false;
}
aAnimationValue.mServo =
Servo_ComputedValues_ExtractAnimationValue(aComputedStyle.mCurrentStyle,
aProperty).Consume();
return !!aAnimationValue.mServo;
}
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()->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->ResolveStyleByRemovingAnimation(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 = DoUpdateTransitions(disp,
aElement,
afterChangeStyle->GetPseudoType(),
collection,
aOldStyleContext,
afterChangeStyle.get());
}
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(
dom::Element *aElement,
CSSPseudoElementType aPseudoType,
const ServoComputedValuesWithParent& aOldStyle,
const ServoComputedValuesWithParent& aNewStyle)
{
if (!mPresContext->IsDynamic()) {
// For print or print preview, ignore transitions.
return false;
}
CSSTransitionCollection* collection =
CSSTransitionCollection::GetAnimationCollection(aElement, aPseudoType);
const nsStyleDisplay *disp = Servo_GetStyleDisplay(aNewStyle.mCurrentStyle);
return DoUpdateTransitions(disp,
aElement, aPseudoType,
collection,
aOldStyle, aNewStyle);
}
template<typename StyleType>
bool
nsTransitionManager::DoUpdateTransitions(
const nsStyleDisplay* aDisp,
dom::Element* aElement,
CSSPseudoElementType aPseudoType,
CSSTransitionCollection*& aElementTransitions,
StyleType aOldStyle,
StyleType aNewStyle)
{
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, aPseudoType,
aElementTransitions,
aOldStyle, aNewStyle,
&startedAny, &whichStarted);
}
} else if (nsCSSProps::IsShorthand(property)) {
CSSPROPS_FOR_SHORTHAND_SUBPROPERTIES(subprop, property,
CSSEnabledState::eForAllContent)
{
ConsiderInitiatingTransition(*subprop, t, aElement, aPseudoType,
aElementTransitions,
aOldStyle, aNewStyle,
&startedAny, &whichStarted);
}
} else {
ConsiderInitiatingTransition(property, t, aElement, aPseudoType,
aElementTransitions,
aOldStyle, aNewStyle,
&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?");
AnimationValue 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(),
aNewStyle, currentValue) ||
currentValue != anim->ToValue()) {
// stop the transition
if (anim->HasCurrentEffect()) {
EffectSet* effectSet =
EffectSet::GetEffectSet(aElement, aPseudoType);
if (effectSet) {
effectSet->UpdateAnimationGeneration(mPresContext);
}
}
anim->CancelFromStyle();
animations.RemoveElementAt(i);
}
} while (i != 0);
if (animations.IsEmpty()) {
aElementTransitions->Destroy();
aElementTransitions = nullptr;
}
}
return startedAny;
}
static Keyframe&
AppendKeyframe(double aOffset,
nsCSSPropertyID aProperty,
AnimationValue&& aValue,
nsTArray<Keyframe>& aKeyframes)
{
Keyframe& frame = *aKeyframes.AppendElement();
frame.mOffset.emplace(aOffset);
PropertyValuePair& pv = *frame.mPropertyValues.AppendElement();
pv.mProperty = aProperty;
if (aValue.mServo) {
pv.mServoDeclarationBlock =
Servo_AnimationValue_Uncompute(aValue.mServo).Consume();
} else {
DebugOnly<bool> uncomputeResult =
StyleAnimationValue::UncomputeValue(aProperty, Move(aValue.mGecko),
pv.mValue);
MOZ_ASSERT(uncomputeResult,
"Unable to get specified value from computed value");
}
return frame;
}
static nsTArray<Keyframe>
GetTransitionKeyframes(nsCSSPropertyID aProperty,
AnimationValue&& aStartValue,
AnimationValue&& 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;
}
static bool
IsAnimatable(nsCSSPropertyID aProperty, bool aIsServo)
{
if (aIsServo) {
return Servo_Property_IsAnimatable(aProperty);
}
return nsCSSProps::kAnimTypeTable[aProperty] != eStyleAnimType_None;
}
template<typename StyleType>
void
nsTransitionManager::ConsiderInitiatingTransition(
nsCSSPropertyID aProperty,
const StyleTransition& aTransition,
dom::Element* aElement,
CSSPseudoElementType aPseudoType,
CSSTransitionCollection*& aElementTransitions,
StyleType aOldStyle,
StyleType aNewStyle,
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 (!IsAnimatable(aProperty, aElement->IsStyledByServo())) {
return;
}
dom::DocumentTimeline* timeline = aElement->OwnerDoc()->Timeline();
AnimationValue startValue, endValue;
bool haveValues =
ExtractNonDiscreteComputedValue(aProperty, aOldStyle, startValue) &&
ExtractNonDiscreteComputedValue(aProperty, aNewStyle, endValue);
bool haveChange = startValue != endValue;
bool shouldAnimate =
haveValues &&
haveChange &&
startValue.IsInterpolableWith(aProperty, endValue);
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, aPseudoType);
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;
}
AnimationValue 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 =
TimingParamsFromCSSParams(duration, delay,
1.0 /* iteration count */,
dom::PlaybackDirection::Normal,
dom::FillMode::Backwards);
// aElement is non-null here, so we emplace it directly.
Maybe<OwningAnimationTarget> target;
target.emplace(aElement, aPseudoType);
KeyframeEffectParams effectOptions;
RefPtr<ElementPropertyTransition> pt =
new ElementPropertyTransition(aElement->OwnerDoc(), target, timing,
startForReversingTest, reversePortion,
effectOptions);
pt->SetKeyframes(GetTransitionKeyframes(aProperty,
Move(startValue), Move(endValue), tf),
aNewStyle);
RefPtr<CSSTransition> animation =
new CSSTransition(mPresContext->Document()->GetScopeObject());
animation->SetOwningElement(OwningElementRef(*aElement, aPseudoType));
animation->SetTimelineNoUpdate(timeline);
animation->SetCreationSequence(
mPresContext->RestyleManager()->GetAnimationGeneration());
animation->SetEffectFromStyle(pt);
animation->PlayFromStyle();
if (!aElementTransitions) {
bool createdCollection = false;
aElementTransitions =
CSSTransitionCollection::GetOrCreateAnimationCollection(
aElement, aPseudoType, &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, aPseudoType);
if (effectSet) {
effectSet->UpdateAnimationGeneration(mPresContext);
}
*aStartedAny = true;
aWhichStarted->AddProperty(aProperty);
}
void
nsTransitionManager::PruneCompletedTransitions(mozilla::dom::Element* aElement,
CSSPseudoElementType aPseudoType,
nsStyleContext* aNewStyleContext)
{
MOZ_ASSERT(!aElement->IsGeneratedContentContainerForBefore() &&
!aElement->IsGeneratedContentContainerForAfter());
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.
AnimationValue 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;
}
}