nsTransitionManager.cpp (21826B)
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/. */ 6 7 /* Code to start and animate CSS transitions. */ 8 9 #include "nsTransitionManager.h" 10 11 #include "AnimatedPropertyIDSet.h" 12 #include "CSSPropertyId.h" 13 #include "mozilla/ComputedStyle.h" 14 #include "mozilla/EffectSet.h" 15 #include "mozilla/ElementAnimationData.h" 16 #include "mozilla/EventDispatcher.h" 17 #include "mozilla/RestyleManager.h" 18 #include "mozilla/ServoBindings.h" 19 #include "mozilla/StyleAnimationValue.h" 20 #include "mozilla/dom/Document.h" 21 #include "mozilla/dom/DocumentTimeline.h" 22 #include "mozilla/dom/Element.h" 23 #include "nsAnimationManager.h" 24 #include "nsCSSPropertyIDSet.h" 25 #include "nsCSSProps.h" 26 #include "nsDisplayList.h" 27 #include "nsIContent.h" 28 #include "nsIFrame.h" 29 #include "nsRFPService.h" 30 #include "nsStyleChangeList.h" 31 32 using mozilla::dom::CSSTransition; 33 using mozilla::dom::DocumentTimeline; 34 using mozilla::dom::KeyframeEffect; 35 36 using namespace mozilla; 37 using namespace mozilla::css; 38 39 bool nsTransitionManager::UpdateTransitions( 40 dom::Element* aElement, const PseudoStyleRequest& aPseudoRequest, 41 const ComputedStyle& aOldStyle, const ComputedStyle& aNewStyle) { 42 if (mPresContext->Medium() == nsGkAtoms::print) { 43 // For print or print preview, ignore transitions. 44 return false; 45 } 46 47 MOZ_ASSERT(mPresContext->IsDynamic()); 48 if (aNewStyle.StyleDisplay()->mDisplay == StyleDisplay::None) { 49 StopAnimationsForElement(aElement, aPseudoRequest); 50 return false; 51 } 52 53 auto* collection = CSSTransitionCollection::Get(aElement, aPseudoRequest); 54 return DoUpdateTransitions(*aNewStyle.StyleUIReset(), aElement, 55 aPseudoRequest, collection, aOldStyle, aNewStyle); 56 } 57 58 // This function expands the shorthands and "all" keyword specified in 59 // transition-property, and then execute |aHandler| on the expanded longhand. 60 // |aHandler| should be a lamda function which accepts NonCustomCSSPropertyId. 61 template <typename T> 62 static void ExpandTransitionProperty(const StyleTransitionProperty& aProperty, 63 T aHandler) { 64 switch (aProperty.tag) { 65 case StyleTransitionProperty::Tag::Unsupported: 66 break; 67 case StyleTransitionProperty::Tag::Custom: { 68 auto property = 69 CSSPropertyId::FromCustomName(aProperty.AsCustom().AsAtom()); 70 aHandler(property); 71 break; 72 } 73 case StyleTransitionProperty::Tag::NonCustom: { 74 NonCustomCSSPropertyId id = 75 NonCustomCSSPropertyId(aProperty.AsNonCustom()._0); 76 if (nsCSSProps::IsShorthand(id)) { 77 CSSPROPS_FOR_SHORTHAND_SUBPROPERTIES(subprop, id, 78 CSSEnabledState::ForAllContent) { 79 CSSPropertyId property(*subprop); 80 aHandler(property); 81 } 82 } else { 83 CSSPropertyId property(id); 84 aHandler(property); 85 } 86 break; 87 } 88 } 89 } 90 91 bool nsTransitionManager::DoUpdateTransitions( 92 const nsStyleUIReset& aStyle, dom::Element* aElement, 93 const PseudoStyleRequest& aPseudoRequest, 94 CSSTransitionCollection*& aElementTransitions, 95 const ComputedStyle& aOldStyle, const ComputedStyle& aNewStyle) { 96 MOZ_ASSERT(!aElementTransitions || &aElementTransitions->mElement == aElement, 97 "Element mismatch"); 98 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(); 107 108 // The spec says a negative duration is treated as zero. 109 const float duration = 110 std::max(aStyle.GetTransitionDuration(i).ToMilliseconds(), 0.0f); 111 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; 117 } 118 119 const auto behavior = aStyle.GetTransitionBehavior(i); 120 ExpandTransitionProperty( 121 aStyle.GetTransitionProperty(i), [&](const CSSPropertyId& 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, behavior, aElement, 127 aPseudoRequest, aElementTransitions, aOldStyle, aNewStyle, 128 propertiesChecked); 129 }); 130 } 131 132 // Stop any transitions for properties that are no longer in 133 // 'transition-property', including finished transitions. 134 // Also stop any transitions (and remove any finished transitions) 135 // for properties that just changed (and are still in the set of 136 // properties to transition), but for which we didn't just start the 137 // transition. This can happen delay and duration are both zero, or 138 // because the new value is not interpolable. 139 if (aElementTransitions) { 140 const bool checkProperties = !aStyle.GetTransitionProperty(0).IsAll(); 141 AnimatedPropertyIDSet allTransitionProperties; 142 if (checkProperties) { 143 for (uint32_t i = aStyle.mTransitionPropertyCount; i-- != 0;) { 144 ExpandTransitionProperty(aStyle.GetTransitionProperty(i), 145 [&](const CSSPropertyId& aProperty) { 146 allTransitionProperties.AddProperty( 147 aProperty.ToPhysical(aNewStyle)); 148 }); 149 } 150 } 151 152 OwningCSSTransitionPtrArray& animations = aElementTransitions->mAnimations; 153 size_t i = animations.Length(); 154 MOZ_ASSERT(i != 0, "empty transitions list?"); 155 AnimationValue currentValue; 156 do { 157 --i; 158 CSSTransition* anim = animations[i]; 159 const CSSPropertyId& property = anim->TransitionProperty(); 160 if ( 161 // Properties no longer in `transition-property`. 162 (checkProperties && !allTransitionProperties.HasProperty(property)) || 163 // Properties whose computed values changed but for which we did not 164 // start a new transition (because delay and duration are both zero, 165 // or because the new value is not interpolable); a new transition 166 // would have anim->ToValue() matching currentValue. 167 !Servo_ComputedValues_TransitionValueMatches( 168 &aNewStyle, &property, anim->ToValue().mServo.get())) { 169 // Stop the transition. 170 DoCancelTransition(aElement, aPseudoRequest, aElementTransitions, i); 171 } 172 } while (i != 0); 173 } 174 175 return startedAny; 176 } 177 178 static Keyframe& AppendKeyframe(double aOffset, const CSSPropertyId& aProperty, 179 AnimationValue&& aValue, 180 nsTArray<Keyframe>& aKeyframes) { 181 Keyframe& frame = *aKeyframes.AppendElement(); 182 frame.mOffset.emplace(aOffset); 183 MOZ_ASSERT(aValue.mServo); 184 RefPtr<StyleLockedDeclarationBlock> decl = 185 Servo_AnimationValue_Uncompute(aValue.mServo).Consume(); 186 frame.mPropertyValues.AppendElement( 187 PropertyValuePair(aProperty, std::move(decl))); 188 return frame; 189 } 190 191 static nsTArray<Keyframe> GetTransitionKeyframes(const CSSPropertyId& aProperty, 192 AnimationValue&& aStartValue, 193 AnimationValue&& aEndValue) { 194 nsTArray<Keyframe> keyframes(2); 195 196 AppendKeyframe(0.0, aProperty, std::move(aStartValue), keyframes); 197 AppendKeyframe(1.0, aProperty, std::move(aEndValue), keyframes); 198 199 return keyframes; 200 } 201 202 using ReplacedTransitionProperties = 203 CSSTransition::ReplacedTransitionProperties; 204 static Maybe<ReplacedTransitionProperties> GetReplacedTransitionProperties( 205 const CSSTransition& aTransition, 206 const DocumentTimeline* aTimelineToMatch) { 207 Maybe<ReplacedTransitionProperties> result; 208 209 if (!aTransition.HasCurrentEffect()) { 210 return result; 211 } 212 213 // Transition needs to be running on the same timeline. 214 if (aTransition.GetTimeline() != aTimelineToMatch) { 215 return result; 216 } 217 218 auto startTime = aTransition.GetStartTime(); 219 if (startTime.IsNull() && !aTransition.GetPendingReadyTime().IsNull()) { 220 startTime = 221 aTimelineToMatch->ToTimelineTime(aTransition.GetPendingReadyTime()); 222 } 223 224 if (startTime.IsNull()) { 225 return result; 226 } 227 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; 234 } 235 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; 243 } 244 245 const AnimationPropertySegment& segment = 246 keyframeEffect->Properties()[0].mSegments[0]; 247 248 result.emplace(ReplacedTransitionProperties( 249 {startTime.Value(), aTransition.PlaybackRate(), 250 keyframeEffect->SpecifiedTiming(), segment.mTimingFunction, 251 segment.mFromValue, segment.mToValue})); 252 253 return result; 254 } 255 256 bool nsTransitionManager::ConsiderInitiatingTransition( 257 const CSSPropertyId& aProperty, const nsStyleUIReset& aStyle, 258 uint32_t aTransitionIndex, float aDelay, float aDuration, 259 mozilla::StyleTransitionBehavior aBehavior, dom::Element* aElement, 260 const PseudoStyleRequest& aPseudoRequest, 261 CSSTransitionCollection*& aElementTransitions, 262 const ComputedStyle& aOldStyle, const ComputedStyle& aNewStyle, 263 AnimatedPropertyIDSet& aPropertiesChecked) { 264 // IsShorthand itself will assert if aProperty is not a property. 265 MOZ_ASSERT(aProperty.IsCustom() || !nsCSSProps::IsShorthand(aProperty.mId), 266 "property out of range"); 267 NS_ASSERTION( 268 !aElementTransitions || &aElementTransitions->mElement == aElement, 269 "Element mismatch"); 270 271 CSSPropertyId property = aProperty.ToPhysical(aNewStyle); 272 273 // A later item in transition-property already specified a transition for 274 // this property, so we ignore this one. 275 // 276 // See http://lists.w3.org/Archives/Public/www-style/2009Aug/0109.html . 277 if (aPropertiesChecked.HasProperty(property)) { 278 return false; 279 } 280 281 aPropertiesChecked.AddProperty(property); 282 283 if (aDuration + aDelay <= 0.0f) { 284 return false; 285 } 286 287 size_t currentIndex = nsTArray<KeyframeEffect>::NoIndex; 288 const auto* oldTransition = [&]() -> const CSSTransition* { 289 if (!aElementTransitions) { 290 return nullptr; 291 } 292 const OwningCSSTransitionPtrArray& animations = 293 aElementTransitions->mAnimations; 294 for (size_t i = 0, i_end = animations.Length(); i < i_end; ++i) { 295 if (animations[i]->TransitionProperty() == property) { 296 currentIndex = i; 297 return animations[i]; 298 } 299 } 300 return nullptr; 301 }(); 302 303 // For compositor animations, |aOldStyle| may have out-of-date transition 304 // rules, and it may be equal to the |endValue| of a reversing transition by 305 // accidentally. This causes Servo_ComputedValues_ShouldTransition() to return 306 // an incorrect result. Therefore, we have to recompute the current value if 307 // this transition is running on the compositor, to make sure we create the 308 // transition properly. Here, we pre-compute the progress and collect the 309 // necessary info, so Servo_ComputedValues_ShouldTransition() could compute 310 // the current value if needed. 311 // FIXME: Bug 1634945. We should use the last value from the compositor as the 312 // current value. 313 Maybe<ReplacedTransitionProperties> replacedTransitionProperties; 314 Maybe<double> progress; 315 if (oldTransition) { 316 // If this new transition is replacing an existing transition, we store 317 // select parameters from the replaced transition so that later, once all 318 // scripts have run, we can update the start value of the transition using 319 // TimeStamp::Now(). This allows us to avoid a large jump when starting a 320 // new transition when the main thread lags behind the compositor. 321 // 322 // Note: We compute this before calling 323 // Servo_ComputedValues_ShouldTransition() so we can reuse it for computing 324 // the current value and setting the replaced transition properties later in 325 // this function. 326 const dom::DocumentTimeline* timeline = aElement->OwnerDoc()->Timeline(); 327 replacedTransitionProperties = 328 GetReplacedTransitionProperties(*oldTransition, timeline); 329 progress = replacedTransitionProperties.andThen( 330 [&](const ReplacedTransitionProperties& aProperties) { 331 const dom::AnimationTimeline* timeline = oldTransition->GetTimeline(); 332 MOZ_ASSERT(timeline); 333 return CSSTransition::ComputeTransformedProgress(*timeline, 334 aProperties); 335 }); 336 } 337 338 AnimationValue startValue, endValue; 339 const StyleShouldTransitionResult result = 340 Servo_ComputedValues_ShouldTransition( 341 &aOldStyle, &aNewStyle, &property, aBehavior, 342 oldTransition ? oldTransition->ToValue().mServo.get() : nullptr, 343 replacedTransitionProperties 344 ? replacedTransitionProperties->mFromValue.mServo.get() 345 : nullptr, 346 // Note: It's possible to replace the keyframes by Web Animations API, 347 // so we have to pass the mToValue from the keyframe segment, to make 348 // sure this value is aligned with mFromValue. 349 replacedTransitionProperties 350 ? replacedTransitionProperties->mToValue.mServo.get() 351 : nullptr, 352 progress.ptrOr(nullptr), &startValue.mServo, &endValue.mServo); 353 354 // If we got a style change that changed the value to the endpoint 355 // of the currently running transition, we don't want to interrupt 356 // its timing function. 357 // This needs to be before the !shouldAnimate && haveCurrentTransition 358 // case below because we might be close enough to the end of the 359 // transition that the current value rounds to the final value. In 360 // this case, we'll end up with shouldAnimate as false (because 361 // there's no value change), but we need to return early here rather 362 // than cancel the running transition because shouldAnimate is false! 363 // 364 // Likewise, if we got a style change that changed the value to the 365 // endpoint of our finished transition, we also don't want to start 366 // a new transition for the reasons described in 367 // https://lists.w3.org/Archives/Public/www-style/2015Jan/0444.html . 368 if (result.old_transition_value_matches) { 369 // GetAnimationRule already called RestyleForAnimation. 370 return false; 371 } 372 373 if (!result.should_animate) { 374 if (oldTransition) { 375 // We're in the middle of a transition, and just got a non-transition 376 // style change to something that we can't animate. This might happen 377 // because we got a non-transition style change changing to the current 378 // in-progress value (which is particularly easy to cause when we're 379 // currently in the 'transition-delay'). It also might happen because we 380 // just got a style change to a value that can't be interpolated. 381 DoCancelTransition(aElement, aPseudoRequest, aElementTransitions, 382 currentIndex); 383 } 384 return false; 385 } 386 387 AnimationValue startForReversingTest = startValue; 388 double reversePortion = 1.0; 389 390 // If the new transition reverses an existing one, we'll need to 391 // handle the timing differently. 392 if (oldTransition && oldTransition->HasCurrentEffect() && 393 oldTransition->StartForReversingTest() == endValue) { 394 // Compute the appropriate negative transition-delay such that right 395 // now we'd end up at the current position. 396 double valuePortion = 397 oldTransition->CurrentValuePortion() * oldTransition->ReversePortion() + 398 (1.0 - oldTransition->ReversePortion()); 399 // A timing function with negative y1 (or y2!) might make 400 // valuePortion negative. In this case, we still want to apply our 401 // reversing logic based on relative distances, not make duration 402 // negative. 403 if (valuePortion < 0.0) { 404 valuePortion = -valuePortion; 405 } 406 // A timing function with y2 (or y1!) greater than one might 407 // advance past its terminal value. It's probably a good idea to 408 // clamp valuePortion to be at most one to preserve the invariant 409 // that a transition will complete within at most its specified 410 // time. 411 if (valuePortion > 1.0) { 412 valuePortion = 1.0; 413 } 414 415 // Negative delays are essentially part of the transition 416 // function, so reduce them along with the duration, but don't 417 // reduce positive delays. 418 if (aDelay < 0.0f && std::isfinite(aDelay)) { 419 aDelay *= valuePortion; 420 } 421 422 if (std::isfinite(aDuration)) { 423 aDuration *= valuePortion; 424 } 425 426 startForReversingTest = oldTransition->ToValue(); 427 reversePortion = valuePortion; 428 } 429 430 TimingParams timing = TimingParamsFromCSSParams( 431 Some(aDuration), aDelay, 1.0 /* iteration count */, 432 StyleAnimationDirection::Normal, StyleAnimationFillMode::Backwards); 433 434 const StyleComputedTimingFunction& tf = 435 aStyle.GetTransitionTimingFunction(aTransitionIndex); 436 if (!tf.IsLinearKeyword()) { 437 timing.SetTimingFunction(Some(tf)); 438 } 439 440 RefPtr<CSSTransition> transition = DoCreateTransition( 441 property, aElement, aPseudoRequest, aNewStyle, aElementTransitions, 442 std::move(timing), std::move(startValue), std::move(endValue), 443 std::move(startForReversingTest), reversePortion); 444 if (!transition) { 445 return false; 446 } 447 448 OwningCSSTransitionPtrArray& transitions = aElementTransitions->mAnimations; 449 #ifdef DEBUG 450 for (size_t i = 0, i_end = transitions.Length(); i < i_end; ++i) { 451 MOZ_ASSERT( 452 i == currentIndex || transitions[i]->TransitionProperty() != property, 453 "duplicate transitions for property"); 454 } 455 #endif 456 if (oldTransition) { 457 if (replacedTransitionProperties) { 458 transition->SetReplacedTransition( 459 std::move(replacedTransitionProperties.ref())); 460 } 461 462 transitions[currentIndex]->CancelFromStyle(PostRestyleMode::IfNeeded); 463 oldTransition = nullptr; // Clear pointer so it doesn't dangle 464 transitions[currentIndex] = transition; 465 } else { 466 // XXX(Bug 1631371) Check if this should use a fallible operation as it 467 // pretended earlier. 468 transitions.AppendElement(transition); 469 } 470 471 if (auto* effectSet = EffectSet::Get(aElement, aPseudoRequest)) { 472 effectSet->UpdateAnimationGeneration(mPresContext); 473 } 474 475 return true; 476 } 477 478 already_AddRefed<CSSTransition> nsTransitionManager::DoCreateTransition( 479 const CSSPropertyId& aProperty, dom::Element* aElement, 480 const PseudoStyleRequest& aPseudoRequest, 481 const mozilla::ComputedStyle& aNewStyle, 482 CSSTransitionCollection*& aElementTransitions, TimingParams&& aTiming, 483 AnimationValue&& aStartValue, AnimationValue&& aEndValue, 484 AnimationValue&& aStartForReversingTest, double aReversePortion) { 485 dom::DocumentTimeline* timeline = aElement->OwnerDoc()->Timeline(); 486 KeyframeEffectParams effectOptions; 487 auto keyframeEffect = MakeRefPtr<KeyframeEffect>( 488 aElement->OwnerDoc(), OwningAnimationTarget(aElement, aPseudoRequest), 489 std::move(aTiming), effectOptions); 490 491 keyframeEffect->SetKeyframes( 492 GetTransitionKeyframes(aProperty, std::move(aStartValue), 493 std::move(aEndValue)), 494 &aNewStyle, timeline); 495 496 if (NS_WARN_IF(MOZ_UNLIKELY(!keyframeEffect->IsValidTransition()))) { 497 return nullptr; 498 } 499 500 auto animation = MakeRefPtr<CSSTransition>( 501 mPresContext->Document()->GetScopeObject(), aProperty); 502 animation->SetOwningElement(OwningElementRef(*aElement, aPseudoRequest)); 503 animation->SetTimelineNoUpdate(timeline); 504 animation->SetCreationSequence( 505 mPresContext->RestyleManager()->GetAnimationGeneration()); 506 animation->SetEffectFromStyle(keyframeEffect); 507 animation->SetReverseParameters(std::move(aStartForReversingTest), 508 aReversePortion); 509 animation->PlayFromStyle(); 510 511 if (!aElementTransitions) { 512 aElementTransitions = 513 &aElement->EnsureAnimationData().EnsureTransitionCollection( 514 *aElement, aPseudoRequest); 515 if (!aElementTransitions->isInList()) { 516 AddElementCollection(aElementTransitions); 517 } 518 } 519 return animation.forget(); 520 } 521 522 void nsTransitionManager::DoCancelTransition( 523 dom::Element* aElement, const PseudoStyleRequest& aPseudoRequest, 524 CSSTransitionCollection*& aElementTransitions, size_t aIndex) { 525 MOZ_ASSERT(aElementTransitions); 526 OwningCSSTransitionPtrArray& transitions = aElementTransitions->mAnimations; 527 CSSTransition* transition = transitions[aIndex]; 528 529 if (transition->HasCurrentEffect()) { 530 if (auto* effectSet = EffectSet::Get(aElement, aPseudoRequest)) { 531 effectSet->UpdateAnimationGeneration(mPresContext); 532 } 533 } 534 transition->CancelFromStyle(PostRestyleMode::IfNeeded); 535 transitions.RemoveElementAt(aIndex); 536 537 if (transitions.IsEmpty()) { 538 aElementTransitions->Destroy(); 539 // |aElementTransitions| is now a dangling pointer! 540 aElementTransitions = nullptr; 541 } 542 }