Bug 1692937 [wpt PR 27636] - new parameter --include-file for wptrunner, a=testonly
[gecko.git] / layout / style / nsTransitionManager.cpp
blob38919b6e45eebe194151a4563dfc75140d2711a4
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"
23 #include "nsIFrame.h"
24 #include "Layers.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) {
46 return false;
49 aAnimationValue.mServo =
50 Servo_ComputedValues_ExtractAnimationValue(&aComputedStyle, aProperty)
51 .Consume();
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.
61 return false;
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,
75 "Element mismatch");
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) {
87 continue;
90 nsCSSPropertyID property = aDisp.GetTransitionProperty(i);
91 if (property == eCSSPropertyExtra_no_properties ||
92 property == eCSSPropertyExtra_variable ||
93 property == eCSSProperty_UNKNOWN) {
94 // Nothing to do.
95 continue;
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)) {
107 continue;
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);
120 } else {
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);
162 } else {
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;
173 do {
174 --i;
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);
190 if (effectSet) {
191 effectSet->UpdateAnimationGeneration(mPresContext);
194 anim->CancelFromStyle(PostRestyleMode::IfNeeded);
195 animations.RemoveElementAt(i);
197 } while (i != 0);
199 if (animations.IsEmpty()) {
200 aElementTransitions->Destroy();
201 aElementTransitions = nullptr;
205 return startedAny;
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)));
218 return frame;
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);
229 return 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
242 // replaceable.
243 if (!aTransition || !aTransition->HasCurrentEffect() ||
244 !aTransition->IsRunningOnCompositor() ||
245 aTransition->GetStartTime().IsNull()) {
246 return result;
249 // Transition needs to be running on the same timeline.
250 if (aTransition->GetTimeline() != aTimelineToMatch) {
251 return result;
254 // The transition needs to have a keyframe effect.
255 const KeyframeEffect* keyframeEffect =
256 aTransition->GetEffect() ? aTransition->GetEffect()->AsKeyframeEffect()
257 : nullptr;
258 if (!keyframeEffect) {
259 return result;
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()) {
268 return result;
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}));
279 return result;
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");
290 NS_ASSERTION(
291 !aElementTransitions || aElementTransitions->mElement == aElement,
292 "Element mismatch");
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)) {
301 return false;
304 aPropertiesChecked.AddProperty(aProperty);
306 if (!IsTransitionable(aProperty)) {
307 return false;
310 float delay = aStyleDisplay.GetTransitionDelay(transitionIdx);
312 // The spec says a negative duration is treated as zero.
313 float duration =
314 std::max(aStyleDisplay.GetTransitionDuration(transitionIdx), 0.0f);
316 // If the combined duration of this transition is 0 or less don't start a
317 // transition.
318 if (delay + duration <= 0.0f) {
319 return false;
322 dom::DocumentTimeline* timeline = aElement->OwnerDoc()->Timeline();
324 AnimationValue startValue, endValue;
325 bool haveValues =
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;
342 currentIndex = i;
343 oldTransition = animations[i];
344 break;
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.
366 return false;
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);
383 if (effectSet) {
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.
394 return false;
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
413 // negative.
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
421 // time.
422 if (valuePortion > 1.0) {
423 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.
429 if (delay < 0.0f) {
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)),
457 &aNewStyle);
459 if (NS_WARN_IF(MOZ_UNLIKELY(!keyframeEffect->IsValidTransition()))) {
460 return false;
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),
471 reversePortion);
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");
482 return false;
485 if (createdCollection) {
486 AddElementCollection(aElementTransitions);
490 OwningCSSTransitionPtrArray& animations = aElementTransitions->mAnimations;
491 #ifdef DEBUG
492 for (size_t i = 0, i_end = animations.Length(); i < i_end; ++i) {
493 MOZ_ASSERT(
494 i == currentIndex || animations[i]->TransitionProperty() != aProperty,
495 "duplicate transitions for property");
497 #endif
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;
515 } else {
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);
522 if (effectSet) {
523 effectSet->UpdateAnimationGeneration(mPresContext);
526 return true;