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 "mozilla/ComputedStyle.h"
15 #include "mozilla/MemoryReporting.h"
16 #include "nsCSSPropertyIDSet.h"
17 #include "mozilla/EffectSet.h"
18 #include "mozilla/EventDispatcher.h"
19 #include "mozilla/ServoBindings.h"
20 #include "mozilla/StyleAnimationValue.h"
21 #include "mozilla/dom/DocumentTimeline.h"
22 #include "mozilla/dom/Element.h"
25 #include "FrameLayerBuilder.h"
26 #include "nsCSSProps.h"
27 #include "nsCSSPseudoElements.h"
28 #include "nsDisplayList.h"
29 #include "nsRFPService.h"
30 #include "nsStyleChangeList.h"
31 #include "mozilla/RestyleManager.h"
33 using mozilla::dom::Animation
;
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 static inline bool ExtractNonDiscreteComputedValue(
42 nsCSSPropertyID aProperty
, const ComputedStyle
& aComputedStyle
,
43 AnimationValue
& aAnimationValue
) {
44 if (Servo_Property_IsDiscreteAnimatable(aProperty
) &&
45 aProperty
!= eCSSProperty_visibility
) {
49 aAnimationValue
.mServo
=
50 Servo_ComputedValues_ExtractAnimationValue(&aComputedStyle
, aProperty
)
52 return !!aAnimationValue
.mServo
;
55 bool nsTransitionManager::UpdateTransitions(dom::Element
* aElement
,
56 PseudoStyleType aPseudoType
,
57 const ComputedStyle
& aOldStyle
,
58 const ComputedStyle
& aNewStyle
) {
59 if (!mPresContext
->IsDynamic()) {
60 // For print or print preview, ignore transitions.
64 CSSTransitionCollection
* collection
=
65 CSSTransitionCollection::GetAnimationCollection(aElement
, aPseudoType
);
66 return DoUpdateTransitions(*aNewStyle
.StyleDisplay(), aElement
, aPseudoType
,
67 collection
, aOldStyle
, aNewStyle
);
70 bool nsTransitionManager::DoUpdateTransitions(
71 const nsStyleDisplay
& aDisp
, dom::Element
* aElement
,
72 PseudoStyleType aPseudoType
, CSSTransitionCollection
*& aElementTransitions
,
73 const ComputedStyle
& aOldStyle
, const ComputedStyle
& aNewStyle
) {
74 MOZ_ASSERT(!aElementTransitions
|| aElementTransitions
->mElement
== aElement
,
77 // Per http://lists.w3.org/Archives/Public/www-style/2009Aug/0109.html
78 // I'll consider only the transitions from the number of items in
79 // 'transition-property' on down, and later ones will override earlier
80 // ones (tracked using |propertiesChecked|).
81 bool startedAny
= false;
82 nsCSSPropertyIDSet propertiesChecked
;
83 for (uint32_t i
= aDisp
.mTransitionPropertyCount
; i
--;) {
84 // We're not going to look at any further transitions, so we can just avoid
85 // looking at this if we know it will not start any transitions.
86 if (i
== 0 && aDisp
.GetTransitionCombinedDuration(i
) <= 0.0f
) {
90 nsCSSPropertyID property
= aDisp
.GetTransitionProperty(i
);
91 if (property
== eCSSPropertyExtra_no_properties
||
92 property
== eCSSPropertyExtra_variable
||
93 property
== eCSSProperty_UNKNOWN
) {
97 // We might have something to transition. See if any of the
98 // properties in question changed and are animatable.
99 // FIXME: Would be good to find a way to share code between this
100 // interpretation of transition-property and the one below.
101 // FIXME(emilio): This should probably just use the "all" shorthand id, and
102 // we should probably remove eCSSPropertyExtra_all_properties.
103 if (property
== eCSSPropertyExtra_all_properties
) {
104 for (nsCSSPropertyID p
= nsCSSPropertyID(0);
105 p
< eCSSProperty_COUNT_no_shorthands
; p
= nsCSSPropertyID(p
+ 1)) {
106 if (!nsCSSProps::IsEnabled(p
, CSSEnabledState::ForAllContent
)) {
109 startedAny
|= ConsiderInitiatingTransition(
110 p
, aDisp
, i
, aElement
, aPseudoType
, aElementTransitions
, aOldStyle
,
111 aNewStyle
, propertiesChecked
);
113 } else if (nsCSSProps::IsShorthand(property
)) {
114 CSSPROPS_FOR_SHORTHAND_SUBPROPERTIES(subprop
, property
,
115 CSSEnabledState::ForAllContent
) {
116 startedAny
|= ConsiderInitiatingTransition(
117 *subprop
, aDisp
, i
, aElement
, aPseudoType
, aElementTransitions
,
118 aOldStyle
, aNewStyle
, propertiesChecked
);
121 startedAny
|= ConsiderInitiatingTransition(
122 property
, aDisp
, i
, aElement
, aPseudoType
, aElementTransitions
,
123 aOldStyle
, aNewStyle
, propertiesChecked
);
127 // Stop any transitions for properties that are no longer in
128 // 'transition-property', including finished transitions.
129 // Also stop any transitions (and remove any finished transitions)
130 // for properties that just changed (and are still in the set of
131 // properties to transition), but for which we didn't just start the
132 // transition. This can happen delay and duration are both zero, or
133 // because the new value is not interpolable.
134 // Note that we also do the latter set of work in
135 // nsTransitionManager::PruneCompletedTransitions.
136 if (aElementTransitions
) {
137 bool checkProperties
=
138 aDisp
.GetTransitionProperty(0) != eCSSPropertyExtra_all_properties
;
139 nsCSSPropertyIDSet allTransitionProperties
;
140 if (checkProperties
) {
141 for (uint32_t i
= aDisp
.mTransitionPropertyCount
; i
-- != 0;) {
142 // FIXME: Would be good to find a way to share code between this
143 // interpretation of transition-property and the one above.
144 nsCSSPropertyID property
= aDisp
.GetTransitionProperty(i
);
145 if (property
== eCSSPropertyExtra_no_properties
||
146 property
== eCSSPropertyExtra_variable
||
147 property
== eCSSProperty_UNKNOWN
) {
148 // Nothing to do, but need to exclude this from cases below.
149 } else if (property
== eCSSPropertyExtra_all_properties
) {
150 for (nsCSSPropertyID p
= nsCSSPropertyID(0);
151 p
< eCSSProperty_COUNT_no_shorthands
;
152 p
= nsCSSPropertyID(p
+ 1)) {
153 allTransitionProperties
.AddProperty(
154 nsCSSProps::Physicalize(p
, aNewStyle
));
156 } else if (nsCSSProps::IsShorthand(property
)) {
157 CSSPROPS_FOR_SHORTHAND_SUBPROPERTIES(subprop
, property
,
158 CSSEnabledState::ForAllContent
) {
159 auto p
= nsCSSProps::Physicalize(*subprop
, aNewStyle
);
160 allTransitionProperties
.AddProperty(p
);
163 allTransitionProperties
.AddProperty(
164 nsCSSProps::Physicalize(property
, aNewStyle
));
169 OwningCSSTransitionPtrArray
& animations
= aElementTransitions
->mAnimations
;
170 size_t i
= animations
.Length();
171 MOZ_ASSERT(i
!= 0, "empty transitions list?");
172 AnimationValue currentValue
;
175 CSSTransition
* anim
= animations
[i
];
176 // properties no longer in 'transition-property'
177 if ((checkProperties
&&
178 !allTransitionProperties
.HasProperty(anim
->TransitionProperty())) ||
179 // properties whose computed values changed but for which we
180 // did not start a new transition (because delay and
181 // duration are both zero, or because the new value is not
182 // interpolable); a new transition would have anim->ToValue()
183 // matching currentValue
184 !ExtractNonDiscreteComputedValue(anim
->TransitionProperty(),
185 aNewStyle
, currentValue
) ||
186 currentValue
!= anim
->ToValue()) {
187 // stop the transition
188 if (anim
->HasCurrentEffect()) {
189 EffectSet
* effectSet
= EffectSet::GetEffectSet(aElement
, aPseudoType
);
191 effectSet
->UpdateAnimationGeneration(mPresContext
);
194 anim
->CancelFromStyle(PostRestyleMode::IfNeeded
);
195 animations
.RemoveElementAt(i
);
199 if (animations
.IsEmpty()) {
200 aElementTransitions
->Destroy();
201 aElementTransitions
= nullptr;
208 static Keyframe
& AppendKeyframe(double aOffset
, nsCSSPropertyID aProperty
,
209 AnimationValue
&& aValue
,
210 nsTArray
<Keyframe
>& aKeyframes
) {
211 Keyframe
& frame
= *aKeyframes
.AppendElement();
212 frame
.mOffset
.emplace(aOffset
);
213 MOZ_ASSERT(aValue
.mServo
);
214 RefPtr
<RawServoDeclarationBlock
> decl
=
215 Servo_AnimationValue_Uncompute(aValue
.mServo
).Consume();
216 frame
.mPropertyValues
.AppendElement(
217 PropertyValuePair(aProperty
, std::move(decl
)));
221 static nsTArray
<Keyframe
> GetTransitionKeyframes(nsCSSPropertyID aProperty
,
222 AnimationValue
&& aStartValue
,
223 AnimationValue
&& aEndValue
) {
224 nsTArray
<Keyframe
> keyframes(2);
226 AppendKeyframe(0.0, aProperty
, std::move(aStartValue
), keyframes
);
227 AppendKeyframe(1.0, aProperty
, std::move(aEndValue
), keyframes
);
232 static bool IsTransitionable(nsCSSPropertyID aProperty
) {
233 return Servo_Property_IsTransitionable(aProperty
);
236 static Maybe
<CSSTransition::ReplacedTransitionProperties
>
237 GetReplacedTransitionProperties(const CSSTransition
* aTransition
,
238 const DocumentTimeline
* aTimelineToMatch
) {
239 Maybe
<CSSTransition::ReplacedTransitionProperties
> result
;
241 // Transition needs to be currently running on the compositor to be
243 if (!aTransition
|| !aTransition
->HasCurrentEffect() ||
244 !aTransition
->IsRunningOnCompositor() ||
245 aTransition
->GetStartTime().IsNull()) {
249 // Transition needs to be running on the same timeline.
250 if (aTransition
->GetTimeline() != aTimelineToMatch
) {
254 // The transition needs to have a keyframe effect.
255 const KeyframeEffect
* keyframeEffect
=
256 aTransition
->GetEffect() ? aTransition
->GetEffect()->AsKeyframeEffect()
258 if (!keyframeEffect
) {
262 // The keyframe effect needs to be a simple transition of the original
263 // transition property (i.e. not replaced with something else).
264 if (keyframeEffect
->Properties().Length() != 1 ||
265 keyframeEffect
->Properties()[0].mSegments
.Length() != 1 ||
266 keyframeEffect
->Properties()[0].mProperty
!=
267 aTransition
->TransitionProperty()) {
271 const AnimationPropertySegment
& segment
=
272 keyframeEffect
->Properties()[0].mSegments
[0];
274 result
.emplace(CSSTransition::ReplacedTransitionProperties(
275 {aTransition
->GetStartTime().Value(), aTransition
->PlaybackRate(),
276 keyframeEffect
->SpecifiedTiming(), segment
.mTimingFunction
,
277 segment
.mFromValue
, segment
.mToValue
}));
282 bool nsTransitionManager::ConsiderInitiatingTransition(
283 nsCSSPropertyID aProperty
, const nsStyleDisplay
& aStyleDisplay
,
284 uint32_t transitionIdx
, dom::Element
* aElement
, PseudoStyleType aPseudoType
,
285 CSSTransitionCollection
*& aElementTransitions
,
286 const ComputedStyle
& aOldStyle
, const ComputedStyle
& aNewStyle
,
287 nsCSSPropertyIDSet
& aPropertiesChecked
) {
288 // IsShorthand itself will assert if aProperty is not a property.
289 MOZ_ASSERT(!nsCSSProps::IsShorthand(aProperty
), "property out of range");
291 !aElementTransitions
|| aElementTransitions
->mElement
== aElement
,
294 aProperty
= nsCSSProps::Physicalize(aProperty
, aNewStyle
);
296 // A later item in transition-property already specified a transition for
297 // this property, so we ignore this one.
299 // See http://lists.w3.org/Archives/Public/www-style/2009Aug/0109.html .
300 if (aPropertiesChecked
.HasProperty(aProperty
)) {
304 aPropertiesChecked
.AddProperty(aProperty
);
306 if (!IsTransitionable(aProperty
)) {
310 float delay
= aStyleDisplay
.GetTransitionDelay(transitionIdx
);
312 // The spec says a negative duration is treated as zero.
314 std::max(aStyleDisplay
.GetTransitionDuration(transitionIdx
), 0.0f
);
316 // If the combined duration of this transition is 0 or less don't start a
318 if (delay
+ duration
<= 0.0f
) {
322 dom::DocumentTimeline
* timeline
= aElement
->OwnerDoc()->Timeline();
324 AnimationValue startValue
, endValue
;
326 ExtractNonDiscreteComputedValue(aProperty
, aOldStyle
, startValue
) &&
327 ExtractNonDiscreteComputedValue(aProperty
, aNewStyle
, endValue
);
329 bool haveChange
= startValue
!= endValue
;
331 bool shouldAnimate
= haveValues
&& haveChange
&&
332 startValue
.IsInterpolableWith(aProperty
, endValue
);
334 bool haveCurrentTransition
= false;
335 size_t currentIndex
= nsTArray
<KeyframeEffect
>::NoIndex
;
336 const CSSTransition
* oldTransition
= nullptr;
337 if (aElementTransitions
) {
338 OwningCSSTransitionPtrArray
& animations
= aElementTransitions
->mAnimations
;
339 for (size_t i
= 0, i_end
= animations
.Length(); i
< i_end
; ++i
) {
340 if (animations
[i
]->TransitionProperty() == aProperty
) {
341 haveCurrentTransition
= true;
343 oldTransition
= animations
[i
];
349 // If we got a style change that changed the value to the endpoint
350 // of the currently running transition, we don't want to interrupt
351 // its timing function.
352 // This needs to be before the !shouldAnimate && haveCurrentTransition
353 // case below because we might be close enough to the end of the
354 // transition that the current value rounds to the final value. In
355 // this case, we'll end up with shouldAnimate as false (because
356 // there's no value change), but we need to return early here rather
357 // than cancel the running transition because shouldAnimate is false!
359 // Likewise, if we got a style change that changed the value to the
360 // endpoint of our finished transition, we also don't want to start
361 // a new transition for the reasons described in
362 // https://lists.w3.org/Archives/Public/www-style/2015Jan/0444.html .
363 if (haveCurrentTransition
&& haveValues
&&
364 aElementTransitions
->mAnimations
[currentIndex
]->ToValue() == endValue
) {
365 // GetAnimationRule already called RestyleForAnimation.
369 if (!shouldAnimate
) {
370 if (haveCurrentTransition
) {
371 // We're in the middle of a transition, and just got a non-transition
372 // style change to something that we can't animate. This might happen
373 // because we got a non-transition style change changing to the current
374 // in-progress value (which is particularly easy to cause when we're
375 // currently in the 'transition-delay'). It also might happen because we
376 // just got a style change to a value that can't be interpolated.
377 OwningCSSTransitionPtrArray
& animations
=
378 aElementTransitions
->mAnimations
;
379 animations
[currentIndex
]->CancelFromStyle(PostRestyleMode::IfNeeded
);
380 oldTransition
= nullptr; // Clear pointer so it doesn't dangle
381 animations
.RemoveElementAt(currentIndex
);
382 EffectSet
* effectSet
= EffectSet::GetEffectSet(aElement
, aPseudoType
);
384 effectSet
->UpdateAnimationGeneration(mPresContext
);
387 if (animations
.IsEmpty()) {
388 aElementTransitions
->Destroy();
389 // |aElementTransitions| is now a dangling pointer!
390 aElementTransitions
= nullptr;
392 // GetAnimationRule already called RestyleForAnimation.
397 AnimationValue startForReversingTest
= startValue
;
398 double reversePortion
= 1.0;
400 // If the new transition reverses an existing one, we'll need to
401 // handle the timing differently.
402 if (haveCurrentTransition
&&
403 aElementTransitions
->mAnimations
[currentIndex
]->HasCurrentEffect() &&
404 oldTransition
&& oldTransition
->StartForReversingTest() == endValue
) {
405 // Compute the appropriate negative transition-delay such that right
406 // now we'd end up at the current position.
407 double valuePortion
=
408 oldTransition
->CurrentValuePortion() * oldTransition
->ReversePortion() +
409 (1.0 - oldTransition
->ReversePortion());
410 // A timing function with negative y1 (or y2!) might make
411 // valuePortion negative. In this case, we still want to apply our
412 // reversing logic based on relative distances, not make duration
414 if (valuePortion
< 0.0) {
415 valuePortion
= -valuePortion
;
417 // A timing function with y2 (or y1!) greater than one might
418 // advance past its terminal value. It's probably a good idea to
419 // clamp valuePortion to be at most one to preserve the invariant
420 // that a transition will complete within at most its specified
422 if (valuePortion
> 1.0) {
426 // Negative delays are essentially part of the transition
427 // function, so reduce them along with the duration, but don't
428 // reduce positive delays.
430 delay
*= valuePortion
;
433 duration
*= valuePortion
;
435 startForReversingTest
= oldTransition
->ToValue();
436 reversePortion
= valuePortion
;
439 TimingParams timing
= TimingParamsFromCSSParams(
440 duration
, delay
, 1.0 /* iteration count */,
441 dom::PlaybackDirection::Normal
, dom::FillMode::Backwards
);
443 const nsTimingFunction
& tf
=
444 aStyleDisplay
.GetTransitionTimingFunction(transitionIdx
);
445 if (!tf
.IsLinear()) {
446 timing
.SetTimingFunction(Some(ComputedTimingFunction(tf
)));
449 KeyframeEffectParams effectOptions
;
450 RefPtr
<KeyframeEffect
> keyframeEffect
= new KeyframeEffect(
451 aElement
->OwnerDoc(), OwningAnimationTarget(aElement
, aPseudoType
),
452 std::move(timing
), effectOptions
);
454 keyframeEffect
->SetKeyframes(
455 GetTransitionKeyframes(aProperty
, std::move(startValue
),
456 std::move(endValue
)),
459 if (NS_WARN_IF(MOZ_UNLIKELY(!keyframeEffect
->IsValidTransition()))) {
463 RefPtr
<CSSTransition
> animation
=
464 new CSSTransition(mPresContext
->Document()->GetScopeObject());
465 animation
->SetOwningElement(OwningElementRef(*aElement
, aPseudoType
));
466 animation
->SetTimelineNoUpdate(timeline
);
467 animation
->SetCreationSequence(
468 mPresContext
->RestyleManager()->GetAnimationGeneration());
469 animation
->SetEffectFromStyle(keyframeEffect
);
470 animation
->SetReverseParameters(std::move(startForReversingTest
),
472 animation
->PlayFromStyle();
474 if (!aElementTransitions
) {
475 bool createdCollection
= false;
476 aElementTransitions
=
477 CSSTransitionCollection::GetOrCreateAnimationCollection(
478 aElement
, aPseudoType
, &createdCollection
);
479 if (!aElementTransitions
) {
480 MOZ_ASSERT(!createdCollection
, "outparam should agree with return value");
481 NS_WARNING("allocating collection failed");
485 if (createdCollection
) {
486 AddElementCollection(aElementTransitions
);
490 OwningCSSTransitionPtrArray
& animations
= aElementTransitions
->mAnimations
;
492 for (size_t i
= 0, i_end
= animations
.Length(); i
< i_end
; ++i
) {
494 i
== currentIndex
|| animations
[i
]->TransitionProperty() != aProperty
,
495 "duplicate transitions for property");
498 if (haveCurrentTransition
) {
499 // If this new transition is replacing an existing transition that is
500 // running on the compositor, we store select parameters from the replaced
501 // transition so that later, once all scripts have run, we can update the
502 // start value of the transition using TimeStamp::Now(). This allows us to
503 // avoid a large jump when starting a new transition when the main thread
504 // lags behind the compositor.
505 auto replacedTransitionProperties
=
506 GetReplacedTransitionProperties(oldTransition
, timeline
);
507 if (replacedTransitionProperties
) {
508 animation
->SetReplacedTransition(
509 std::move(replacedTransitionProperties
.ref()));
512 animations
[currentIndex
]->CancelFromStyle(PostRestyleMode::IfNeeded
);
513 oldTransition
= nullptr; // Clear pointer so it doesn't dangle
514 animations
[currentIndex
] = animation
;
516 // XXX(Bug 1631371) Check if this should use a fallible operation as it
517 // pretended earlier.
518 animations
.AppendElement(animation
);
521 EffectSet
* effectSet
= EffectSet::GetEffectSet(aElement
, aPseudoType
);
523 effectSet
->UpdateAnimationGeneration(mPresContext
);