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/ElementAnimationData.h"
19 #include "mozilla/EventDispatcher.h"
20 #include "mozilla/ServoBindings.h"
21 #include "mozilla/StyleAnimationValue.h"
22 #include "mozilla/dom/DocumentTimeline.h"
23 #include "mozilla/dom/Element.h"
25 #include "nsCSSProps.h"
26 #include "nsCSSPseudoElements.h"
27 #include "nsDisplayList.h"
28 #include "nsRFPService.h"
29 #include "nsStyleChangeList.h"
30 #include "mozilla/RestyleManager.h"
32 using mozilla::dom::CSSTransition
;
33 using mozilla::dom::DocumentTimeline
;
34 using mozilla::dom::KeyframeEffect
;
36 using namespace mozilla
;
37 using namespace mozilla::css
;
39 static inline bool ExtractNonDiscreteComputedValue(
40 nsCSSPropertyID aProperty
, const ComputedStyle
& aComputedStyle
,
41 AnimationValue
& aAnimationValue
) {
42 if (Servo_Property_IsDiscreteAnimatable(aProperty
) &&
43 aProperty
!= eCSSProperty_visibility
) {
47 aAnimationValue
.mServo
=
48 Servo_ComputedValues_ExtractAnimationValue(&aComputedStyle
, aProperty
)
50 return !!aAnimationValue
.mServo
;
53 bool nsTransitionManager::UpdateTransitions(dom::Element
* aElement
,
54 PseudoStyleType aPseudoType
,
55 const ComputedStyle
& aOldStyle
,
56 const ComputedStyle
& aNewStyle
) {
57 if (mPresContext
->Medium() == nsGkAtoms::print
) {
58 // For print or print preview, ignore transitions.
62 MOZ_ASSERT(mPresContext
->IsDynamic());
63 if (aNewStyle
.StyleDisplay()->mDisplay
== StyleDisplay::None
) {
64 StopAnimationsForElement(aElement
, aPseudoType
);
68 auto* collection
= CSSTransitionCollection::Get(aElement
, aPseudoType
);
69 return DoUpdateTransitions(*aNewStyle
.StyleUIReset(), aElement
, aPseudoType
,
70 collection
, aOldStyle
, aNewStyle
);
73 // This function expands the shorthands and "all" keyword specified in
74 // transition-property, and then execute |aHandler| on the expanded longhand.
75 // |aHandler| should be a lamda function which accepts nsCSSPropertyID.
77 static void ExpandTransitionProperty(nsCSSPropertyID aProperty
, T aHandler
) {
78 if (aProperty
== eCSSPropertyExtra_no_properties
||
79 aProperty
== eCSSPropertyExtra_variable
||
80 aProperty
== eCSSProperty_UNKNOWN
) {
85 // FIXME(emilio): This should probably just use the "all" shorthand id, and we
86 // should probably remove eCSSPropertyExtra_all_properties.
87 if (aProperty
== eCSSPropertyExtra_all_properties
) {
88 for (nsCSSPropertyID p
= nsCSSPropertyID(0);
89 p
< eCSSProperty_COUNT_no_shorthands
; p
= nsCSSPropertyID(p
+ 1)) {
90 if (!nsCSSProps::IsEnabled(p
, CSSEnabledState::ForAllContent
)) {
95 } else if (nsCSSProps::IsShorthand(aProperty
)) {
96 CSSPROPS_FOR_SHORTHAND_SUBPROPERTIES(subprop
, aProperty
,
97 CSSEnabledState::ForAllContent
) {
105 bool nsTransitionManager::DoUpdateTransitions(
106 const nsStyleUIReset
& aStyle
, dom::Element
* aElement
,
107 PseudoStyleType aPseudoType
, CSSTransitionCollection
*& aElementTransitions
,
108 const ComputedStyle
& aOldStyle
, const ComputedStyle
& aNewStyle
) {
109 MOZ_ASSERT(!aElementTransitions
|| &aElementTransitions
->mElement
== aElement
,
112 // Per http://lists.w3.org/Archives/Public/www-style/2009Aug/0109.html
113 // I'll consider only the transitions from the number of items in
114 // 'transition-property' on down, and later ones will override earlier
115 // ones (tracked using |propertiesChecked|).
116 bool startedAny
= false;
117 nsCSSPropertyIDSet propertiesChecked
;
118 for (uint32_t i
= aStyle
.mTransitionPropertyCount
; i
--;) {
119 // We're not going to look at any further transitions, so we can just avoid
120 // looking at this if we know it will not start any transitions.
121 if (i
== 0 && aStyle
.GetTransitionCombinedDuration(i
).seconds
<= 0.0f
) {
125 ExpandTransitionProperty(
126 aStyle
.GetTransitionProperty(i
), [&](nsCSSPropertyID aProperty
) {
127 // We might have something to transition. See if any of the
128 // properties in question changed and are animatable.
129 startedAny
|= ConsiderInitiatingTransition(
130 aProperty
, aStyle
, i
, aElement
, aPseudoType
, aElementTransitions
,
131 aOldStyle
, aNewStyle
, propertiesChecked
);
135 // Stop any transitions for properties that are no longer in
136 // 'transition-property', including finished transitions.
137 // Also stop any transitions (and remove any finished transitions)
138 // for properties that just changed (and are still in the set of
139 // properties to transition), but for which we didn't just start the
140 // transition. This can happen delay and duration are both zero, or
141 // because the new value is not interpolable.
142 if (aElementTransitions
) {
143 bool checkProperties
=
144 aStyle
.GetTransitionProperty(0) != eCSSPropertyExtra_all_properties
;
145 nsCSSPropertyIDSet allTransitionProperties
;
146 if (checkProperties
) {
147 for (uint32_t i
= aStyle
.mTransitionPropertyCount
; i
-- != 0;) {
148 ExpandTransitionProperty(
149 aStyle
.GetTransitionProperty(i
), [&](nsCSSPropertyID aProperty
) {
150 allTransitionProperties
.AddProperty(
151 nsCSSProps::Physicalize(aProperty
, aNewStyle
));
156 OwningCSSTransitionPtrArray
& animations
= aElementTransitions
->mAnimations
;
157 size_t i
= animations
.Length();
158 MOZ_ASSERT(i
!= 0, "empty transitions list?");
159 AnimationValue currentValue
;
162 CSSTransition
* anim
= animations
[i
];
163 const nsCSSPropertyID property
= anim
->TransitionProperty();
165 // Properties no longer in `transition-property`.
166 (checkProperties
&& !allTransitionProperties
.HasProperty(property
)) ||
167 // Properties whose computed values changed but for which we did not
168 // start a new transition (because delay and duration are both zero,
169 // or because the new value is not interpolable); a new transition
170 // would have anim->ToValue() matching currentValue.
171 !ExtractNonDiscreteComputedValue(property
, aNewStyle
, currentValue
) ||
172 currentValue
!= anim
->ToValue()) {
173 // Stop the transition.
174 DoCancelTransition(aElement
, aPseudoType
, aElementTransitions
, i
);
182 static Keyframe
& AppendKeyframe(double aOffset
, nsCSSPropertyID aProperty
,
183 AnimationValue
&& aValue
,
184 nsTArray
<Keyframe
>& aKeyframes
) {
185 Keyframe
& frame
= *aKeyframes
.AppendElement();
186 frame
.mOffset
.emplace(aOffset
);
187 MOZ_ASSERT(aValue
.mServo
);
188 RefPtr
<StyleLockedDeclarationBlock
> decl
=
189 Servo_AnimationValue_Uncompute(aValue
.mServo
).Consume();
190 frame
.mPropertyValues
.AppendElement(
191 PropertyValuePair(aProperty
, std::move(decl
)));
195 static nsTArray
<Keyframe
> GetTransitionKeyframes(nsCSSPropertyID aProperty
,
196 AnimationValue
&& aStartValue
,
197 AnimationValue
&& aEndValue
) {
198 nsTArray
<Keyframe
> keyframes(2);
200 AppendKeyframe(0.0, aProperty
, std::move(aStartValue
), keyframes
);
201 AppendKeyframe(1.0, aProperty
, std::move(aEndValue
), keyframes
);
206 static bool IsTransitionable(nsCSSPropertyID aProperty
) {
207 return Servo_Property_IsTransitionable(aProperty
);
210 static Maybe
<CSSTransition::ReplacedTransitionProperties
>
211 GetReplacedTransitionProperties(const CSSTransition
* aTransition
,
212 const DocumentTimeline
* aTimelineToMatch
) {
213 Maybe
<CSSTransition::ReplacedTransitionProperties
> result
;
215 // Transition needs to be currently running on the compositor to be
217 if (!aTransition
|| !aTransition
->HasCurrentEffect() ||
218 !aTransition
->IsRunningOnCompositor() ||
219 aTransition
->GetStartTime().IsNull()) {
223 // Transition needs to be running on the same timeline.
224 if (aTransition
->GetTimeline() != aTimelineToMatch
) {
228 // The transition needs to have a keyframe effect.
229 const KeyframeEffect
* keyframeEffect
=
230 aTransition
->GetEffect() ? aTransition
->GetEffect()->AsKeyframeEffect()
232 if (!keyframeEffect
) {
236 // The keyframe effect needs to be a simple transition of the original
237 // transition property (i.e. not replaced with something else).
238 if (keyframeEffect
->Properties().Length() != 1 ||
239 keyframeEffect
->Properties()[0].mSegments
.Length() != 1 ||
240 keyframeEffect
->Properties()[0].mProperty
!=
241 aTransition
->TransitionProperty()) {
245 const AnimationPropertySegment
& segment
=
246 keyframeEffect
->Properties()[0].mSegments
[0];
248 result
.emplace(CSSTransition::ReplacedTransitionProperties(
249 {aTransition
->GetStartTime().Value(), aTransition
->PlaybackRate(),
250 keyframeEffect
->SpecifiedTiming(), segment
.mTimingFunction
,
251 segment
.mFromValue
, segment
.mToValue
}));
256 bool nsTransitionManager::ConsiderInitiatingTransition(
257 nsCSSPropertyID aProperty
, const nsStyleUIReset
& aStyle
,
258 uint32_t transitionIdx
, dom::Element
* aElement
, PseudoStyleType aPseudoType
,
259 CSSTransitionCollection
*& aElementTransitions
,
260 const ComputedStyle
& aOldStyle
, const ComputedStyle
& aNewStyle
,
261 nsCSSPropertyIDSet
& aPropertiesChecked
) {
262 // IsShorthand itself will assert if aProperty is not a property.
263 MOZ_ASSERT(!nsCSSProps::IsShorthand(aProperty
), "property out of range");
265 !aElementTransitions
|| &aElementTransitions
->mElement
== aElement
,
268 aProperty
= nsCSSProps::Physicalize(aProperty
, aNewStyle
);
270 // A later item in transition-property already specified a transition for
271 // this property, so we ignore this one.
273 // See http://lists.w3.org/Archives/Public/www-style/2009Aug/0109.html .
274 if (aPropertiesChecked
.HasProperty(aProperty
)) {
278 aPropertiesChecked
.AddProperty(aProperty
);
280 if (!IsTransitionable(aProperty
)) {
284 float delay
= aStyle
.GetTransitionDelay(transitionIdx
).ToMilliseconds();
286 // The spec says a negative duration is treated as zero.
287 float duration
= std::max(
288 aStyle
.GetTransitionDuration(transitionIdx
).ToMilliseconds(), 0.0f
);
290 // If the combined duration of this transition is 0 or less don't start a
292 if (delay
+ duration
<= 0.0f
) {
296 AnimationValue startValue
, endValue
;
298 ExtractNonDiscreteComputedValue(aProperty
, aOldStyle
, startValue
) &&
299 ExtractNonDiscreteComputedValue(aProperty
, aNewStyle
, endValue
);
301 bool haveChange
= startValue
!= endValue
;
303 bool shouldAnimate
= haveValues
&& haveChange
&&
304 startValue
.IsInterpolableWith(aProperty
, endValue
);
306 bool haveCurrentTransition
= false;
307 size_t currentIndex
= nsTArray
<KeyframeEffect
>::NoIndex
;
308 const CSSTransition
* oldTransition
= nullptr;
309 if (aElementTransitions
) {
310 OwningCSSTransitionPtrArray
& animations
= aElementTransitions
->mAnimations
;
311 for (size_t i
= 0, i_end
= animations
.Length(); i
< i_end
; ++i
) {
312 if (animations
[i
]->TransitionProperty() == aProperty
) {
313 haveCurrentTransition
= true;
315 oldTransition
= animations
[i
];
321 // If we got a style change that changed the value to the endpoint
322 // of the currently running transition, we don't want to interrupt
323 // its timing function.
324 // This needs to be before the !shouldAnimate && haveCurrentTransition
325 // case below because we might be close enough to the end of the
326 // transition that the current value rounds to the final value. In
327 // this case, we'll end up with shouldAnimate as false (because
328 // there's no value change), but we need to return early here rather
329 // than cancel the running transition because shouldAnimate is false!
331 // Likewise, if we got a style change that changed the value to the
332 // endpoint of our finished transition, we also don't want to start
333 // a new transition for the reasons described in
334 // https://lists.w3.org/Archives/Public/www-style/2015Jan/0444.html .
335 if (haveCurrentTransition
&& haveValues
&&
336 aElementTransitions
->mAnimations
[currentIndex
]->ToValue() == endValue
) {
337 // GetAnimationRule already called RestyleForAnimation.
341 if (!shouldAnimate
) {
342 if (haveCurrentTransition
) {
343 // We're in the middle of a transition, and just got a non-transition
344 // style change to something that we can't animate. This might happen
345 // because we got a non-transition style change changing to the current
346 // in-progress value (which is particularly easy to cause when we're
347 // currently in the 'transition-delay'). It also might happen because we
348 // just got a style change to a value that can't be interpolated.
349 DoCancelTransition(aElement
, aPseudoType
, aElementTransitions
,
355 AnimationValue startForReversingTest
= startValue
;
356 double reversePortion
= 1.0;
358 // If the new transition reverses an existing one, we'll need to
359 // handle the timing differently.
360 if (haveCurrentTransition
&&
361 aElementTransitions
->mAnimations
[currentIndex
]->HasCurrentEffect() &&
362 oldTransition
&& oldTransition
->StartForReversingTest() == endValue
) {
363 // Compute the appropriate negative transition-delay such that right
364 // now we'd end up at the current position.
365 double valuePortion
=
366 oldTransition
->CurrentValuePortion() * oldTransition
->ReversePortion() +
367 (1.0 - oldTransition
->ReversePortion());
368 // A timing function with negative y1 (or y2!) might make
369 // valuePortion negative. In this case, we still want to apply our
370 // reversing logic based on relative distances, not make duration
372 if (valuePortion
< 0.0) {
373 valuePortion
= -valuePortion
;
375 // A timing function with y2 (or y1!) greater than one might
376 // advance past its terminal value. It's probably a good idea to
377 // clamp valuePortion to be at most one to preserve the invariant
378 // that a transition will complete within at most its specified
380 if (valuePortion
> 1.0) {
384 // Negative delays are essentially part of the transition
385 // function, so reduce them along with the duration, but don't
386 // reduce positive delays.
387 if (delay
< 0.0f
&& std::isfinite(delay
)) {
388 delay
*= valuePortion
;
391 if (std::isfinite(duration
)) {
392 duration
*= valuePortion
;
395 startForReversingTest
= oldTransition
->ToValue();
396 reversePortion
= valuePortion
;
399 TimingParams timing
= TimingParamsFromCSSParams(
400 duration
, delay
, 1.0 /* iteration count */,
401 dom::PlaybackDirection::Normal
, dom::FillMode::Backwards
);
403 const StyleComputedTimingFunction
& tf
=
404 aStyle
.GetTransitionTimingFunction(transitionIdx
);
405 if (!tf
.IsLinearKeyword()) {
406 timing
.SetTimingFunction(Some(tf
));
409 RefPtr
<CSSTransition
> transition
= DoCreateTransition(
410 aProperty
, aElement
, aPseudoType
, aNewStyle
, aElementTransitions
,
411 std::move(timing
), std::move(startValue
), std::move(endValue
),
412 std::move(startForReversingTest
), reversePortion
);
417 OwningCSSTransitionPtrArray
& transitions
= aElementTransitions
->mAnimations
;
419 for (size_t i
= 0, i_end
= transitions
.Length(); i
< i_end
; ++i
) {
421 i
== currentIndex
|| transitions
[i
]->TransitionProperty() != aProperty
,
422 "duplicate transitions for property");
425 if (haveCurrentTransition
) {
426 // If this new transition is replacing an existing transition that is
427 // running on the compositor, we store select parameters from the replaced
428 // transition so that later, once all scripts have run, we can update the
429 // start value of the transition using TimeStamp::Now(). This allows us to
430 // avoid a large jump when starting a new transition when the main thread
431 // lags behind the compositor.
432 const dom::DocumentTimeline
* timeline
= aElement
->OwnerDoc()->Timeline();
433 auto replacedTransitionProperties
=
434 GetReplacedTransitionProperties(oldTransition
, timeline
);
435 if (replacedTransitionProperties
) {
436 transition
->SetReplacedTransition(
437 std::move(replacedTransitionProperties
.ref()));
440 transitions
[currentIndex
]->CancelFromStyle(PostRestyleMode::IfNeeded
);
441 oldTransition
= nullptr; // Clear pointer so it doesn't dangle
442 transitions
[currentIndex
] = transition
;
444 // XXX(Bug 1631371) Check if this should use a fallible operation as it
445 // pretended earlier.
446 transitions
.AppendElement(transition
);
449 if (auto* effectSet
= EffectSet::Get(aElement
, aPseudoType
)) {
450 effectSet
->UpdateAnimationGeneration(mPresContext
);
456 already_AddRefed
<CSSTransition
> nsTransitionManager::DoCreateTransition(
457 nsCSSPropertyID aProperty
, dom::Element
* aElement
,
458 PseudoStyleType aPseudoType
, const mozilla::ComputedStyle
& aNewStyle
,
459 CSSTransitionCollection
*& aElementTransitions
, TimingParams
&& aTiming
,
460 AnimationValue
&& aStartValue
, AnimationValue
&& aEndValue
,
461 AnimationValue
&& aStartForReversingTest
, double aReversePortion
) {
462 dom::DocumentTimeline
* timeline
= aElement
->OwnerDoc()->Timeline();
463 KeyframeEffectParams effectOptions
;
464 RefPtr
<KeyframeEffect
> keyframeEffect
= new KeyframeEffect(
465 aElement
->OwnerDoc(), OwningAnimationTarget(aElement
, aPseudoType
),
466 std::move(aTiming
), effectOptions
);
468 keyframeEffect
->SetKeyframes(
469 GetTransitionKeyframes(aProperty
, std::move(aStartValue
),
470 std::move(aEndValue
)),
471 &aNewStyle
, timeline
);
473 if (NS_WARN_IF(MOZ_UNLIKELY(!keyframeEffect
->IsValidTransition()))) {
477 RefPtr
<CSSTransition
> animation
=
478 new CSSTransition(mPresContext
->Document()->GetScopeObject());
479 animation
->SetOwningElement(OwningElementRef(*aElement
, aPseudoType
));
480 animation
->SetTimelineNoUpdate(timeline
);
481 animation
->SetCreationSequence(
482 mPresContext
->RestyleManager()->GetAnimationGeneration());
483 animation
->SetEffectFromStyle(keyframeEffect
);
484 animation
->SetReverseParameters(std::move(aStartForReversingTest
),
486 animation
->PlayFromStyle();
488 if (!aElementTransitions
) {
489 aElementTransitions
=
490 &aElement
->EnsureAnimationData().EnsureTransitionCollection(
491 *aElement
, aPseudoType
);
492 if (!aElementTransitions
->isInList()) {
493 AddElementCollection(aElementTransitions
);
496 return animation
.forget();
499 void nsTransitionManager::DoCancelTransition(
500 dom::Element
* aElement
, PseudoStyleType aPseudoType
,
501 CSSTransitionCollection
*& aElementTransitions
, size_t aIndex
) {
502 MOZ_ASSERT(aElementTransitions
);
503 OwningCSSTransitionPtrArray
& transitions
= aElementTransitions
->mAnimations
;
504 CSSTransition
* transition
= transitions
[aIndex
];
506 if (transition
->HasCurrentEffect()) {
507 if (auto* effectSet
= EffectSet::Get(aElement
, aPseudoType
)) {
508 effectSet
->UpdateAnimationGeneration(mPresContext
);
511 transition
->CancelFromStyle(PostRestyleMode::IfNeeded
);
512 transitions
.RemoveElementAt(aIndex
);
514 if (transitions
.IsEmpty()) {
515 aElementTransitions
->Destroy();
516 // |aElementTransitions| is now a dangling pointer!
517 aElementTransitions
= nullptr;