Bug 1839315: part 4) Link from `SheetLoadData::mWasAlternate` to spec. r=emilio DONTBUILD
[gecko.git] / layout / style / nsTransitionManager.cpp
blob014c0375daa8c031c20ed015bfcc1418bb50636d
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"
24 #include "nsIFrame.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) {
44 return false;
47 aAnimationValue.mServo =
48 Servo_ComputedValues_ExtractAnimationValue(&aComputedStyle, aProperty)
49 .Consume();
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.
59 return false;
62 MOZ_ASSERT(mPresContext->IsDynamic());
63 if (aNewStyle.StyleDisplay()->mDisplay == StyleDisplay::None) {
64 StopAnimationsForElement(aElement, aPseudoType);
65 return false;
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.
76 template <typename T>
77 static void ExpandTransitionProperty(nsCSSPropertyID aProperty, T aHandler) {
78 if (aProperty == eCSSPropertyExtra_no_properties ||
79 aProperty == eCSSPropertyExtra_variable ||
80 aProperty == eCSSProperty_UNKNOWN) {
81 // Nothing to do.
82 return;
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)) {
91 continue;
93 aHandler(p);
95 } else if (nsCSSProps::IsShorthand(aProperty)) {
96 CSSPROPS_FOR_SHORTHAND_SUBPROPERTIES(subprop, aProperty,
97 CSSEnabledState::ForAllContent) {
98 aHandler(*subprop);
100 } else {
101 aHandler(aProperty);
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,
110 "Element mismatch");
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) {
122 continue;
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;
160 do {
161 --i;
162 CSSTransition* anim = animations[i];
163 const nsCSSPropertyID property = anim->TransitionProperty();
164 if (
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);
176 } while (i != 0);
179 return startedAny;
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)));
192 return frame;
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);
203 return 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
216 // replaceable.
217 if (!aTransition || !aTransition->HasCurrentEffect() ||
218 !aTransition->IsRunningOnCompositor() ||
219 aTransition->GetStartTime().IsNull()) {
220 return result;
223 // Transition needs to be running on the same timeline.
224 if (aTransition->GetTimeline() != aTimelineToMatch) {
225 return result;
228 // The transition needs to have a keyframe effect.
229 const KeyframeEffect* keyframeEffect =
230 aTransition->GetEffect() ? aTransition->GetEffect()->AsKeyframeEffect()
231 : nullptr;
232 if (!keyframeEffect) {
233 return result;
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()) {
242 return result;
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}));
253 return result;
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");
264 NS_ASSERTION(
265 !aElementTransitions || &aElementTransitions->mElement == aElement,
266 "Element mismatch");
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)) {
275 return false;
278 aPropertiesChecked.AddProperty(aProperty);
280 if (!IsTransitionable(aProperty)) {
281 return false;
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
291 // transition.
292 if (delay + duration <= 0.0f) {
293 return false;
296 AnimationValue startValue, endValue;
297 bool haveValues =
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;
314 currentIndex = i;
315 oldTransition = animations[i];
316 break;
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.
338 return false;
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,
350 currentIndex);
352 return false;
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
371 // negative.
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
379 // time.
380 if (valuePortion > 1.0) {
381 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);
413 if (!transition) {
414 return false;
417 OwningCSSTransitionPtrArray& transitions = aElementTransitions->mAnimations;
418 #ifdef DEBUG
419 for (size_t i = 0, i_end = transitions.Length(); i < i_end; ++i) {
420 MOZ_ASSERT(
421 i == currentIndex || transitions[i]->TransitionProperty() != aProperty,
422 "duplicate transitions for property");
424 #endif
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;
443 } else {
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);
453 return true;
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()))) {
474 return nullptr;
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),
485 aReversePortion);
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;