1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 /* Code to start and animate CSS transitions. */
9 #include "nsTransitionManager.h"
10 #include "mozilla/dom/Document.h"
11 #include "nsAnimationManager.h"
13 #include "nsIContent.h"
14 #include "AnimatedPropertyID.h"
15 #include "AnimatedPropertyIDSet.h"
16 #include "mozilla/ComputedStyle.h"
17 #include "mozilla/MemoryReporting.h"
18 #include "nsCSSPropertyIDSet.h"
19 #include "mozilla/EffectSet.h"
20 #include "mozilla/ElementAnimationData.h"
21 #include "mozilla/EventDispatcher.h"
22 #include "mozilla/ServoBindings.h"
23 #include "mozilla/StyleAnimationValue.h"
24 #include "mozilla/dom/DocumentTimeline.h"
25 #include "mozilla/dom/Element.h"
27 #include "nsCSSProps.h"
28 #include "nsCSSPseudoElements.h"
29 #include "nsDisplayList.h"
30 #include "nsRFPService.h"
31 #include "nsStyleChangeList.h"
32 #include "mozilla/RestyleManager.h"
34 using mozilla::dom::CSSTransition
;
35 using mozilla::dom::DocumentTimeline
;
36 using mozilla::dom::KeyframeEffect
;
38 using namespace mozilla
;
39 using namespace mozilla::css
;
41 bool nsTransitionManager::UpdateTransitions(dom::Element
* aElement
,
42 PseudoStyleType aPseudoType
,
43 const ComputedStyle
& aOldStyle
,
44 const ComputedStyle
& aNewStyle
) {
45 if (mPresContext
->Medium() == nsGkAtoms::print
) {
46 // For print or print preview, ignore transitions.
50 MOZ_ASSERT(mPresContext
->IsDynamic());
51 if (aNewStyle
.StyleDisplay()->mDisplay
== StyleDisplay::None
) {
52 StopAnimationsForElement(aElement
, aPseudoType
);
56 auto* collection
= CSSTransitionCollection::Get(aElement
, aPseudoType
);
57 return DoUpdateTransitions(*aNewStyle
.StyleUIReset(), aElement
, aPseudoType
,
58 collection
, aOldStyle
, aNewStyle
);
61 // This function expands the shorthands and "all" keyword specified in
62 // transition-property, and then execute |aHandler| on the expanded longhand.
63 // |aHandler| should be a lamda function which accepts nsCSSPropertyID.
65 static void ExpandTransitionProperty(const StyleTransitionProperty
& aProperty
,
67 switch (aProperty
.tag
) {
68 case StyleTransitionProperty::Tag::Unsupported
:
70 case StyleTransitionProperty::Tag::Custom
: {
71 AnimatedPropertyID
property(aProperty
.AsCustom().AsAtom());
75 case StyleTransitionProperty::Tag::NonCustom
: {
76 nsCSSPropertyID id
= nsCSSPropertyID(aProperty
.AsNonCustom()._0
);
77 if (nsCSSProps::IsShorthand(id
)) {
78 CSSPROPS_FOR_SHORTHAND_SUBPROPERTIES(subprop
, id
,
79 CSSEnabledState::ForAllContent
) {
80 AnimatedPropertyID
property(*subprop
);
84 AnimatedPropertyID
property(id
);
92 bool nsTransitionManager::DoUpdateTransitions(
93 const nsStyleUIReset
& aStyle
, dom::Element
* aElement
,
94 PseudoStyleType aPseudoType
, CSSTransitionCollection
*& aElementTransitions
,
95 const ComputedStyle
& aOldStyle
, const ComputedStyle
& aNewStyle
) {
96 MOZ_ASSERT(!aElementTransitions
|| &aElementTransitions
->mElement
== aElement
,
99 // Per http://lists.w3.org/Archives/Public/www-style/2009Aug/0109.html
100 // I'll consider only the transitions from the number of items in
101 // 'transition-property' on down, and later ones will override earlier
102 // ones (tracked using |propertiesChecked|).
103 bool startedAny
= false;
104 AnimatedPropertyIDSet propertiesChecked
;
105 for (uint32_t i
= aStyle
.mTransitionPropertyCount
; i
--;) {
106 const float delay
= aStyle
.GetTransitionDelay(i
).ToMilliseconds();
108 // The spec says a negative duration is treated as zero.
109 const float duration
=
110 std::max(aStyle
.GetTransitionDuration(i
).ToMilliseconds(), 0.0f
);
112 // If the combined duration of this transition is 0 or less we won't start a
113 // transition, we can avoid even looking at transition-property if we're the
115 if (i
== 0 && delay
+ duration
<= 0.0f
) {
119 const auto behavior
= aStyle
.GetTransitionBehavior(i
);
120 ExpandTransitionProperty(aStyle
.GetTransitionProperty(i
),
121 [&](const AnimatedPropertyID
& aProperty
) {
122 // We might have something to transition. See if
123 // any of the properties in question changed and
125 startedAny
|= ConsiderInitiatingTransition(
126 aProperty
, aStyle
, i
, delay
, duration
,
127 behavior
, aElement
, aPseudoType
,
128 aElementTransitions
, aOldStyle
, aNewStyle
,
133 // Stop any transitions for properties that are no longer in
134 // 'transition-property', including finished transitions.
135 // Also stop any transitions (and remove any finished transitions)
136 // for properties that just changed (and are still in the set of
137 // properties to transition), but for which we didn't just start the
138 // transition. This can happen delay and duration are both zero, or
139 // because the new value is not interpolable.
140 if (aElementTransitions
) {
141 const bool checkProperties
= !aStyle
.GetTransitionProperty(0).IsAll();
142 AnimatedPropertyIDSet allTransitionProperties
;
143 if (checkProperties
) {
144 for (uint32_t i
= aStyle
.mTransitionPropertyCount
; i
-- != 0;) {
145 ExpandTransitionProperty(aStyle
.GetTransitionProperty(i
),
146 [&](const AnimatedPropertyID
& aProperty
) {
147 allTransitionProperties
.AddProperty(
148 aProperty
.ToPhysical(aNewStyle
));
153 OwningCSSTransitionPtrArray
& animations
= aElementTransitions
->mAnimations
;
154 size_t i
= animations
.Length();
155 MOZ_ASSERT(i
!= 0, "empty transitions list?");
156 AnimationValue currentValue
;
159 CSSTransition
* anim
= animations
[i
];
160 const AnimatedPropertyID
& property
= anim
->TransitionProperty();
162 // Properties no longer in `transition-property`.
163 (checkProperties
&& !allTransitionProperties
.HasProperty(property
)) ||
164 // Properties whose computed values changed but for which we did not
165 // start a new transition (because delay and duration are both zero,
166 // or because the new value is not interpolable); a new transition
167 // would have anim->ToValue() matching currentValue.
168 !Servo_ComputedValues_TransitionValueMatches(
169 &aNewStyle
, &property
, anim
->ToValue().mServo
.get())) {
170 // Stop the transition.
171 DoCancelTransition(aElement
, aPseudoType
, aElementTransitions
, i
);
179 static Keyframe
& AppendKeyframe(double aOffset
,
180 const AnimatedPropertyID
& aProperty
,
181 AnimationValue
&& aValue
,
182 nsTArray
<Keyframe
>& aKeyframes
) {
183 Keyframe
& frame
= *aKeyframes
.AppendElement();
184 frame
.mOffset
.emplace(aOffset
);
185 MOZ_ASSERT(aValue
.mServo
);
186 RefPtr
<StyleLockedDeclarationBlock
> decl
=
187 Servo_AnimationValue_Uncompute(aValue
.mServo
).Consume();
188 frame
.mPropertyValues
.AppendElement(
189 PropertyValuePair(aProperty
, std::move(decl
)));
193 static nsTArray
<Keyframe
> GetTransitionKeyframes(
194 const AnimatedPropertyID
& aProperty
, AnimationValue
&& aStartValue
,
195 AnimationValue
&& aEndValue
) {
196 nsTArray
<Keyframe
> keyframes(2);
198 AppendKeyframe(0.0, aProperty
, std::move(aStartValue
), keyframes
);
199 AppendKeyframe(1.0, aProperty
, std::move(aEndValue
), keyframes
);
204 using ReplacedTransitionProperties
=
205 CSSTransition::ReplacedTransitionProperties
;
206 static Maybe
<ReplacedTransitionProperties
> GetReplacedTransitionProperties(
207 const CSSTransition
* aTransition
,
208 const DocumentTimeline
* aTimelineToMatch
) {
209 Maybe
<ReplacedTransitionProperties
> result
;
211 // Transition needs to be currently running on the compositor to be
213 if (!aTransition
|| !aTransition
->HasCurrentEffect() ||
214 !aTransition
->IsRunningOnCompositor() ||
215 aTransition
->GetStartTime().IsNull()) {
219 // Transition needs to be running on the same timeline.
220 if (aTransition
->GetTimeline() != aTimelineToMatch
) {
224 // The transition needs to have a keyframe effect.
225 const KeyframeEffect
* keyframeEffect
=
226 aTransition
->GetEffect() ? aTransition
->GetEffect()->AsKeyframeEffect()
228 if (!keyframeEffect
) {
232 // The keyframe effect needs to be a simple transition of the original
233 // transition property (i.e. not replaced with something else).
234 if (keyframeEffect
->Properties().Length() != 1 ||
235 keyframeEffect
->Properties()[0].mSegments
.Length() != 1 ||
236 keyframeEffect
->Properties()[0].mProperty
!=
237 aTransition
->TransitionProperty()) {
241 const AnimationPropertySegment
& segment
=
242 keyframeEffect
->Properties()[0].mSegments
[0];
244 result
.emplace(ReplacedTransitionProperties(
245 {aTransition
->GetStartTime().Value(), aTransition
->PlaybackRate(),
246 keyframeEffect
->SpecifiedTiming(), segment
.mTimingFunction
,
247 segment
.mFromValue
, segment
.mToValue
}));
252 bool nsTransitionManager::ConsiderInitiatingTransition(
253 const AnimatedPropertyID
& aProperty
, const nsStyleUIReset
& aStyle
,
254 uint32_t aTransitionIndex
, float aDelay
, float aDuration
,
255 mozilla::StyleTransitionBehavior aBehavior
, dom::Element
* aElement
,
256 PseudoStyleType aPseudoType
, CSSTransitionCollection
*& aElementTransitions
,
257 const ComputedStyle
& aOldStyle
, const ComputedStyle
& aNewStyle
,
258 AnimatedPropertyIDSet
& aPropertiesChecked
) {
259 // IsShorthand itself will assert if aProperty is not a property.
260 MOZ_ASSERT(aProperty
.IsCustom() || !nsCSSProps::IsShorthand(aProperty
.mID
),
261 "property out of range");
263 !aElementTransitions
|| &aElementTransitions
->mElement
== aElement
,
266 AnimatedPropertyID property
= aProperty
.ToPhysical(aNewStyle
);
268 // A later item in transition-property already specified a transition for
269 // this property, so we ignore this one.
271 // See http://lists.w3.org/Archives/Public/www-style/2009Aug/0109.html .
272 if (aPropertiesChecked
.HasProperty(property
)) {
276 aPropertiesChecked
.AddProperty(property
);
278 if (aDuration
+ aDelay
<= 0.0f
) {
282 size_t currentIndex
= nsTArray
<KeyframeEffect
>::NoIndex
;
283 const auto* oldTransition
= [&]() -> const CSSTransition
* {
284 if (!aElementTransitions
) {
287 const OwningCSSTransitionPtrArray
& animations
=
288 aElementTransitions
->mAnimations
;
289 for (size_t i
= 0, i_end
= animations
.Length(); i
< i_end
; ++i
) {
290 if (animations
[i
]->TransitionProperty() == property
) {
292 return animations
[i
];
298 // For compositor animations, |aOldStyle| may have out-of-date transition
299 // rules, and it may be equal to the |endValue| of a reversing transition by
300 // accidentally. This causes Servo_ComputedValues_ShouldTransition() returns
301 // an incorrect result. Therefore, we have to recompute the current value if
302 // this transition is running on the compositor, to make sure we create the
303 // transition properly. Here, we precompute the progress and collect the
304 // necessary info, so Servo_ComputedValues_ShouldTransition() could compute
305 // the current value if needed.
306 // FIXME: Bug 1634945. We should use the last value from the compositor as the
308 Maybe
<ReplacedTransitionProperties
> replacedTransitionProperties
;
309 Maybe
<double> progress
;
311 // If this new transition is replacing an existing transition that is
312 // running on the compositor, we store select parameters from the replaced
313 // transition so that later, once all scripts have run, we can update the
314 // start value of the transition using TimeStamp::Now(). This allows us to
315 // avoid a large jump when starting a new transition when the main thread
316 // lags behind the compositor.
318 // Note: We compute this before calling
319 // Servo_ComputedValues_ShouldTransition() so we can reuse it for computing
320 // the current value and setting the replaced transition properties later in
321 // this function. Also, |replacedTransitionProperties| is Nothing() if the
322 // running transition is not on the compositor.
323 const dom::DocumentTimeline
* timeline
= aElement
->OwnerDoc()->Timeline();
324 replacedTransitionProperties
=
325 GetReplacedTransitionProperties(oldTransition
, timeline
);
326 progress
= replacedTransitionProperties
.andThen(
327 [&](const ReplacedTransitionProperties
& aProperties
) {
328 const dom::AnimationTimeline
* timeline
= oldTransition
->GetTimeline();
329 MOZ_ASSERT(timeline
);
330 return CSSTransition::ComputeTransformedProgress(*timeline
,
335 AnimationValue startValue
, endValue
;
336 const StyleShouldTransitionResult result
=
337 Servo_ComputedValues_ShouldTransition(
338 &aOldStyle
, &aNewStyle
, &property
, aBehavior
,
339 oldTransition
? oldTransition
->ToValue().mServo
.get() : nullptr,
340 replacedTransitionProperties
341 ? replacedTransitionProperties
->mFromValue
.mServo
.get()
343 // Note: It's possible to replace the keyframes by Web Animations API,
344 // so we have to pass the mToValue from the keyframe segment, to make
345 // sure this value is aligned with mFromValue.
346 replacedTransitionProperties
347 ? replacedTransitionProperties
->mToValue
.mServo
.get()
349 progress
.ptrOr(nullptr), &startValue
.mServo
, &endValue
.mServo
);
351 // If we got a style change that changed the value to the endpoint
352 // of the currently running transition, we don't want to interrupt
353 // its timing function.
354 // This needs to be before the !shouldAnimate && haveCurrentTransition
355 // case below because we might be close enough to the end of the
356 // transition that the current value rounds to the final value. In
357 // this case, we'll end up with shouldAnimate as false (because
358 // there's no value change), but we need to return early here rather
359 // than cancel the running transition because shouldAnimate is false!
361 // Likewise, if we got a style change that changed the value to the
362 // endpoint of our finished transition, we also don't want to start
363 // a new transition for the reasons described in
364 // https://lists.w3.org/Archives/Public/www-style/2015Jan/0444.html .
365 if (result
.old_transition_value_matches
) {
366 // GetAnimationRule already called RestyleForAnimation.
370 if (!result
.should_animate
) {
372 // We're in the middle of a transition, and just got a non-transition
373 // style change to something that we can't animate. This might happen
374 // because we got a non-transition style change changing to the current
375 // in-progress value (which is particularly easy to cause when we're
376 // currently in the 'transition-delay'). It also might happen because we
377 // just got a style change to a value that can't be interpolated.
378 DoCancelTransition(aElement
, aPseudoType
, aElementTransitions
,
384 AnimationValue startForReversingTest
= startValue
;
385 double reversePortion
= 1.0;
387 // If the new transition reverses an existing one, we'll need to
388 // handle the timing differently.
389 if (oldTransition
&& oldTransition
->HasCurrentEffect() &&
390 oldTransition
->StartForReversingTest() == endValue
) {
391 // Compute the appropriate negative transition-delay such that right
392 // now we'd end up at the current position.
393 double valuePortion
=
394 oldTransition
->CurrentValuePortion() * oldTransition
->ReversePortion() +
395 (1.0 - oldTransition
->ReversePortion());
396 // A timing function with negative y1 (or y2!) might make
397 // valuePortion negative. In this case, we still want to apply our
398 // reversing logic based on relative distances, not make duration
400 if (valuePortion
< 0.0) {
401 valuePortion
= -valuePortion
;
403 // A timing function with y2 (or y1!) greater than one might
404 // advance past its terminal value. It's probably a good idea to
405 // clamp valuePortion to be at most one to preserve the invariant
406 // that a transition will complete within at most its specified
408 if (valuePortion
> 1.0) {
412 // Negative delays are essentially part of the transition
413 // function, so reduce them along with the duration, but don't
414 // reduce positive delays.
415 if (aDelay
< 0.0f
&& std::isfinite(aDelay
)) {
416 aDelay
*= valuePortion
;
419 if (std::isfinite(aDuration
)) {
420 aDuration
*= valuePortion
;
423 startForReversingTest
= oldTransition
->ToValue();
424 reversePortion
= valuePortion
;
427 TimingParams timing
= TimingParamsFromCSSParams(
428 aDuration
, aDelay
, 1.0 /* iteration count */,
429 StyleAnimationDirection::Normal
, StyleAnimationFillMode::Backwards
);
431 const StyleComputedTimingFunction
& tf
=
432 aStyle
.GetTransitionTimingFunction(aTransitionIndex
);
433 if (!tf
.IsLinearKeyword()) {
434 timing
.SetTimingFunction(Some(tf
));
437 RefPtr
<CSSTransition
> transition
= DoCreateTransition(
438 property
, aElement
, aPseudoType
, aNewStyle
, aElementTransitions
,
439 std::move(timing
), std::move(startValue
), std::move(endValue
),
440 std::move(startForReversingTest
), reversePortion
);
445 OwningCSSTransitionPtrArray
& transitions
= aElementTransitions
->mAnimations
;
447 for (size_t i
= 0, i_end
= transitions
.Length(); i
< i_end
; ++i
) {
449 i
== currentIndex
|| transitions
[i
]->TransitionProperty() != property
,
450 "duplicate transitions for property");
454 if (replacedTransitionProperties
) {
455 transition
->SetReplacedTransition(
456 std::move(replacedTransitionProperties
.ref()));
459 transitions
[currentIndex
]->CancelFromStyle(PostRestyleMode::IfNeeded
);
460 oldTransition
= nullptr; // Clear pointer so it doesn't dangle
461 transitions
[currentIndex
] = transition
;
463 // XXX(Bug 1631371) Check if this should use a fallible operation as it
464 // pretended earlier.
465 transitions
.AppendElement(transition
);
468 if (auto* effectSet
= EffectSet::Get(aElement
, aPseudoType
)) {
469 effectSet
->UpdateAnimationGeneration(mPresContext
);
475 already_AddRefed
<CSSTransition
> nsTransitionManager::DoCreateTransition(
476 const AnimatedPropertyID
& aProperty
, dom::Element
* aElement
,
477 PseudoStyleType aPseudoType
, const mozilla::ComputedStyle
& aNewStyle
,
478 CSSTransitionCollection
*& aElementTransitions
, TimingParams
&& aTiming
,
479 AnimationValue
&& aStartValue
, AnimationValue
&& aEndValue
,
480 AnimationValue
&& aStartForReversingTest
, double aReversePortion
) {
481 dom::DocumentTimeline
* timeline
= aElement
->OwnerDoc()->Timeline();
482 KeyframeEffectParams effectOptions
;
483 auto keyframeEffect
= MakeRefPtr
<KeyframeEffect
>(
484 aElement
->OwnerDoc(), OwningAnimationTarget(aElement
, aPseudoType
),
485 std::move(aTiming
), effectOptions
);
487 keyframeEffect
->SetKeyframes(
488 GetTransitionKeyframes(aProperty
, std::move(aStartValue
),
489 std::move(aEndValue
)),
490 &aNewStyle
, timeline
);
492 if (NS_WARN_IF(MOZ_UNLIKELY(!keyframeEffect
->IsValidTransition()))) {
496 auto animation
= MakeRefPtr
<CSSTransition
>(
497 mPresContext
->Document()->GetScopeObject(), aProperty
);
498 animation
->SetOwningElement(OwningElementRef(*aElement
, aPseudoType
));
499 animation
->SetTimelineNoUpdate(timeline
);
500 animation
->SetCreationSequence(
501 mPresContext
->RestyleManager()->GetAnimationGeneration());
502 animation
->SetEffectFromStyle(keyframeEffect
);
503 animation
->SetReverseParameters(std::move(aStartForReversingTest
),
505 animation
->PlayFromStyle();
507 if (!aElementTransitions
) {
508 aElementTransitions
=
509 &aElement
->EnsureAnimationData().EnsureTransitionCollection(
510 *aElement
, aPseudoType
);
511 if (!aElementTransitions
->isInList()) {
512 AddElementCollection(aElementTransitions
);
515 return animation
.forget();
518 void nsTransitionManager::DoCancelTransition(
519 dom::Element
* aElement
, PseudoStyleType aPseudoType
,
520 CSSTransitionCollection
*& aElementTransitions
, size_t aIndex
) {
521 MOZ_ASSERT(aElementTransitions
);
522 OwningCSSTransitionPtrArray
& transitions
= aElementTransitions
->mAnimations
;
523 CSSTransition
* transition
= transitions
[aIndex
];
525 if (transition
->HasCurrentEffect()) {
526 if (auto* effectSet
= EffectSet::Get(aElement
, aPseudoType
)) {
527 effectSet
->UpdateAnimationGeneration(mPresContext
);
530 transition
->CancelFromStyle(PostRestyleMode::IfNeeded
);
531 transitions
.RemoveElementAt(aIndex
);
533 if (transitions
.IsEmpty()) {
534 aElementTransitions
->Destroy();
535 // |aElementTransitions| is now a dangling pointer!
536 aElementTransitions
= nullptr;