Backed out changeset 1b14354719c0 (bug 1895254) for causing bustages on NavigationTra...
[gecko.git] / layout / style / nsTransitionManager.cpp
blobb9cb80764ecbcac3a78534f08b77f3709cbdff64
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"
26 #include "nsIFrame.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.
47 return false;
50 MOZ_ASSERT(mPresContext->IsDynamic());
51 if (aNewStyle.StyleDisplay()->mDisplay == StyleDisplay::None) {
52 StopAnimationsForElement(aElement, aPseudoType);
53 return false;
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.
64 template <typename T>
65 static void ExpandTransitionProperty(const StyleTransitionProperty& aProperty,
66 T aHandler) {
67 switch (aProperty.tag) {
68 case StyleTransitionProperty::Tag::Unsupported:
69 break;
70 case StyleTransitionProperty::Tag::Custom: {
71 AnimatedPropertyID property(aProperty.AsCustom().AsAtom());
72 aHandler(property);
73 break;
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);
81 aHandler(property);
83 } else {
84 AnimatedPropertyID property(id);
85 aHandler(property);
87 break;
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,
97 "Element mismatch");
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
114 // last one.
115 if (i == 0 && delay + duration <= 0.0f) {
116 continue;
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
124 // are animatable.
125 startedAny |= ConsiderInitiatingTransition(
126 aProperty, aStyle, i, delay, duration,
127 behavior, aElement, aPseudoType,
128 aElementTransitions, aOldStyle, aNewStyle,
129 propertiesChecked);
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;
157 do {
158 --i;
159 CSSTransition* anim = animations[i];
160 const AnimatedPropertyID& property = anim->TransitionProperty();
161 if (
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);
173 } while (i != 0);
176 return startedAny;
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)));
190 return frame;
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);
201 return 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
212 // replaceable.
213 if (!aTransition || !aTransition->HasCurrentEffect() ||
214 !aTransition->IsRunningOnCompositor() ||
215 aTransition->GetStartTime().IsNull()) {
216 return result;
219 // Transition needs to be running on the same timeline.
220 if (aTransition->GetTimeline() != aTimelineToMatch) {
221 return result;
224 // The transition needs to have a keyframe effect.
225 const KeyframeEffect* keyframeEffect =
226 aTransition->GetEffect() ? aTransition->GetEffect()->AsKeyframeEffect()
227 : nullptr;
228 if (!keyframeEffect) {
229 return result;
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()) {
238 return result;
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}));
249 return result;
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");
262 NS_ASSERTION(
263 !aElementTransitions || &aElementTransitions->mElement == aElement,
264 "Element mismatch");
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)) {
273 return false;
276 aPropertiesChecked.AddProperty(property);
278 if (aDuration + aDelay <= 0.0f) {
279 return false;
282 size_t currentIndex = nsTArray<KeyframeEffect>::NoIndex;
283 const auto* oldTransition = [&]() -> const CSSTransition* {
284 if (!aElementTransitions) {
285 return nullptr;
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) {
291 currentIndex = i;
292 return animations[i];
295 return nullptr;
296 }();
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
307 // current value.
308 Maybe<ReplacedTransitionProperties> replacedTransitionProperties;
309 Maybe<double> progress;
310 if (oldTransition) {
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,
331 aProperties);
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()
342 : nullptr,
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()
348 : nullptr,
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.
367 return false;
370 if (!result.should_animate) {
371 if (oldTransition) {
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,
379 currentIndex);
381 return false;
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
399 // negative.
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
407 // time.
408 if (valuePortion > 1.0) {
409 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);
441 if (!transition) {
442 return false;
445 OwningCSSTransitionPtrArray& transitions = aElementTransitions->mAnimations;
446 #ifdef DEBUG
447 for (size_t i = 0, i_end = transitions.Length(); i < i_end; ++i) {
448 MOZ_ASSERT(
449 i == currentIndex || transitions[i]->TransitionProperty() != property,
450 "duplicate transitions for property");
452 #endif
453 if (oldTransition) {
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;
462 } else {
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);
472 return true;
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()))) {
493 return nullptr;
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),
504 aReversePortion);
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;