/* -*- 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/. */ /* Code to start and animate CSS transitions. */ #include "nsTransitionManager.h" #include "mozilla/dom/Document.h" #include "nsAnimationManager.h" #include "nsIContent.h" #include "AnimatedPropertyID.h" #include "AnimatedPropertyIDSet.h" #include "mozilla/ComputedStyle.h" #include "mozilla/MemoryReporting.h" #include "nsCSSPropertyIDSet.h" #include "mozilla/EffectSet.h" #include "mozilla/ElementAnimationData.h" #include "mozilla/EventDispatcher.h" #include "mozilla/ServoBindings.h" #include "mozilla/StyleAnimationValue.h" #include "mozilla/dom/DocumentTimeline.h" #include "mozilla/dom/Element.h" #include "nsIFrame.h" #include "nsCSSProps.h" #include "nsDisplayList.h" #include "nsRFPService.h" #include "nsStyleChangeList.h" #include "mozilla/RestyleManager.h" using mozilla::dom::CSSTransition; using mozilla::dom::DocumentTimeline; using mozilla::dom::KeyframeEffect; using namespace mozilla; using namespace mozilla::css; bool nsTransitionManager::UpdateTransitions( dom::Element* aElement, const PseudoStyleRequest& aPseudoRequest, const ComputedStyle& aOldStyle, const ComputedStyle& aNewStyle) { if (mPresContext->Medium() == nsGkAtoms::print) { // For print or print preview, ignore transitions. return false; } MOZ_ASSERT(mPresContext->IsDynamic()); if (aNewStyle.StyleDisplay()->mDisplay == StyleDisplay::None) { StopAnimationsForElement(aElement, aPseudoRequest); return false; } auto* collection = CSSTransitionCollection::Get(aElement, aPseudoRequest); return DoUpdateTransitions(*aNewStyle.StyleUIReset(), aElement, aPseudoRequest, collection, aOldStyle, aNewStyle); } // This function expands the shorthands and "all" keyword specified in // transition-property, and then execute |aHandler| on the expanded longhand. // |aHandler| should be a lamda function which accepts nsCSSPropertyID. template static void ExpandTransitionProperty(const StyleTransitionProperty& aProperty, T aHandler) { switch (aProperty.tag) { case StyleTransitionProperty::Tag::Unsupported: break; case StyleTransitionProperty::Tag::Custom: { AnimatedPropertyID property(aProperty.AsCustom().AsAtom()); aHandler(property); break; } case StyleTransitionProperty::Tag::NonCustom: { nsCSSPropertyID id = nsCSSPropertyID(aProperty.AsNonCustom()._0); if (nsCSSProps::IsShorthand(id)) { CSSPROPS_FOR_SHORTHAND_SUBPROPERTIES(subprop, id, CSSEnabledState::ForAllContent) { AnimatedPropertyID property(*subprop); aHandler(property); } } else { AnimatedPropertyID property(id); aHandler(property); } break; } } } bool nsTransitionManager::DoUpdateTransitions( const nsStyleUIReset& aStyle, dom::Element* aElement, const PseudoStyleRequest& aPseudoRequest, CSSTransitionCollection*& aElementTransitions, const ComputedStyle& aOldStyle, const ComputedStyle& aNewStyle) { 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 |propertiesChecked|). bool startedAny = false; AnimatedPropertyIDSet propertiesChecked; for (uint32_t i = aStyle.mTransitionPropertyCount; i--;) { const float delay = aStyle.GetTransitionDelay(i).ToMilliseconds(); // The spec says a negative duration is treated as zero. const float duration = std::max(aStyle.GetTransitionDuration(i).ToMilliseconds(), 0.0f); // If the combined duration of this transition is 0 or less we won't start a // transition, we can avoid even looking at transition-property if we're the // last one. if (i == 0 && delay + duration <= 0.0f) { continue; } const auto behavior = aStyle.GetTransitionBehavior(i); ExpandTransitionProperty(aStyle.GetTransitionProperty(i), [&](const AnimatedPropertyID& aProperty) { // We might have something to transition. See if // any of the properties in question changed and // are animatable. startedAny |= ConsiderInitiatingTransition( aProperty, aStyle, i, delay, duration, behavior, aElement, aPseudoRequest, aElementTransitions, aOldStyle, aNewStyle, propertiesChecked); }); } // 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. if (aElementTransitions) { const bool checkProperties = !aStyle.GetTransitionProperty(0).IsAll(); AnimatedPropertyIDSet allTransitionProperties; if (checkProperties) { for (uint32_t i = aStyle.mTransitionPropertyCount; i-- != 0;) { ExpandTransitionProperty(aStyle.GetTransitionProperty(i), [&](const AnimatedPropertyID& aProperty) { allTransitionProperties.AddProperty( aProperty.ToPhysical(aNewStyle)); }); } } OwningCSSTransitionPtrArray& animations = aElementTransitions->mAnimations; size_t i = animations.Length(); MOZ_ASSERT(i != 0, "empty transitions list?"); AnimationValue currentValue; do { --i; CSSTransition* anim = animations[i]; const AnimatedPropertyID& property = anim->TransitionProperty(); if ( // Properties no longer in `transition-property`. (checkProperties && !allTransitionProperties.HasProperty(property)) || // 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. !Servo_ComputedValues_TransitionValueMatches( &aNewStyle, &property, anim->ToValue().mServo.get())) { // Stop the transition. DoCancelTransition(aElement, aPseudoRequest, aElementTransitions, i); } } while (i != 0); } return startedAny; } static Keyframe& AppendKeyframe(double aOffset, const AnimatedPropertyID& aProperty, AnimationValue&& aValue, nsTArray& aKeyframes) { Keyframe& frame = *aKeyframes.AppendElement(); frame.mOffset.emplace(aOffset); MOZ_ASSERT(aValue.mServo); RefPtr decl = Servo_AnimationValue_Uncompute(aValue.mServo).Consume(); frame.mPropertyValues.AppendElement( PropertyValuePair(aProperty, std::move(decl))); return frame; } static nsTArray GetTransitionKeyframes( const AnimatedPropertyID& aProperty, AnimationValue&& aStartValue, AnimationValue&& aEndValue) { nsTArray keyframes(2); AppendKeyframe(0.0, aProperty, std::move(aStartValue), keyframes); AppendKeyframe(1.0, aProperty, std::move(aEndValue), keyframes); return keyframes; } using ReplacedTransitionProperties = CSSTransition::ReplacedTransitionProperties; static Maybe GetReplacedTransitionProperties( const CSSTransition& aTransition, const DocumentTimeline* aTimelineToMatch) { Maybe result; if (!aTransition.HasCurrentEffect()) { return result; } // Transition needs to be running on the same timeline. if (aTransition.GetTimeline() != aTimelineToMatch) { return result; } auto startTime = aTransition.GetStartTime(); if (startTime.IsNull() && !aTransition.GetPendingReadyTime().IsNull()) { startTime = aTimelineToMatch->ToTimelineTime(aTransition.GetPendingReadyTime()); } if (startTime.IsNull()) { return result; } // The transition needs to have a keyframe effect. const KeyframeEffect* keyframeEffect = aTransition.GetEffect() ? aTransition.GetEffect()->AsKeyframeEffect() : nullptr; if (!keyframeEffect) { return result; } // The keyframe effect needs to be a simple transition of the original // transition property (i.e. not replaced with something else). if (keyframeEffect->Properties().Length() != 1 || keyframeEffect->Properties()[0].mSegments.Length() != 1 || keyframeEffect->Properties()[0].mProperty != aTransition.TransitionProperty()) { return result; } const AnimationPropertySegment& segment = keyframeEffect->Properties()[0].mSegments[0]; result.emplace(ReplacedTransitionProperties( {startTime.Value(), aTransition.PlaybackRate(), keyframeEffect->SpecifiedTiming(), segment.mTimingFunction, segment.mFromValue, segment.mToValue})); return result; } bool nsTransitionManager::ConsiderInitiatingTransition( const AnimatedPropertyID& aProperty, const nsStyleUIReset& aStyle, uint32_t aTransitionIndex, float aDelay, float aDuration, mozilla::StyleTransitionBehavior aBehavior, dom::Element* aElement, const PseudoStyleRequest& aPseudoRequest, CSSTransitionCollection*& aElementTransitions, const ComputedStyle& aOldStyle, const ComputedStyle& aNewStyle, AnimatedPropertyIDSet& aPropertiesChecked) { // IsShorthand itself will assert if aProperty is not a property. MOZ_ASSERT(aProperty.IsCustom() || !nsCSSProps::IsShorthand(aProperty.mID), "property out of range"); NS_ASSERTION( !aElementTransitions || &aElementTransitions->mElement == aElement, "Element mismatch"); AnimatedPropertyID property = aProperty.ToPhysical(aNewStyle); // A later item in transition-property already specified a transition for // this property, so we ignore this one. // // See http://lists.w3.org/Archives/Public/www-style/2009Aug/0109.html . if (aPropertiesChecked.HasProperty(property)) { return false; } aPropertiesChecked.AddProperty(property); if (aDuration + aDelay <= 0.0f) { return false; } size_t currentIndex = nsTArray::NoIndex; const auto* oldTransition = [&]() -> const CSSTransition* { if (!aElementTransitions) { return nullptr; } const OwningCSSTransitionPtrArray& animations = aElementTransitions->mAnimations; for (size_t i = 0, i_end = animations.Length(); i < i_end; ++i) { if (animations[i]->TransitionProperty() == property) { currentIndex = i; return animations[i]; } } return nullptr; }(); // For compositor animations, |aOldStyle| may have out-of-date transition // rules, and it may be equal to the |endValue| of a reversing transition by // accidentally. This causes Servo_ComputedValues_ShouldTransition() to return // an incorrect result. Therefore, we have to recompute the current value if // this transition is running on the compositor, to make sure we create the // transition properly. Here, we pre-compute the progress and collect the // necessary info, so Servo_ComputedValues_ShouldTransition() could compute // the current value if needed. // FIXME: Bug 1634945. We should use the last value from the compositor as the // current value. Maybe replacedTransitionProperties; Maybe progress; if (oldTransition) { // If this new transition is replacing an existing transition, 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. // // Note: We compute this before calling // Servo_ComputedValues_ShouldTransition() so we can reuse it for computing // the current value and setting the replaced transition properties later in // this function. const dom::DocumentTimeline* timeline = aElement->OwnerDoc()->Timeline(); replacedTransitionProperties = GetReplacedTransitionProperties(*oldTransition, timeline); progress = replacedTransitionProperties.andThen( [&](const ReplacedTransitionProperties& aProperties) { const dom::AnimationTimeline* timeline = oldTransition->GetTimeline(); MOZ_ASSERT(timeline); return CSSTransition::ComputeTransformedProgress(*timeline, aProperties); }); } AnimationValue startValue, endValue; const StyleShouldTransitionResult result = Servo_ComputedValues_ShouldTransition( &aOldStyle, &aNewStyle, &property, aBehavior, oldTransition ? oldTransition->ToValue().mServo.get() : nullptr, replacedTransitionProperties ? replacedTransitionProperties->mFromValue.mServo.get() : nullptr, // Note: It's possible to replace the keyframes by Web Animations API, // so we have to pass the mToValue from the keyframe segment, to make // sure this value is aligned with mFromValue. replacedTransitionProperties ? replacedTransitionProperties->mToValue.mServo.get() : nullptr, progress.ptrOr(nullptr), &startValue.mServo, &endValue.mServo); // 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 (result.old_transition_value_matches) { // GetAnimationRule already called RestyleForAnimation. return false; } if (!result.should_animate) { if (oldTransition) { // 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. DoCancelTransition(aElement, aPseudoRequest, aElementTransitions, currentIndex); } return false; } AnimationValue startForReversingTest = startValue; double reversePortion = 1.0; // If the new transition reverses an existing one, we'll need to // handle the timing differently. if (oldTransition && oldTransition->HasCurrentEffect() && oldTransition->StartForReversingTest() == endValue) { // Compute the appropriate negative transition-delay such that right // now we'd end up at the current position. double valuePortion = oldTransition->CurrentValuePortion() * oldTransition->ReversePortion() + (1.0 - oldTransition->ReversePortion()); // 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 (aDelay < 0.0f && std::isfinite(aDelay)) { aDelay *= valuePortion; } if (std::isfinite(aDuration)) { aDuration *= valuePortion; } startForReversingTest = oldTransition->ToValue(); reversePortion = valuePortion; } TimingParams timing = TimingParamsFromCSSParams( Some(aDuration), aDelay, 1.0 /* iteration count */, StyleAnimationDirection::Normal, StyleAnimationFillMode::Backwards); const StyleComputedTimingFunction& tf = aStyle.GetTransitionTimingFunction(aTransitionIndex); if (!tf.IsLinearKeyword()) { timing.SetTimingFunction(Some(tf)); } RefPtr transition = DoCreateTransition( property, aElement, aPseudoRequest, aNewStyle, aElementTransitions, std::move(timing), std::move(startValue), std::move(endValue), std::move(startForReversingTest), reversePortion); if (!transition) { return false; } OwningCSSTransitionPtrArray& transitions = aElementTransitions->mAnimations; #ifdef DEBUG for (size_t i = 0, i_end = transitions.Length(); i < i_end; ++i) { MOZ_ASSERT( i == currentIndex || transitions[i]->TransitionProperty() != property, "duplicate transitions for property"); } #endif if (oldTransition) { if (replacedTransitionProperties) { transition->SetReplacedTransition( std::move(replacedTransitionProperties.ref())); } transitions[currentIndex]->CancelFromStyle(PostRestyleMode::IfNeeded); oldTransition = nullptr; // Clear pointer so it doesn't dangle transitions[currentIndex] = transition; } else { // XXX(Bug 1631371) Check if this should use a fallible operation as it // pretended earlier. transitions.AppendElement(transition); } if (auto* effectSet = EffectSet::Get(aElement, aPseudoRequest)) { effectSet->UpdateAnimationGeneration(mPresContext); } return true; } already_AddRefed nsTransitionManager::DoCreateTransition( const AnimatedPropertyID& aProperty, dom::Element* aElement, const PseudoStyleRequest& aPseudoRequest, const mozilla::ComputedStyle& aNewStyle, CSSTransitionCollection*& aElementTransitions, TimingParams&& aTiming, AnimationValue&& aStartValue, AnimationValue&& aEndValue, AnimationValue&& aStartForReversingTest, double aReversePortion) { dom::DocumentTimeline* timeline = aElement->OwnerDoc()->Timeline(); KeyframeEffectParams effectOptions; auto keyframeEffect = MakeRefPtr( aElement->OwnerDoc(), OwningAnimationTarget(aElement, aPseudoRequest), std::move(aTiming), effectOptions); keyframeEffect->SetKeyframes( GetTransitionKeyframes(aProperty, std::move(aStartValue), std::move(aEndValue)), &aNewStyle, timeline); if (NS_WARN_IF(MOZ_UNLIKELY(!keyframeEffect->IsValidTransition()))) { return nullptr; } auto animation = MakeRefPtr( mPresContext->Document()->GetScopeObject(), aProperty); animation->SetOwningElement(OwningElementRef(*aElement, aPseudoRequest)); animation->SetTimelineNoUpdate(timeline); animation->SetCreationSequence( mPresContext->RestyleManager()->GetAnimationGeneration()); animation->SetEffectFromStyle(keyframeEffect); animation->SetReverseParameters(std::move(aStartForReversingTest), aReversePortion); animation->PlayFromStyle(); if (!aElementTransitions) { aElementTransitions = &aElement->EnsureAnimationData().EnsureTransitionCollection( *aElement, aPseudoRequest); if (!aElementTransitions->isInList()) { AddElementCollection(aElementTransitions); } } return animation.forget(); } void nsTransitionManager::DoCancelTransition( dom::Element* aElement, const PseudoStyleRequest& aPseudoRequest, CSSTransitionCollection*& aElementTransitions, size_t aIndex) { MOZ_ASSERT(aElementTransitions); OwningCSSTransitionPtrArray& transitions = aElementTransitions->mAnimations; CSSTransition* transition = transitions[aIndex]; if (transition->HasCurrentEffect()) { if (auto* effectSet = EffectSet::Get(aElement, aPseudoRequest)) { effectSet->UpdateAnimationGeneration(mPresContext); } } transition->CancelFromStyle(PostRestyleMode::IfNeeded); transitions.RemoveElementAt(aIndex); if (transitions.IsEmpty()) { aElementTransitions->Destroy(); // |aElementTransitions| is now a dangling pointer! aElementTransitions = nullptr; } }