ViewTransition.cpp (79968B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 #include "ViewTransition.h" 6 7 #include "Units.h" 8 #include "WindowRenderer.h" 9 #include "mozilla/AnimationEventDispatcher.h" 10 #include "mozilla/EffectSet.h" 11 #include "mozilla/ElementAnimationData.h" 12 #include "mozilla/FlowMarkers.h" 13 #include "mozilla/SVGIntegrationUtils.h" 14 #include "mozilla/ServoStyleConsts.h" 15 #include "mozilla/WritingModes.h" 16 #include "mozilla/dom/BindContext.h" 17 #include "mozilla/dom/DocumentInlines.h" 18 #include "mozilla/dom/DocumentTimeline.h" 19 #include "mozilla/dom/Promise-inl.h" 20 #include "mozilla/dom/ViewTransitionBinding.h" 21 #include "mozilla/dom/ViewTransitionTypeSet.h" 22 #include "mozilla/image/WebRenderImageProvider.h" 23 #include "mozilla/layers/RenderRootStateManager.h" 24 #include "mozilla/layers/WebRenderBridgeChild.h" 25 #include "mozilla/layers/WebRenderLayerManager.h" 26 #include "mozilla/webrender/WebRenderAPI.h" 27 #include "nsCanvasFrame.h" 28 #include "nsDisplayList.h" 29 #include "nsFrameState.h" 30 #include "nsITimer.h" 31 #include "nsLayoutUtils.h" 32 #include "nsPresContext.h" 33 #include "nsString.h" 34 35 namespace mozilla::dom { 36 37 LazyLogModule gViewTransitionsLog("ViewTransitions"); 38 39 NS_DECLARE_FRAME_PROPERTY_RELEASABLE(ViewTransitionCaptureName, nsAtom) 40 41 static void SetCaptured(nsIFrame* aFrame, bool aCaptured, 42 nsAtom* aNameIfCaptured) { 43 aFrame->AddOrRemoveStateBits(NS_FRAME_CAPTURED_IN_VIEW_TRANSITION, aCaptured); 44 if (aCaptured) { 45 aFrame->AddProperty(ViewTransitionCaptureName(), 46 do_AddRef(aNameIfCaptured).take()); 47 } else { 48 aFrame->RemoveProperty(ViewTransitionCaptureName()); 49 } 50 aFrame->InvalidateFrameSubtree(); 51 if (aFrame->Style()->IsRootElementStyle()) { 52 aFrame->PresShell()->GetRootFrame()->InvalidateFrameSubtree(); 53 } 54 } 55 56 // Set capture's old transform to a <transform-function> that would map 57 // element's border box from the snapshot containing block origin to its 58 // current visual position. 59 // 60 // Since we're using viewport as the snapshot origin, we can use 61 // GetBoundingClientRect() effectively... 62 // 63 // TODO(emilio): This might need revision. 64 static CSSToCSSMatrix4x4Flagged EffectiveTransform(nsIFrame* aFrame) { 65 if (aFrame->GetSize().IsEmpty() || aFrame->Style()->IsRootElementStyle()) { 66 return {}; 67 } 68 69 auto matrix = CSSToCSSMatrix4x4Flagged::FromUnknownMatrix( 70 nsLayoutUtils::GetTransformToAncestor( 71 RelativeTo{aFrame}, 72 RelativeTo{nsLayoutUtils::GetContainingBlockForClientRect(aFrame)}, 73 nsIFrame::IN_CSS_UNITS, nullptr)); 74 75 // Compensate for the default transform-origin of 50% 50% using border box 76 // dimensions. 77 auto borderBoxRect = CSSRect::FromAppUnits(aFrame->GetRect()); 78 matrix.ChangeBasis(-borderBoxRect.Width() / 2, -borderBoxRect.Height() / 2, 79 0.0f); 80 return matrix; 81 } 82 83 enum class CapturedRectType { BorderBox, InkOverflowBox }; 84 85 static inline nsRect SnapRect(const nsRect& aRect, nscoord aAppUnitsPerPixel) { 86 return LayoutDeviceIntRect::ToAppUnits( 87 LayoutDeviceIntRect::FromUnknownRect( 88 aRect.ToOutsidePixels(aAppUnitsPerPixel)), 89 aAppUnitsPerPixel); 90 } 91 92 static inline nsRect CapturedRect(const nsIFrame* aFrame, 93 const nsSize& aSnapshotContainingBlockSize, 94 CapturedRectType aType) { 95 if (aFrame->Style()->IsRootElementStyle()) { 96 return nsRect(nsPoint(), aSnapshotContainingBlockSize); 97 } 98 99 if (aType == CapturedRectType::BorderBox) { 100 return aFrame->GetRectRelativeToSelf(); 101 } 102 103 return SnapRect(aFrame->InkOverflowRectRelativeToSelf(), 104 aFrame->PresContext()->AppUnitsPerDevPixel()); 105 } 106 107 static StyleViewTransitionClass DocumentScopedClassListFor( 108 const nsIFrame* aFrame) { 109 return aFrame->StyleUIReset()->mViewTransitionClass; 110 } 111 112 static constexpr wr::ImageKey kNoKey{{0}, 0}; 113 struct OldSnapshotData { 114 wr::ImageKey mImageKey = kNoKey; 115 // Snapshot size should match the captured element’s InkOverflowBox size, 116 // snapped. 117 nsRect mSnapshotRect; 118 RefPtr<layers::RenderRootStateManager> mManager; 119 bool mUsed = false; 120 121 OldSnapshotData() = default; 122 123 explicit OldSnapshotData(nsIFrame* aFrame, 124 const nsSize& aSnapshotContainingBlockSize) 125 : mSnapshotRect(CapturedRect(aFrame, aSnapshotContainingBlockSize, 126 CapturedRectType::InkOverflowBox)) {} 127 128 void EnsureKey(layers::RenderRootStateManager* aManager, 129 wr::IpcResourceUpdateQueue& aResources) { 130 if (mImageKey != kNoKey) { 131 MOZ_ASSERT(mManager == aManager, "Stale manager?"); 132 return; 133 } 134 mManager = aManager; 135 mImageKey = aManager->WrBridge()->GetNextImageKey(); 136 aResources.AddSnapshotImage(wr::SnapshotImageKey{mImageKey}); 137 } 138 139 ~OldSnapshotData() { 140 if (mManager) { 141 wr::SnapshotImageKey key = {mImageKey}; 142 if (mUsed) { 143 mManager->AddSnapshotImageKeyForDiscard(key); 144 } else { 145 mManager->AddUnusedSnapshotImageKeyForDiscard(key); 146 } 147 } 148 } 149 }; 150 151 struct CapturedElementOldState { 152 OldSnapshotData mSnapshot; 153 // Whether we tried to capture an image. Note we might fail to get a 154 // snapshot, so this might not be the same as !!mImage. 155 bool mTriedImage = false; 156 157 nsSize mBorderBoxSize; 158 CSSToCSSMatrix4x4Flagged mTransform; 159 StyleWritingModeProperty mWritingMode = 160 StyleWritingModeProperty::HorizontalTb; 161 StyleDirection mDirection = StyleDirection::Ltr; 162 StyleTextOrientation mTextOrientation = StyleTextOrientation::Mixed; 163 StyleBlend mMixBlendMode = StyleBlend::Normal; 164 StyleOwnedSlice<StyleFilter> mBackdropFilters; 165 // Note: it's unfortunate we cannot just store the bits here. color-scheme 166 // property uses idents for serialization. If the idents and bits are not 167 // aligned, we assert it in ToCSS. 168 StyleColorScheme mColorScheme; 169 170 CapturedElementOldState(nsIFrame* aFrame, 171 const nsSize& aSnapshotContainingBlockSize) 172 : mSnapshot(aFrame, aSnapshotContainingBlockSize), 173 mTriedImage(true), 174 mBorderBoxSize(CapturedRect(aFrame, aSnapshotContainingBlockSize, 175 CapturedRectType::BorderBox) 176 .Size()), 177 mTransform(EffectiveTransform(aFrame)), 178 mWritingMode(aFrame->StyleVisibility()->mWritingMode), 179 mDirection(aFrame->StyleVisibility()->mDirection), 180 mTextOrientation(aFrame->StyleVisibility()->mTextOrientation), 181 mMixBlendMode(aFrame->StyleEffects()->mMixBlendMode), 182 mBackdropFilters(aFrame->StyleEffects()->mBackdropFilters), 183 mColorScheme(aFrame->StyleUI()->mColorScheme) {} 184 185 CapturedElementOldState() = default; 186 }; 187 188 // https://drafts.csswg.org/css-view-transitions/#captured-element 189 struct ViewTransition::CapturedElement { 190 CapturedElementOldState mOldState; 191 RefPtr<Element> mNewElement; 192 wr::SnapshotImageKey mNewSnapshotKey{kNoKey}; 193 // Snapshot size + offset, should match the captured element’s InkOverflowBox 194 // size, snapped. 195 nsRect mNewSnapshotRect; 196 nsSize mNewBorderBoxSize; 197 198 CapturedElement() = default; 199 200 CapturedElement(nsIFrame* aFrame, const nsSize& aSnapshotContainingBlockSize, 201 StyleViewTransitionClass&& aClassList) 202 : mOldState(aFrame, aSnapshotContainingBlockSize), 203 mClassList(std::move(aClassList)) {} 204 205 // https://drafts.csswg.org/css-view-transitions-1/#captured-element-style-definitions 206 nsTArray<Keyframe> mGroupKeyframes; 207 // The group animation-name rule and group styles rule, merged into one. 208 RefPtr<StyleLockedDeclarationBlock> mGroupRule; 209 // The image pair isolation rule. 210 RefPtr<StyleLockedDeclarationBlock> mImagePairRule; 211 // The rules for ::view-transition-old(<name>). 212 RefPtr<StyleLockedDeclarationBlock> mOldRule; 213 // The rules for ::view-transition-new(<name>). 214 RefPtr<StyleLockedDeclarationBlock> mNewRule; 215 216 // The view-transition-class associated with this captured element. 217 // https://drafts.csswg.org/css-view-transitions-2/#captured-element-class-list 218 StyleViewTransitionClass mClassList; 219 220 // If snapshots are very large, compute active rects to restrict their 221 // bounds, based on what's most likely visible during the transition. 222 Maybe<nsRect> mOldActiveRect; 223 Maybe<nsRect> mNewActiveRect; 224 225 void CaptureClassList(StyleViewTransitionClass&& aClassList) { 226 mClassList = std::move(aClassList); 227 } 228 229 ~CapturedElement() { 230 if (wr::AsImageKey(mNewSnapshotKey) != kNoKey) { 231 MOZ_ASSERT(mOldState.mSnapshot.mManager); 232 mOldState.mSnapshot.mManager->AddSnapshotImageKeyForDiscard( 233 mNewSnapshotKey); 234 } 235 } 236 }; 237 238 static inline void ImplCycleCollectionTraverse( 239 nsCycleCollectionTraversalCallback& aCb, 240 const ViewTransition::CapturedElement& aField, const char* aName, 241 uint32_t aFlags = 0) { 242 ImplCycleCollectionTraverse(aCb, aField.mNewElement, aName, aFlags); 243 } 244 245 NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(ViewTransition, mDocument, 246 mUpdateCallback, 247 mUpdateCallbackDonePromise, mReadyPromise, 248 mFinishedPromise, mNamedElements, mTypes, 249 mSnapshotContainingBlock) 250 251 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ViewTransition) 252 NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY 253 NS_INTERFACE_MAP_ENTRY(nsISupports) 254 NS_INTERFACE_MAP_END 255 256 NS_IMPL_CYCLE_COLLECTING_ADDREF(ViewTransition) 257 NS_IMPL_CYCLE_COLLECTING_RELEASE(ViewTransition) 258 259 ViewTransition::ViewTransition(Document& aDoc, 260 ViewTransitionUpdateCallback* aCb, 261 TypeList&& aTypeList) 262 : mDocument(&aDoc), mUpdateCallback(aCb), mTypeList(std::move(aTypeList)) {} 263 264 ViewTransition::~ViewTransition() { ClearTimeoutTimer(); } 265 266 Element* ViewTransition::GetViewTransitionTreeRoot() const { 267 return mSnapshotContainingBlock 268 ? mSnapshotContainingBlock->GetFirstElementChild() 269 : nullptr; 270 } 271 272 void ViewTransition::GetCapturedFrames( 273 nsTArray<nsIFrame*>& aCapturedFrames) const { 274 if (mOldCaptureElements) { 275 for (const auto& [f, _] : *mOldCaptureElements) { 276 aCapturedFrames.AppendElement(f); 277 } 278 } 279 280 for (const auto& entry : mNamedElements) { 281 CapturedElement& capturedElement = *entry.GetData(); 282 if (capturedElement.mNewElement && 283 capturedElement.mNewElement->GetPrimaryFrame()) { 284 aCapturedFrames.AppendElement( 285 capturedElement.mNewElement->GetPrimaryFrame()); 286 } 287 } 288 } 289 290 Maybe<nsRect> ViewTransition::GetOldInkOverflowRect(nsAtom* aName) const { 291 auto* el = mNamedElements.Get(aName); 292 if (NS_WARN_IF(!el)) { 293 return {}; 294 } 295 return Some(el->mOldState.mSnapshot.mSnapshotRect); 296 } 297 298 Maybe<nsRect> ViewTransition::GetNewInkOverflowRect(nsAtom* aName) const { 299 auto* el = mNamedElements.Get(aName); 300 if (NS_WARN_IF(!el)) { 301 return {}; 302 } 303 return Some(el->mNewSnapshotRect); 304 } 305 306 Maybe<nsSize> ViewTransition::GetOldBorderBoxSize(nsAtom* aName) const { 307 auto* el = mNamedElements.Get(aName); 308 if (NS_WARN_IF(!el)) { 309 return {}; 310 } 311 return Some(el->mOldState.mBorderBoxSize); 312 } 313 314 Maybe<nsSize> ViewTransition::GetNewBorderBoxSize(nsAtom* aName) const { 315 auto* el = mNamedElements.Get(aName); 316 if (NS_WARN_IF(!el)) { 317 return {}; 318 } 319 return Some(el->mNewBorderBoxSize); 320 } 321 322 const wr::ImageKey* ViewTransition::GetOrCreateOldImageKey( 323 nsAtom* aName, layers::RenderRootStateManager* aManager, 324 wr::IpcResourceUpdateQueue& aResources) const { 325 auto* el = mNamedElements.Get(aName); 326 if (NS_WARN_IF(!el)) { 327 return nullptr; 328 } 329 el->mOldState.mSnapshot.EnsureKey(aManager, aResources); 330 return &el->mOldState.mSnapshot.mImageKey; 331 } 332 333 const wr::ImageKey* ViewTransition::ReadOldImageKey( 334 nsAtom* aName, layers::RenderRootStateManager* aManager, 335 wr::IpcResourceUpdateQueue& aResources) const { 336 auto* el = mNamedElements.Get(aName); 337 if (NS_WARN_IF(!el)) { 338 return nullptr; 339 } 340 341 el->mOldState.mSnapshot.mUsed = true; 342 return &el->mOldState.mSnapshot.mImageKey; 343 } 344 345 const wr::ImageKey* ViewTransition::GetNewImageKey(nsAtom* aName) const { 346 auto* el = mNamedElements.Get(aName); 347 if (NS_WARN_IF(!el)) { 348 return nullptr; 349 } 350 return &el->mNewSnapshotKey._0; 351 } 352 353 const wr::ImageKey* ViewTransition::GetImageKeyForCapturedFrame( 354 nsIFrame* aFrame, layers::RenderRootStateManager* aManager, 355 wr::IpcResourceUpdateQueue& aResources) const { 356 MOZ_ASSERT(aFrame); 357 MOZ_ASSERT(aFrame->HasAnyStateBits(NS_FRAME_CAPTURED_IN_VIEW_TRANSITION)); 358 359 nsAtom* name = aFrame->GetProperty(ViewTransitionCaptureName()); 360 if (NS_WARN_IF(!name)) { 361 return nullptr; 362 } 363 const bool isOld = mPhase < Phase::Animating; 364 365 VT_LOG("ViewTransition::GetImageKeyForCapturedFrame(%s, old=%d)\n", 366 nsAtomCString(name).get(), isOld); 367 368 if (isOld) { 369 const auto* key = GetOrCreateOldImageKey(name, aManager, aResources); 370 VT_LOG(" > old image is %s", key ? ToString(*key).c_str() : "null"); 371 return key; 372 } 373 auto* el = mNamedElements.Get(name); 374 if (NS_WARN_IF(!el)) { 375 return nullptr; 376 } 377 if (NS_WARN_IF(el->mNewElement != aFrame->GetContent())) { 378 return nullptr; 379 } 380 if (wr::AsImageKey(el->mNewSnapshotKey) == kNoKey) { 381 MOZ_ASSERT(!el->mOldState.mSnapshot.mManager || 382 el->mOldState.mSnapshot.mManager == aManager, 383 "Stale manager?"); 384 el->mNewSnapshotKey = {aManager->WrBridge()->GetNextImageKey()}; 385 el->mOldState.mSnapshot.mManager = aManager; 386 aResources.AddSnapshotImage(el->mNewSnapshotKey); 387 } 388 VT_LOG(" > new image is %s", ToString(el->mNewSnapshotKey._0).c_str()); 389 return &el->mNewSnapshotKey._0; 390 } 391 392 nsIGlobalObject* ViewTransition::GetParentObject() const { 393 return mDocument ? mDocument->GetParentObject() : nullptr; 394 } 395 396 Promise* ViewTransition::GetUpdateCallbackDone(ErrorResult& aRv) { 397 if (!mUpdateCallbackDonePromise) { 398 mUpdateCallbackDonePromise = Promise::Create(GetParentObject(), aRv); 399 } 400 return mUpdateCallbackDonePromise; 401 } 402 403 Promise* ViewTransition::GetReady(ErrorResult& aRv) { 404 if (!mReadyPromise) { 405 mReadyPromise = Promise::Create(GetParentObject(), aRv); 406 } 407 return mReadyPromise; 408 } 409 410 Promise* ViewTransition::GetFinished(ErrorResult& aRv) { 411 if (!mFinishedPromise) { 412 mFinishedPromise = Promise::Create(GetParentObject(), aRv); 413 } 414 return mFinishedPromise; 415 } 416 417 // This performs the step 5 in setup view transition. 418 // https://drafts.csswg.org/css-view-transitions-1/#setup-view-transition 419 void ViewTransition::MaybeScheduleUpdateCallback() { 420 AUTO_PROFILER_FLOW_MARKER("ViewTransition::MaybeScheduleUpdateCallback", 421 LAYOUT, Flow::FromPointer(this)); 422 // 1. If transition’s phase is "done", then abort these steps. 423 // Note: This happens if transition was skipped before this point. 424 if (mPhase == Phase::Done) { 425 return; 426 } 427 428 RefPtr doc = mDocument; 429 430 // 2. Schedule the update callback for transition. 431 doc->ScheduleViewTransitionUpdateCallback(this); 432 433 // 3. Flush the update callback queue. 434 doc->FlushViewTransitionUpdateCallbackQueue(); 435 } 436 437 // https://drafts.csswg.org/css-view-transitions-1/#call-the-update-callback 438 void ViewTransition::CallUpdateCallback(ErrorResult& aRv) { 439 MOZ_ASSERT(mDocument); 440 // Step 1: Assert: transition's phase is "done", or before 441 // "update-callback-called". 442 MOZ_ASSERT(mPhase == Phase::Done || 443 UnderlyingValue(mPhase) < 444 UnderlyingValue(Phase::UpdateCallbackCalled)); 445 VT_LOG("ViewTransition::CallUpdateCallback(%d)\n", int(mPhase)); 446 AUTO_PROFILER_FLOW_MARKER("ViewTransition::CallUpdateCallback", LAYOUT, 447 Flow::FromPointer(this)); 448 449 // Step 5: If transition's phase is not "done", then set transition's phase 450 // to "update-callback-called". 451 // 452 // NOTE(emilio): This is swapped with the spec because the spec is broken, 453 // see https://github.com/w3c/csswg-drafts/issues/10822 454 if (mPhase != Phase::Done) { 455 mPhase = Phase::UpdateCallbackCalled; 456 } 457 458 // Step 2: Let callbackPromise be null. 459 RefPtr<Promise> callbackPromise; 460 if (!mUpdateCallback) { 461 // Step 3: If transition's update callback is null, then set callbackPromise 462 // to a promise resolved with undefined, in transition’s relevant Realm. 463 callbackPromise = 464 Promise::CreateResolvedWithUndefined(GetParentObject(), aRv); 465 } else { 466 // Step 4: Otherwise set callbackPromise to the result of invoking 467 // transition’s update callback. MOZ_KnownLive because the callback can only 468 // go away when we get CCd. 469 callbackPromise = MOZ_KnownLive(mUpdateCallback)->Call(aRv); 470 } 471 if (aRv.Failed()) { 472 // TODO(emilio): Do we need extra error handling here? 473 return; 474 } 475 MOZ_ASSERT(callbackPromise); 476 // Step 8: React to callbackPromise with fulfillSteps and rejectSteps. 477 callbackPromise->AddCallbacksWithCycleCollectedArgs( 478 [](JSContext*, JS::Handle<JS::Value>, ErrorResult& aRv, 479 ViewTransition* aVt) { 480 AUTO_PROFILER_FLOW_MARKER("ViewTransition::UpdateCallbackResolve", 481 LAYOUT, Flow::FromPointer(aVt)); 482 // We clear the timeout when we are ready to activate. Otherwise, any 483 // animations with the duration longer than 484 // StaticPrefs::dom_viewTransitions_timeout_ms() will be interrupted. 485 // FIXME: We may need a better solution to tweak the timeout, e.g. reset 486 // the timeout to a longer value or so on. 487 aVt->ClearTimeoutTimer(); 488 489 // Step 6: Let fulfillSteps be to following steps: 490 if (Promise* ucd = aVt->GetUpdateCallbackDone(aRv)) { 491 // 6.1: Resolve transition's update callback done promise with 492 // undefined. 493 ucd->MaybeResolveWithUndefined(); 494 } 495 // Unlike other timings, this is not guaranteed to happen with clean 496 // layout, and Activate() needs to look at the frame tree to capture the 497 // new state, so we need to flush frames. Do it here so that we deal 498 // with other potential script execution skipping the transition or 499 // what not in a consistent way. 500 aVt->mDocument->FlushPendingNotifications(FlushType::Layout); 501 if (aVt->mPhase == Phase::Done) { 502 // "Skip a transition" step 8. We need to resolve "finished" after 503 // update-callback-done. 504 if (Promise* finished = aVt->GetFinished(aRv)) { 505 finished->MaybeResolveWithUndefined(); 506 } 507 } 508 aVt->Activate(); 509 }, 510 [](JSContext*, JS::Handle<JS::Value> aReason, ErrorResult& aRv, 511 ViewTransition* aVt) { 512 AUTO_PROFILER_FLOW_MARKER("ViewTransition::UpdateCallbackReject", 513 LAYOUT, Flow::FromPointer(aVt)); 514 // Clear the timeout because we are ready to skip the view transitions. 515 aVt->ClearTimeoutTimer(); 516 517 // Step 7: Let rejectSteps be to following steps: 518 if (Promise* ucd = aVt->GetUpdateCallbackDone(aRv)) { 519 // 7.1: Reject transition's update callback done promise with reason. 520 ucd->MaybeReject(aReason); 521 } 522 523 // 7.2: If transition's phase is "done", then return. 524 if (aVt->mPhase == Phase::Done) { 525 // "Skip a transition" step 8. We need to resolve "finished" after 526 // update-callback-done. 527 if (Promise* finished = aVt->GetFinished(aRv)) { 528 finished->MaybeReject(aReason); 529 } 530 return; 531 } 532 533 // 7.3: Mark as handled transition's ready promise. 534 if (Promise* ready = aVt->GetReady(aRv)) { 535 MOZ_ALWAYS_TRUE(ready->SetAnyPromiseIsHandled()); 536 } 537 aVt->SkipTransition(SkipTransitionReason::UpdateCallbackRejected, 538 aReason); 539 }, 540 RefPtr(this)); 541 542 // Step 9: To skip a transition after a timeout, the user agent may perform 543 // the following steps in parallel: 544 MOZ_ASSERT(!mTimeoutTimer); 545 ClearTimeoutTimer(); // Be safe just in case. 546 mTimeoutTimer = NS_NewTimer(); 547 mTimeoutTimer->InitWithNamedFuncCallback( 548 TimeoutCallback, this, StaticPrefs::dom_viewTransitions_timeout_ms(), 549 nsITimer::TYPE_ONE_SHOT, "ViewTransition::TimeoutCallback"_ns); 550 } 551 552 void ViewTransition::ClearTimeoutTimer() { 553 if (mTimeoutTimer) { 554 mTimeoutTimer->Cancel(); 555 mTimeoutTimer = nullptr; 556 } 557 } 558 559 void ViewTransition::TimeoutCallback(nsITimer* aTimer, void* aClosure) { 560 RefPtr vt = static_cast<ViewTransition*>(aClosure); 561 MOZ_DIAGNOSTIC_ASSERT(aTimer == vt->mTimeoutTimer); 562 vt->Timeout(); 563 } 564 565 void ViewTransition::Timeout() { 566 ClearTimeoutTimer(); 567 if (mPhase != Phase::Done && mDocument) { 568 SkipTransition(SkipTransitionReason::Timeout); 569 } 570 } 571 572 static already_AddRefed<Element> MakePseudo(Document& aDoc, 573 PseudoStyleType aType, 574 nsAtom* aName) { 575 RefPtr<Element> el = aDoc.CreateHTMLElement(nsGkAtoms::div); 576 if (aType == PseudoStyleType::mozSnapshotContainingBlock) { 577 el->SetIsNativeAnonymousRoot(); 578 } 579 el->SetPseudoElementType(aType); 580 if (aName) { 581 el->SetAttr(nsGkAtoms::name, nsDependentAtomString(aName), IgnoreErrors()); 582 } 583 // This is not needed, but useful for debugging. 584 el->SetAttr(nsGkAtoms::type, 585 nsDependentAtomString(nsCSSPseudoElements::GetPseudoAtom(aType)), 586 IgnoreErrors()); 587 return el.forget(); 588 } 589 590 static bool SetProp(StyleLockedDeclarationBlock* aDecls, Document* aDoc, 591 NonCustomCSSPropertyId aProp, const nsACString& aValue) { 592 return Servo_DeclarationBlock_SetPropertyById( 593 aDecls, aProp, &aValue, 594 /* is_important = */ false, aDoc->DefaultStyleAttrURLData(), 595 StyleParsingMode::DEFAULT, eCompatibility_FullStandards, 596 &aDoc->EnsureCSSLoader(), StyleCssRuleType::Style, {}); 597 } 598 599 static bool SetProp(StyleLockedDeclarationBlock* aDecls, Document*, 600 NonCustomCSSPropertyId aProp, float aLength, 601 nsCSSUnit aUnit) { 602 return Servo_DeclarationBlock_SetLengthValue(aDecls, aProp, aLength, aUnit); 603 } 604 605 static bool SetProp(StyleLockedDeclarationBlock* aDecls, Document*, 606 NonCustomCSSPropertyId aProp, 607 const CSSToCSSMatrix4x4Flagged& aM) { 608 MOZ_ASSERT(aProp == eCSSProperty_transform); 609 AutoTArray<StyleTransformOperation, 1> ops; 610 ops.AppendElement( 611 StyleTransformOperation::Matrix3D(StyleGenericMatrix3D<StyleNumber>{ 612 aM._11, aM._12, aM._13, aM._14, aM._21, aM._22, aM._23, aM._24, 613 aM._31, aM._32, aM._33, aM._34, aM._41, aM._42, aM._43, aM._44})); 614 return Servo_DeclarationBlock_SetTransform(aDecls, aProp, &ops); 615 } 616 617 static bool SetProp(StyleLockedDeclarationBlock* aDecls, Document* aDoc, 618 NonCustomCSSPropertyId aProp, 619 const StyleWritingModeProperty aWM) { 620 return Servo_DeclarationBlock_SetKeywordValue(aDecls, aProp, (int32_t)aWM); 621 } 622 623 static bool SetProp(StyleLockedDeclarationBlock* aDecls, Document* aDoc, 624 NonCustomCSSPropertyId aProp, 625 const StyleDirection aDirection) { 626 return Servo_DeclarationBlock_SetKeywordValue(aDecls, aProp, 627 (int32_t)aDirection); 628 } 629 630 static bool SetProp(StyleLockedDeclarationBlock* aDecls, Document* aDoc, 631 NonCustomCSSPropertyId aProp, 632 const StyleTextOrientation aTextOrientation) { 633 return Servo_DeclarationBlock_SetKeywordValue(aDecls, aProp, 634 (int32_t)aTextOrientation); 635 } 636 637 static bool SetProp(StyleLockedDeclarationBlock* aDecls, Document* aDoc, 638 NonCustomCSSPropertyId aProp, const StyleBlend aBlend) { 639 return Servo_DeclarationBlock_SetKeywordValue(aDecls, aProp, (int32_t)aBlend); 640 } 641 642 static bool SetProp( 643 StyleLockedDeclarationBlock* aDecls, Document*, 644 NonCustomCSSPropertyId aProp, 645 const StyleOwnedSlice<mozilla::StyleFilter>& aBackdropFilters) { 646 return Servo_DeclarationBlock_SetBackdropFilter(aDecls, aProp, 647 &aBackdropFilters); 648 } 649 650 static bool SetProp(StyleLockedDeclarationBlock* aDecls, Document*, 651 NonCustomCSSPropertyId aProp, 652 const StyleColorScheme& aColorScheme) { 653 return Servo_DeclarationBlock_SetColorScheme(aDecls, aProp, &aColorScheme); 654 } 655 656 static StyleLockedDeclarationBlock* EnsureRule( 657 RefPtr<StyleLockedDeclarationBlock>& aRule) { 658 if (!aRule) { 659 aRule = Servo_DeclarationBlock_CreateEmpty().Consume(); 660 } 661 return aRule.get(); 662 } 663 664 static nsTArray<Keyframe> BuildGroupKeyframes( 665 Document* aDoc, const CSSToCSSMatrix4x4Flagged& aTransform, 666 const nsSize& aSize, const StyleOwnedSlice<StyleFilter>& aBackdropFilters) { 667 Keyframe firstKeyframe; 668 firstKeyframe.mOffset = Some(0.0); 669 PropertyValuePair transform{ 670 CSSPropertyId(eCSSProperty_transform), 671 Servo_DeclarationBlock_CreateEmpty().Consume(), 672 }; 673 SetProp(transform.mServoDeclarationBlock, aDoc, eCSSProperty_transform, 674 aTransform); 675 PropertyValuePair width{ 676 CSSPropertyId(eCSSProperty_width), 677 Servo_DeclarationBlock_CreateEmpty().Consume(), 678 }; 679 CSSSize cssSize = CSSSize::FromAppUnits(aSize); 680 SetProp(width.mServoDeclarationBlock, aDoc, eCSSProperty_width, cssSize.width, 681 eCSSUnit_Pixel); 682 PropertyValuePair height{ 683 CSSPropertyId(eCSSProperty_height), 684 Servo_DeclarationBlock_CreateEmpty().Consume(), 685 }; 686 SetProp(height.mServoDeclarationBlock, aDoc, eCSSProperty_height, 687 cssSize.height, eCSSUnit_Pixel); 688 PropertyValuePair backdropFilters{ 689 CSSPropertyId(eCSSProperty_backdrop_filter), 690 Servo_DeclarationBlock_CreateEmpty().Consume(), 691 }; 692 SetProp(backdropFilters.mServoDeclarationBlock, aDoc, 693 eCSSProperty_backdrop_filter, aBackdropFilters); 694 firstKeyframe.mPropertyValues.AppendElement(std::move(transform)); 695 firstKeyframe.mPropertyValues.AppendElement(std::move(width)); 696 firstKeyframe.mPropertyValues.AppendElement(std::move(height)); 697 firstKeyframe.mPropertyValues.AppendElement(std::move(backdropFilters)); 698 699 Keyframe lastKeyframe; 700 lastKeyframe.mOffset = Some(1.0); 701 lastKeyframe.mPropertyValues.AppendElement( 702 PropertyValuePair{CSSPropertyId(eCSSProperty_transform)}); 703 lastKeyframe.mPropertyValues.AppendElement( 704 PropertyValuePair{CSSPropertyId(eCSSProperty_width)}); 705 lastKeyframe.mPropertyValues.AppendElement( 706 PropertyValuePair{CSSPropertyId(eCSSProperty_height)}); 707 lastKeyframe.mPropertyValues.AppendElement( 708 PropertyValuePair{CSSPropertyId(eCSSProperty_backdrop_filter)}); 709 710 nsTArray<Keyframe> result; 711 result.AppendElement(std::move(firstKeyframe)); 712 result.AppendElement(std::move(lastKeyframe)); 713 return result; 714 } 715 716 bool ViewTransition::GetGroupKeyframes( 717 nsAtom* aAnimationName, const StyleComputedTimingFunction& aTimingFunction, 718 nsTArray<Keyframe>& aResult) { 719 MOZ_ASSERT(StringBeginsWith(nsDependentAtomString(aAnimationName), 720 kGroupAnimPrefix)); 721 RefPtr<nsAtom> transitionName = NS_Atomize(Substring( 722 nsDependentAtomString(aAnimationName), kGroupAnimPrefix.Length())); 723 auto* el = mNamedElements.Get(transitionName); 724 if (NS_WARN_IF(!el) || NS_WARN_IF(el->mGroupKeyframes.IsEmpty())) { 725 return false; 726 } 727 aResult = el->mGroupKeyframes.Clone(); 728 // We assign the timing function always to make sure we don't use the default 729 // linear timing function. 730 MOZ_ASSERT(aResult.Length() == 2); 731 aResult[0].mTimingFunction = Some(aTimingFunction); 732 aResult[1].mTimingFunction = Some(aTimingFunction); 733 return true; 734 } 735 736 // Matches the class list in the captured element. 737 // https://drafts.csswg.org/css-view-transitions-2/#pseudo-element-class-additions 738 bool ViewTransition::MatchClassList( 739 nsAtom* aTransitionName, 740 const nsTArray<StyleAtom>& aPtNameAndClassSelector) const { 741 MOZ_ASSERT(aPtNameAndClassSelector.Length() > 1); 742 743 const auto* el = mNamedElements.Get(aTransitionName); 744 MOZ_ASSERT(el); 745 const auto& classList = el->mClassList._0.AsSpan(); 746 auto hasClass = [&classList](nsAtom* aClass) { 747 // LInear search. The css class list shouldn't be very large in most cases. 748 for (const auto& ident : classList) { 749 if (ident.AsAtom() == aClass) { 750 return true; 751 } 752 } 753 return false; 754 }; 755 756 // A named view transition pseudo-element selector which has one or more 757 // <custom-ident> values in its <pt-class-selector> would only match an 758 // element if the class list value in named elements for the pseudo-element’s 759 // view-transition-name contains all of those values. 760 // i.e. |aPtNameAndClassSelector| should be a subset of |mClassList|. 761 for (const auto& atom : Span(aPtNameAndClassSelector).From(1)) { 762 if (!hasClass(atom.AsAtom())) { 763 return false; 764 } 765 } 766 return true; 767 } 768 769 // In general, we are trying to generate the following pseudo-elements tree: 770 // ::-moz-snapshot-containing-block 771 // └─ ::view-transition 772 // ├─ ::view-transition-group(name) 773 // │ └─ ::view-transition-image-pair(name) 774 // │ ├─ ::view-transition-old(name) 775 // │ └─ ::view-transition-new(name) 776 // └─ ...other groups... 777 // 778 // ::-moz-snapshot-containing-block is the top-layer of the tree. It is the 779 // wrapper of the view transition pseudo-elements tree for the snapshot 780 // containing block concept. And it is the child of the document element. 781 // https://drafts.csswg.org/css-view-transitions-1/#setup-transition-pseudo-elements 782 void ViewTransition::SetupTransitionPseudoElements() { 783 MOZ_ASSERT(!mSnapshotContainingBlock); 784 785 nsAutoScriptBlocker scriptBlocker; 786 787 RefPtr docElement = mDocument->GetRootElement(); 788 if (!docElement) { 789 return; 790 } 791 792 // We don't need to notify while constructing the tree. 793 constexpr bool kNotify = false; 794 795 // Step 1 is a declaration. 796 797 // Step 2: Set document's show view transition tree to true. 798 // (we lazily create this pseudo-element so we don't need the flag for now at 799 // least). 800 // Note: Use mSnapshotContainingBlock to wrap the pseudo-element tree. 801 mSnapshotContainingBlock = MakePseudo( 802 *mDocument, PseudoStyleType::mozSnapshotContainingBlock, nullptr); 803 RefPtr<Element> root = 804 MakePseudo(*mDocument, PseudoStyleType::viewTransition, nullptr); 805 mSnapshotContainingBlock->AppendChildTo(root, kNotify, IgnoreErrors()); 806 #ifdef DEBUG 807 // View transition pseudos don't care about frame tree ordering, so can be 808 // restyled just fine. 809 mSnapshotContainingBlock->SetProperty(nsGkAtoms::restylableAnonymousNode, 810 reinterpret_cast<void*>(true)); 811 #endif 812 813 MOZ_ASSERT(mNames.Length() == mNamedElements.Count()); 814 // Step 3: For each transitionName -> capturedElement of transition’s named 815 // elements: 816 for (nsAtom* transitionName : mNames) { 817 CapturedElement& capturedElement = *mNamedElements.Get(transitionName); 818 // Let group be a new ::view-transition-group(), with its view transition 819 // name set to transitionName. 820 RefPtr<Element> group = MakePseudo( 821 *mDocument, PseudoStyleType::viewTransitionGroup, transitionName); 822 // Append group to transition’s transition root pseudo-element. 823 root->AppendChildTo(group, kNotify, IgnoreErrors()); 824 // Let imagePair be a new ::view-transition-image-pair(), with its view 825 // transition name set to transitionName. 826 RefPtr<Element> imagePair = MakePseudo( 827 *mDocument, PseudoStyleType::viewTransitionImagePair, transitionName); 828 // Append imagePair to group. 829 group->AppendChildTo(imagePair, kNotify, IgnoreErrors()); 830 // If capturedElement's old image is not null, then: 831 if (capturedElement.mOldState.mTriedImage) { 832 // Let old be a new ::view-transition-old(), with its view transition 833 // name set to transitionName, displaying capturedElement's old image as 834 // its replaced content. 835 RefPtr<Element> old = MakePseudo( 836 *mDocument, PseudoStyleType::viewTransitionOld, transitionName); 837 // Append old to imagePair. 838 imagePair->AppendChildTo(old, kNotify, IgnoreErrors()); 839 } else { 840 // Moved around for simplicity. If capturedElement's old image is null, 841 // then: Assert: capturedElement's new element is not null. 842 MOZ_ASSERT(capturedElement.mNewElement); 843 // Set capturedElement's image animation name rule to a new ... 844 auto* rule = EnsureRule(capturedElement.mNewRule); 845 SetProp(rule, mDocument, eCSSProperty_animation_name, 846 "-ua-view-transition-fade-in"_ns); 847 } 848 // If capturedElement's new element is not null, then: 849 if (capturedElement.mNewElement) { 850 // Let new be a new ::view-transition-new(), with its view transition 851 // name set to transitionName. 852 RefPtr<Element> new_ = MakePseudo( 853 *mDocument, PseudoStyleType::viewTransitionNew, transitionName); 854 // Append new to imagePair. 855 imagePair->AppendChildTo(new_, kNotify, IgnoreErrors()); 856 } else { 857 // Moved around from the next step for simplicity. 858 // Assert: capturedElement's old image is not null. 859 // Set capturedElement's image animation name rule to a new CSSStyleRule 860 // representing the following CSS, and append it to document’s dynamic 861 // view transition style sheet: 862 MOZ_ASSERT(capturedElement.mOldState.mTriedImage); 863 SetProp(EnsureRule(capturedElement.mOldRule), mDocument, 864 eCSSProperty_animation_name, "-ua-view-transition-fade-out"_ns); 865 866 // Moved around from "update pseudo-element styles" because it's a one 867 // time operation. 868 auto* rule = EnsureRule(capturedElement.mGroupRule); 869 auto oldRect = 870 CSSPixel::FromAppUnits(capturedElement.mOldState.mBorderBoxSize); 871 SetProp(rule, mDocument, eCSSProperty_width, oldRect.width, 872 eCSSUnit_Pixel); 873 SetProp(rule, mDocument, eCSSProperty_height, oldRect.height, 874 eCSSUnit_Pixel); 875 SetProp(rule, mDocument, eCSSProperty_transform, 876 capturedElement.mOldState.mTransform); 877 SetProp(rule, mDocument, eCSSProperty_writing_mode, 878 capturedElement.mOldState.mWritingMode); 879 SetProp(rule, mDocument, eCSSProperty_direction, 880 capturedElement.mOldState.mDirection); 881 SetProp(rule, mDocument, eCSSProperty_text_orientation, 882 capturedElement.mOldState.mTextOrientation); 883 SetProp(rule, mDocument, eCSSProperty_mix_blend_mode, 884 capturedElement.mOldState.mMixBlendMode); 885 SetProp(rule, mDocument, eCSSProperty_backdrop_filter, 886 capturedElement.mOldState.mBackdropFilters); 887 SetProp(rule, mDocument, eCSSProperty_color_scheme, 888 capturedElement.mOldState.mColorScheme); 889 } 890 // If both of capturedElement's old image and new element are not null, 891 // then: 892 if (capturedElement.mOldState.mTriedImage && capturedElement.mNewElement) { 893 nsAutoCString dynamicAnimationName; 894 nsStyleUtil::AppendQuotedCSSString( 895 NS_ConvertUTF16toUTF8(kGroupAnimPrefix + 896 nsDependentAtomString(transitionName)), 897 dynamicAnimationName); 898 capturedElement.mGroupKeyframes = 899 BuildGroupKeyframes(mDocument, capturedElement.mOldState.mTransform, 900 capturedElement.mOldState.mBorderBoxSize, 901 capturedElement.mOldState.mBackdropFilters); 902 // Set capturedElement's group animation name rule to ... 903 SetProp(EnsureRule(capturedElement.mGroupRule), mDocument, 904 eCSSProperty_animation_name, dynamicAnimationName); 905 906 // Set capturedElement's image pair isolation rule to ... 907 SetProp(EnsureRule(capturedElement.mImagePairRule), mDocument, 908 eCSSProperty_isolation, "isolate"_ns); 909 910 // Set capturedElement's image animation name rule to ... 911 SetProp( 912 EnsureRule(capturedElement.mOldRule), mDocument, 913 eCSSProperty_animation_name, 914 "-ua-view-transition-fade-out, -ua-mix-blend-mode-plus-lighter"_ns); 915 SetProp( 916 EnsureRule(capturedElement.mNewRule), mDocument, 917 eCSSProperty_animation_name, 918 "-ua-view-transition-fade-in, -ua-mix-blend-mode-plus-lighter"_ns); 919 } 920 } 921 BindContext context(*docElement, BindContext::ForNativeAnonymous); 922 if (NS_FAILED(mSnapshotContainingBlock->BindToTree(context, *docElement))) { 923 mSnapshotContainingBlock->UnbindFromTree(); 924 mSnapshotContainingBlock = nullptr; 925 return; 926 } 927 if (mDocument->DevToolsAnonymousAndShadowEventsEnabled()) { 928 mSnapshotContainingBlock->QueueDevtoolsAnonymousEvent( 929 /* aIsRemove = */ false); 930 } 931 if (PresShell* ps = mDocument->GetPresShell()) { 932 ps->ContentAppended(mSnapshotContainingBlock, {}); 933 } 934 } 935 936 // https://drafts.csswg.org/css-view-transitions-1/#style-transition-pseudo-elements-algorithm 937 bool ViewTransition::UpdatePseudoElementStyles(bool aNeedsInvalidation) { 938 // 1. For each transitionName -> capturedElement of transition's "named 939 // elements". 940 for (auto& entry : mNamedElements) { 941 nsAtom* transitionName = entry.GetKey(); 942 CapturedElement& capturedElement = *entry.GetData(); 943 // If capturedElement's new element is null, then: 944 // We already did this in SetupTransitionPseudoElements(). 945 if (!capturedElement.mNewElement) { 946 continue; 947 } 948 // Otherwise. 949 // Return failure if any of the following conditions is true: 950 // * capturedElement's new element has a flat tree ancestor that skips its 951 // contents. 952 // * capturedElement's new element is not rendered. 953 // * capturedElement has more than one box fragment. 954 nsIFrame* frame = capturedElement.mNewElement->GetPrimaryFrame(); 955 if (!frame || frame->IsHiddenByContentVisibilityOnAnyAncestor() || 956 frame->GetPrevContinuation() || frame->GetNextContinuation()) { 957 return false; 958 } 959 auto* rule = EnsureRule(capturedElement.mGroupRule); 960 // Note: mInitialSnapshotContainingBlockSize should be the same as the 961 // current snapshot containing block size because the caller checks it 962 // before calling us. 963 const auto newBorderBoxSize = 964 CapturedRect(frame, mInitialSnapshotContainingBlockSize, 965 CapturedRectType::BorderBox) 966 .Size(); 967 auto size = CSSPixel::FromAppUnits(newBorderBoxSize); 968 // NOTE(emilio): Intentionally not short-circuiting. Int cast is needed to 969 // silence warning. 970 bool groupStyleChanged = 971 int(SetProp(rule, mDocument, eCSSProperty_width, size.width, 972 eCSSUnit_Pixel)) | 973 SetProp(rule, mDocument, eCSSProperty_height, size.height, 974 eCSSUnit_Pixel) | 975 SetProp(rule, mDocument, eCSSProperty_transform, 976 EffectiveTransform(frame)) | 977 SetProp(rule, mDocument, eCSSProperty_writing_mode, 978 frame->StyleVisibility()->mWritingMode) | 979 SetProp(rule, mDocument, eCSSProperty_direction, 980 frame->StyleVisibility()->mDirection) | 981 SetProp(rule, mDocument, eCSSProperty_text_orientation, 982 frame->StyleVisibility()->mTextOrientation) | 983 SetProp(rule, mDocument, eCSSProperty_mix_blend_mode, 984 frame->StyleEffects()->mMixBlendMode) | 985 SetProp(rule, mDocument, eCSSProperty_backdrop_filter, 986 frame->StyleEffects()->mBackdropFilters) | 987 SetProp(rule, mDocument, eCSSProperty_color_scheme, 988 frame->StyleUI()->mColorScheme); 989 if (groupStyleChanged && aNeedsInvalidation) { 990 auto* pseudo = FindPseudo(PseudoStyleRequest( 991 PseudoStyleType::viewTransitionGroup, transitionName)); 992 MOZ_ASSERT(pseudo); 993 // TODO(emilio): Maybe we need something more than recascade? But I don't 994 // see how off-hand. 995 nsLayoutUtils::PostRestyleEvent(pseudo, RestyleHint::RECASCADE_SELF, 996 nsChangeHint(0)); 997 } 998 999 // 5. Live capturing (nothing to do here regarding the capture itself, but 1000 // if the size has changed, then we need to invalidate the new frame). 1001 const auto newSnapshotRect = 1002 CapturedRect(frame, mInitialSnapshotContainingBlockSize, 1003 CapturedRectType::InkOverflowBox); 1004 auto oldRect = capturedElement.mNewSnapshotRect; 1005 capturedElement.mNewSnapshotRect = newSnapshotRect; 1006 capturedElement.mNewBorderBoxSize = newBorderBoxSize; 1007 if (!oldRect.IsEqualEdges(capturedElement.mNewSnapshotRect) && 1008 aNeedsInvalidation) { 1009 frame->PresShell()->FrameNeedsReflow( 1010 frame, IntrinsicDirty::FrameAndAncestors, NS_FRAME_IS_DIRTY); 1011 } 1012 } 1013 return true; 1014 } 1015 1016 // https://drafts.csswg.org/css-view-transitions-1/#activate-view-transition 1017 void ViewTransition::Activate() { 1018 AUTO_PROFILER_FLOW_MARKER("ViewTransition::Activate", LAYOUT, 1019 Flow::FromPointer(this)); 1020 // Step 1: If transition's phase is "done", then return. 1021 if (mPhase == Phase::Done) { 1022 return; 1023 } 1024 1025 // Step 2: Set transition’s relevant global object’s associated document’s 1026 // rendering suppression for view transitions to false. 1027 mDocument->SetRenderingSuppressedForViewTransitions(false); 1028 1029 // Step 3: If transition's initial snapshot containing block size is not 1030 // equal to the snapshot containing block size, then skip the view transition 1031 // for transition, and return. 1032 if (mInitialSnapshotContainingBlockSize != 1033 SnapshotContainingBlockRect().Size()) { 1034 return SkipTransition(SkipTransitionReason::Resize); 1035 } 1036 1037 // Step 4: Capture the new state for transition. 1038 // Step 5 is done along step 4 for performance. 1039 if (auto skipReason = CaptureNewState()) { 1040 // We clear named elements to not leave lingering "captured in a view 1041 // transition" state. 1042 ClearNamedElements(); 1043 // If failure is returned, then skip the view transition for transition... 1044 return SkipTransition(*skipReason); 1045 } 1046 1047 // Step 6: Setup transition pseudo-elements for transition. 1048 SetupTransitionPseudoElements(); 1049 1050 // Step 7: Update pseudo-element styles for transition. 1051 // We don't need to invalidate the pseudo-element styles since we just 1052 // generated them. 1053 if (!UpdatePseudoElementStyles(/* aNeedsInvalidation = */ false)) { 1054 // If failure is returned, then skip the view transition for transition 1055 // with an "InvalidStateError" DOMException in transition's relevant Realm, 1056 // and return. 1057 return SkipTransition(SkipTransitionReason::PseudoUpdateFailure); 1058 } 1059 1060 // Step 8: Set transition's phase to "animating". 1061 mPhase = Phase::Animating; 1062 // Step 9: Resolve transition's ready promise. 1063 if (Promise* ready = GetReady(IgnoreErrors())) { 1064 ready->MaybeResolveWithUndefined(); 1065 } 1066 1067 // Once this view transition is activated, we have to perform the pending 1068 // operations periodically. 1069 MOZ_ASSERT(mDocument); 1070 mDocument->EnsureViewTransitionOperationsHappen(); 1071 } 1072 1073 // https://drafts.csswg.org/css-view-transitions/#perform-pending-transition-operations 1074 void ViewTransition::PerformPendingOperations() { 1075 MOZ_ASSERT(mDocument); 1076 MOZ_ASSERT(mDocument->GetActiveViewTransition() == this); 1077 AUTO_PROFILER_FLOW_MARKER("ViewTransition::PerformPendingOperations", LAYOUT, 1078 Flow::FromPointer(this)); 1079 1080 // Flush the update callback queue. 1081 // Note: this ensures that any changes to the DOM scheduled by other skipped 1082 // transitions are done before the old state for this transition is captured. 1083 // https://github.com/w3c/csswg-drafts/issues/11943 1084 RefPtr doc = mDocument; 1085 doc->FlushViewTransitionUpdateCallbackQueue(); 1086 1087 switch (mPhase) { 1088 case Phase::PendingCapture: 1089 return Setup(); 1090 case Phase::Animating: 1091 return HandleFrame(); 1092 default: 1093 break; 1094 } 1095 } 1096 1097 // https://drafts.csswg.org/css-view-transitions/#snapshot-containing-block 1098 nsRect ViewTransition::SnapshotContainingBlockRect(nsPresContext* aPc) { 1099 return aPc ? nsRect(aPc->GetVisibleArea().TopLeft(), 1100 aPc->GetSizeForViewportUnits()) 1101 : nsRect(); 1102 } 1103 1104 // https://drafts.csswg.org/css-view-transitions/#snapshot-containing-block 1105 nsRect ViewTransition::SnapshotContainingBlockRect() const { 1106 nsPresContext* pc = mDocument->GetPresContext(); 1107 return SnapshotContainingBlockRect(pc); 1108 } 1109 1110 nsRect ViewTransition::CapturedInkOverflowRectForFrame(nsIFrame* aFrame, 1111 bool aIsRoot) { 1112 auto snapshotCb = SnapshotContainingBlockRect(aFrame->PresContext()); 1113 if (aIsRoot) { 1114 return snapshotCb; 1115 } 1116 return CapturedRect(aFrame, snapshotCb.Size(), 1117 CapturedRectType::InkOverflowBox); 1118 } 1119 1120 Element* ViewTransition::FindPseudo(const PseudoStyleRequest& aRequest) const { 1121 Element* root = GetViewTransitionTreeRoot(); 1122 if (!root) { 1123 return nullptr; 1124 } 1125 MOZ_ASSERT(root->GetPseudoElementType() == PseudoStyleType::viewTransition); 1126 1127 if (aRequest.mType == PseudoStyleType::viewTransition) { 1128 return root; 1129 } 1130 1131 // Linear search ::view-transition-group by |aRequest.mIdentifier|. 1132 // Note: perhaps we can add a hashtable to improve the performance if it's 1133 // common that there are a lot of view-transition-names. 1134 Element* group = root->GetFirstElementChild(); 1135 for (; group; group = group->GetNextElementSibling()) { 1136 MOZ_ASSERT(group->HasName(), 1137 "The generated ::view-transition-group() should have a name"); 1138 nsAtom* name = group->GetParsedAttr(nsGkAtoms::name)->GetAtomValue(); 1139 if (name == aRequest.mIdentifier) { 1140 break; 1141 } 1142 } 1143 1144 // No one specifies view-transition-name or we mismatch all names. 1145 if (!group) { 1146 return nullptr; 1147 } 1148 1149 if (aRequest.mType == PseudoStyleType::viewTransitionGroup) { 1150 return group; 1151 } 1152 1153 Element* imagePair = group->GetFirstElementChild(); 1154 MOZ_ASSERT(imagePair, "::view-transition-image-pair() should exist always"); 1155 if (aRequest.mType == PseudoStyleType::viewTransitionImagePair) { 1156 return imagePair; 1157 } 1158 1159 Element* child = imagePair->GetFirstElementChild(); 1160 // Neither ::view-transition-old() nor ::view-transition-new() doesn't exist. 1161 if (!child) { 1162 return nullptr; 1163 } 1164 1165 // Check if the first element matches our request. 1166 const PseudoStyleType type = child->GetPseudoElementType(); 1167 if (type == aRequest.mType) { 1168 return child; 1169 } 1170 1171 // Since the second child is either ::view-transition-new() or nullptr, so we 1172 // can reject viewTransitionOld request here. 1173 if (aRequest.mType == PseudoStyleType::viewTransitionOld) { 1174 return nullptr; 1175 } 1176 1177 child = child->GetNextElementSibling(); 1178 MOZ_ASSERT(aRequest.mType == PseudoStyleType::viewTransitionNew); 1179 MOZ_ASSERT(!child || !child->GetNextElementSibling(), 1180 "No more psuedo elements in this subtree"); 1181 return child; 1182 } 1183 1184 const StyleLockedDeclarationBlock* ViewTransition::GetDynamicRuleFor( 1185 const Element& aElement) const { 1186 if (!aElement.HasName()) { 1187 return nullptr; 1188 } 1189 nsAtom* name = aElement.GetParsedAttr(nsGkAtoms::name)->GetAtomValue(); 1190 auto* capture = mNamedElements.Get(name); 1191 if (!capture) { 1192 return nullptr; 1193 } 1194 1195 switch (aElement.GetPseudoElementType()) { 1196 case PseudoStyleType::viewTransitionNew: 1197 return capture->mNewRule.get(); 1198 case PseudoStyleType::viewTransitionOld: 1199 return capture->mOldRule.get(); 1200 case PseudoStyleType::viewTransitionImagePair: 1201 return capture->mImagePairRule.get(); 1202 case PseudoStyleType::viewTransitionGroup: 1203 return capture->mGroupRule.get(); 1204 default: 1205 return nullptr; 1206 } 1207 } 1208 1209 // This function collects frames in the same stacking context. We only put 1210 // the frames which may create a new create stacking context in the list because 1211 // they (and their descendants) are candidates for captured elements (i.e. with 1212 // a valid view-transition-name). 1213 static void CollectDescendantStackingContexts(nsIFrame* aStackingContextRoot, 1214 nsTArray<nsIFrame*>& aList) { 1215 for (auto& [list, id] : aStackingContextRoot->ChildLists()) { 1216 for (nsIFrame* f : list) { 1217 // FIXME: We probably can skip more frames, e.g. scrollbar or scrollcorner 1218 // to save some time. 1219 1220 // We only want to sort the frames form a new stacking context in the 1221 // current stacking context (including the root stacking context). If it 1222 // creates a new stacking context, its descendants should be traversed 1223 // (and sorted) independently. Also, if a frame has view-transition-name, 1224 // it should create a stacking context as well, so this check must include 1225 // frames with view-transition-name. 1226 // Note: the root frame may not be the root element, so we still have to 1227 // check if |f| is the root element. 1228 if (f->Style()->IsRootElementStyle() || f->IsStackingContext()) { 1229 aList.AppendElement(f); 1230 // We will continue to traverse its descendants after we sort |aList|. 1231 continue; 1232 } 1233 1234 // If any flat tree ancestor of this element skips its contents, then 1235 // continue. 1236 if (f->IsHiddenByContentVisibilityOnAnyAncestor()) { 1237 continue; 1238 } 1239 1240 // If |insertionFrame| doesn't create stacking context, we have to check 1241 // its descendants because they are still in the current stacking context. 1242 CollectDescendantStackingContexts(f, aList); 1243 } 1244 } 1245 } 1246 1247 struct ZOrderComparator { 1248 bool LessThan(const nsIFrame* aLeft, const nsIFrame* aRight) const { 1249 return aLeft->ZIndex().valueOr(0) < aRight->ZIndex().valueOr(0); 1250 } 1251 }; 1252 1253 template <typename Callback> 1254 static bool ForEachDescendantWithViewTransitionNameInPaintOrder( 1255 nsIFrame* aFrame, const Callback& aCb) { 1256 // Call the callback if it specifies view-transition-name. 1257 if (!aFrame->StyleUIReset()->mViewTransitionName.IsNone() && !aCb(aFrame)) { 1258 return false; 1259 } 1260 1261 nsTArray<nsIFrame*> descendantStackingContexts; 1262 CollectDescendantStackingContexts(aFrame, descendantStackingContexts); 1263 // Sort by z-index to make sure we call the callback in paint order. 1264 descendantStackingContexts.StableSort(ZOrderComparator()); 1265 1266 for (nsIFrame* f : descendantStackingContexts) { 1267 if (!ForEachDescendantWithViewTransitionNameInPaintOrder(f, aCb)) { 1268 return false; 1269 } 1270 } 1271 return true; 1272 } 1273 1274 template <typename Callback> 1275 static void ForEachFrameWithViewTransitionName(Document* aDoc, 1276 const Callback& aCb) { 1277 PresShell* ps = aDoc->GetPresShell(); 1278 if (!ps) { 1279 return; 1280 } 1281 nsIFrame* root = ps->GetRootFrame(); 1282 if (!root) { 1283 return; 1284 } 1285 ForEachDescendantWithViewTransitionNameInPaintOrder(root, aCb); 1286 } 1287 1288 // https://drafts.csswg.org/css-view-transitions/#capture-the-old-state 1289 Maybe<SkipTransitionReason> ViewTransition::CaptureOldState() { 1290 MOZ_ASSERT(mNamedElements.IsEmpty()); 1291 1292 // Steps 1/2 are variable declarations. 1293 // Step 3: Let usedTransitionNames be a new set of strings. 1294 nsTHashSet<nsAtom*> usedTransitionNames; 1295 // Step 4: Let captureElements be a new list of elements. 1296 OldCaptureFramesArray captureElements; 1297 1298 // Step 5: If the snapshot containing block size exceeds an 1299 // implementation-defined maximum, then return failure. 1300 // TODO(emilio): Implement a maximum if we deem it needed. 1301 // 1302 // Step 6: Set transition's initial snapshot containing block size to the 1303 // snapshot containing block size. 1304 mInitialSnapshotContainingBlockSize = SnapshotContainingBlockRect().Size(); 1305 1306 // Step 7: For each element of every element that is connected, and has a node 1307 // document equal to document, in paint order: 1308 Maybe<SkipTransitionReason> result; 1309 ForEachFrameWithViewTransitionName(mDocument, [&](nsIFrame* aFrame) { 1310 RefPtr<nsAtom> name = DocumentScopedTransitionNameFor(aFrame); 1311 if (!name) { 1312 // As a fast path we check for v-t-n first. 1313 // If transitionName is none, or element is not rendered, then continue. 1314 return true; 1315 } 1316 if (aFrame->GetPrevContinuation() || aFrame->GetNextContinuation()) { 1317 // If element has more than one box fragment, then continue. 1318 return true; 1319 } 1320 if (!usedTransitionNames.EnsureInserted(name)) { 1321 // We don't expect to see a duplicate transition name when using 1322 // match-element. 1323 MOZ_ASSERT(!aFrame->StyleUIReset()->mViewTransitionName.IsMatchElement()); 1324 1325 // If usedTransitionNames contains transitionName, then return failure. 1326 result.emplace( 1327 SkipTransitionReason::DuplicateTransitionNameCapturingOldState); 1328 return false; 1329 } 1330 SetCaptured(aFrame, true, name.get()); 1331 captureElements.AppendElement(std::make_pair(aFrame, std::move(name))); 1332 return true; 1333 }); 1334 1335 if (result) { 1336 for (auto& [f, name] : captureElements) { 1337 SetCaptured(f, false, nullptr); 1338 } 1339 return result; 1340 } 1341 1342 // Step 8: For each element in captureElements: 1343 // Step 9: For each element in captureElements, set element's captured 1344 // in a view transition to false. 1345 for (auto& [f, name] : captureElements) { 1346 MOZ_ASSERT(f); 1347 MOZ_ASSERT(f->GetContent()->IsElement()); 1348 // Capture the view-transition-class. 1349 // https://drafts.csswg.org/css-view-transitions-2/#vt-class-algorithms 1350 auto capture = MakeUnique<CapturedElement>( 1351 f, mInitialSnapshotContainingBlockSize, DocumentScopedClassListFor(f)); 1352 mNamedElements.InsertOrUpdate(name, std::move(capture)); 1353 mNames.AppendElement(name); 1354 } 1355 1356 if (!captureElements.IsEmpty()) { 1357 AutoRestore guard{mOldCaptureElements}; 1358 mOldCaptureElements = &captureElements; 1359 // When snapshotting an iframe, we need to paint from the root subdoc. 1360 if (RefPtr<PresShell> ps = 1361 nsContentUtils::GetInProcessSubtreeRootDocument(mDocument) 1362 ->GetPresShell()) { 1363 // Build a display list and send it to WR in order to perform the 1364 // capturing of old content. 1365 if (RefPtr widget = ps->GetRootWidget()) { 1366 VT_LOG("ViewTransitions::CaptureOldState(), requesting composite"); 1367 ps->PaintAndRequestComposite(ps->GetRootFrame(), 1368 widget->GetWindowRenderer(), 1369 PaintFlags::PaintCompositeOffscreen); 1370 VT_LOG("ViewTransitions::CaptureOldState(), requesting composite end"); 1371 } 1372 } 1373 } 1374 1375 for (auto& [f, name] : captureElements) { 1376 SetCaptured(f, false, nullptr); 1377 } 1378 return result; 1379 } 1380 1381 // https://drafts.csswg.org/css-view-transitions-1/#capture-the-new-state 1382 Maybe<SkipTransitionReason> ViewTransition::CaptureNewState() { 1383 nsTHashSet<nsAtom*> usedTransitionNames; 1384 Maybe<SkipTransitionReason> result; 1385 ForEachFrameWithViewTransitionName(mDocument, [&](nsIFrame* aFrame) { 1386 // As a fast path we check for v-t-n first. 1387 RefPtr<nsAtom> name = DocumentScopedTransitionNameFor(aFrame); 1388 if (!name) { 1389 return true; 1390 } 1391 if (aFrame->GetPrevContinuation() || aFrame->GetNextContinuation()) { 1392 // If element has more than one box fragment, then continue. 1393 return true; 1394 } 1395 if (!usedTransitionNames.EnsureInserted(name)) { 1396 // We don't expect to see a duplicate transition name when using 1397 // match-element. 1398 MOZ_ASSERT(!aFrame->StyleUIReset()->mViewTransitionName.IsMatchElement()); 1399 result.emplace( 1400 SkipTransitionReason::DuplicateTransitionNameCapturingNewState); 1401 return false; 1402 } 1403 bool wasPresent = true; 1404 auto& capturedElement = mNamedElements.LookupOrInsertWith(name, [&] { 1405 wasPresent = false; 1406 return MakeUnique<CapturedElement>(); 1407 }); 1408 if (!wasPresent) { 1409 mNames.AppendElement(name); 1410 } 1411 capturedElement->mNewElement = aFrame->GetContent()->AsElement(); 1412 auto capturedRect = 1413 CapturedRect(aFrame, mInitialSnapshotContainingBlockSize, 1414 CapturedRectType::InkOverflowBox); 1415 // Note: mInitialSnapshotContainingBlockSize should be the same as the 1416 // current snapshot containing block size at this moment because the caller 1417 // checks it before calling us. 1418 capturedElement->mNewSnapshotRect = capturedRect; 1419 capturedElement->mNewBorderBoxSize = 1420 CapturedRect(aFrame, mInitialSnapshotContainingBlockSize, 1421 CapturedRectType::BorderBox) 1422 .Size(); 1423 // Update its class list. This may override the existing class list because 1424 // the users may change view-transition-class in the callback function. We 1425 // have to use the latest one. 1426 // https://drafts.csswg.org/css-view-transitions-2/#vt-class-algorithms 1427 capturedElement->CaptureClassList(DocumentScopedClassListFor(aFrame)); 1428 SetCaptured(aFrame, true, name); 1429 return true; 1430 }); 1431 return result; 1432 } 1433 1434 // https://drafts.csswg.org/css-view-transitions/#setup-view-transition 1435 void ViewTransition::Setup() { 1436 AUTO_PROFILER_FLOW_MARKER("ViewTransition::Setup", LAYOUT, 1437 Flow::FromPointer(this)); 1438 // Step 2: Capture the old state for transition. 1439 if (auto skipReason = CaptureOldState()) { 1440 // If failure is returned, then skip the view transition for transition 1441 // with an "InvalidStateError" DOMException in transition’s relevant Realm, 1442 // and return. 1443 return SkipTransition(*skipReason); 1444 } 1445 1446 // Step 3: Set document’s rendering suppression for view transitions to true. 1447 mDocument->SetRenderingSuppressedForViewTransitions(true); 1448 1449 // Step 4: Queue a global task on the DOM manipulation task source, given 1450 // transition's relevant global object, to perform the following steps: 1451 // 4.1: If transition's phase is "done", then abort these steps. 1452 // 4.2: Schedule the update callback for transition. 1453 // 4.3: Flush the update callback queue. 1454 mDocument->Dispatch( 1455 NewRunnableMethod("ViewTransition::MaybeScheduleUpdateCallback", this, 1456 &ViewTransition::MaybeScheduleUpdateCallback)); 1457 } 1458 1459 // https://drafts.csswg.org/css-view-transitions-1/#handle-transition-frame 1460 void ViewTransition::HandleFrame() { 1461 // Steps 1-3: Steps 1-3: Compute active animations. 1462 const bool hasActiveAnimations = CheckForActiveAnimations(); 1463 1464 // Step 4: If hasActiveAnimations is false: 1465 if (!hasActiveAnimations) { 1466 AUTO_PROFILER_TERMINATING_FLOW_MARKER("ViewTransition::HandleFrameFinish", 1467 LAYOUT, Flow::FromPointer(this)); 1468 // 4.1: Set transition's phase to "done". 1469 mPhase = Phase::Done; 1470 // 4.2: Clear view transition transition. 1471 ClearActiveTransition(false); 1472 // 4.3: Resolve transition's finished promise. 1473 if (Promise* finished = GetFinished(IgnoreErrors())) { 1474 finished->MaybeResolveWithUndefined(); 1475 } 1476 return; 1477 } 1478 1479 AUTO_PROFILER_FLOW_MARKER("ViewTransition::HandleFrame", LAYOUT, 1480 Flow::FromPointer(this)); 1481 1482 // Step 5: If transition’s initial snapshot containing block size is not equal 1483 // to the snapshot containing block size, then skip the view transition for 1484 // transition with an "InvalidStateError" DOMException in transition’s 1485 // relevant Realm, and return. 1486 if (SnapshotContainingBlockRect().Size() != 1487 mInitialSnapshotContainingBlockSize) { 1488 SkipTransition(SkipTransitionReason::Resize); 1489 return; 1490 } 1491 1492 // Step 6: Update pseudo-element styles for transition. 1493 if (!UpdatePseudoElementStyles(/* aNeedsInvalidation= */ true)) { 1494 // If failure is returned, then skip the view transition for transition 1495 // with an "InvalidStateError" DOMException in transition's relevant Realm, 1496 // and return. 1497 return SkipTransition(SkipTransitionReason::PseudoUpdateFailure); 1498 } 1499 1500 // If the view transition is still animating after HandleFrame(), we have to 1501 // periodically perform operations to check if it is still animating in the 1502 // following ticks. 1503 mDocument->EnsureViewTransitionOperationsHappen(); 1504 } 1505 1506 static bool CheckForActiveAnimationsForEachPseudo( 1507 const Element& aRoot, const AnimationTimeline& aDocTimeline, 1508 const AnimationEventDispatcher& aDispatcher, 1509 PseudoStyleRequest&& aRequest) { 1510 // Check EffectSet because an Animation (either a CSS Animations or a 1511 // script animation) is associated with a KeyframeEffect. If the animation 1512 // doesn't have an associated effect, we can skip it per spec. 1513 // If the effect target is not the element we request, it shouldn't be in 1514 // |effects| either. 1515 EffectSet* effects = EffectSet::Get(&aRoot, aRequest); 1516 if (!effects) { 1517 return false; 1518 } 1519 1520 for (const auto* effect : *effects) { 1521 // 3.1: For each animation whose timeline is a document timeline associated 1522 // with document, and contains at least one associated effect whose effect 1523 // target is element, set hasActiveAnimations to true if any of the 1524 // following conditions is true: 1525 // * animation’s play state is paused or running. 1526 // * document’s pending animation event queue has any events associated 1527 // with animation. 1528 1529 MOZ_ASSERT(effect && effect->GetAnimation(), 1530 "Only effects associated with an animation should be " 1531 "added to an element's effect set"); 1532 const Animation* anim = effect->GetAnimation(); 1533 1534 // The animation's timeline is not the document timeline. 1535 if (anim->GetTimeline() != &aDocTimeline) { 1536 continue; 1537 } 1538 1539 // Return true if any of the following conditions is true: 1540 // 1. animation’s play state is paused or running. 1541 // 2. document’s pending animation event queue has any events associated 1542 // with animation. 1543 const auto playState = anim->PlayState(); 1544 if (playState != AnimationPlayState::Paused && 1545 playState != AnimationPlayState::Running && 1546 !aDispatcher.HasQueuedEventsFor(anim)) { 1547 continue; 1548 } 1549 return true; 1550 } 1551 return false; 1552 } 1553 1554 // This is the implementation of step 3 in HandleFrame(). For each element of 1555 // transition’s transition root pseudo-element’s inclusive descendants, we check 1556 // if it has active animations. 1557 bool ViewTransition::CheckForActiveAnimations() const { 1558 MOZ_ASSERT(mDocument); 1559 1560 if (StaticPrefs::dom_viewTransitions_remain_active()) { 1561 return true; 1562 } 1563 1564 const Element* root = mDocument->GetRootElement(); 1565 if (!root) { 1566 // The documentElement could be removed during animating via script. 1567 return false; 1568 } 1569 1570 const AnimationTimeline* timeline = mDocument->Timeline(); 1571 if (!timeline) { 1572 return false; 1573 } 1574 1575 nsPresContext* presContext = mDocument->GetPresContext(); 1576 if (!presContext) { 1577 return false; 1578 } 1579 1580 const AnimationEventDispatcher* dispatcher = 1581 presContext->AnimationEventDispatcher(); 1582 MOZ_ASSERT(dispatcher); 1583 1584 auto checkForEachPseudo = [&](PseudoStyleRequest&& aRequest) { 1585 return CheckForActiveAnimationsForEachPseudo(*root, *timeline, *dispatcher, 1586 std::move(aRequest)); 1587 }; 1588 1589 bool hasActiveAnimations = 1590 checkForEachPseudo(PseudoStyleRequest(PseudoStyleType::viewTransition)); 1591 for (nsAtom* name : mNamedElements.Keys()) { 1592 if (hasActiveAnimations) { 1593 break; 1594 } 1595 1596 hasActiveAnimations = 1597 checkForEachPseudo({PseudoStyleType::viewTransitionGroup, name}) || 1598 checkForEachPseudo({PseudoStyleType::viewTransitionImagePair, name}) || 1599 checkForEachPseudo({PseudoStyleType::viewTransitionOld, name}) || 1600 checkForEachPseudo({PseudoStyleType::viewTransitionNew, name}); 1601 } 1602 return hasActiveAnimations; 1603 } 1604 1605 void ViewTransition::ClearNamedElements() { 1606 for (auto& entry : mNamedElements) { 1607 if (auto* element = entry.GetData()->mNewElement.get()) { 1608 if (nsIFrame* f = element->GetPrimaryFrame()) { 1609 SetCaptured(f, false, nullptr); 1610 } 1611 } 1612 } 1613 mNamedElements.Clear(); 1614 mNames.Clear(); 1615 } 1616 1617 static void ClearViewTransitionsAnimationData(Element* aRoot) { 1618 if (!aRoot) { 1619 return; 1620 } 1621 1622 auto* data = aRoot->GetAnimationData(); 1623 if (!data) { 1624 return; 1625 } 1626 data->ClearViewTransitionPseudos(); 1627 } 1628 1629 // https://drafts.csswg.org/css-view-transitions-1/#clear-view-transition 1630 void ViewTransition::ClearActiveTransition(bool aIsDocumentHidden) { 1631 // Steps 1-2 1632 MOZ_ASSERT(mDocument); 1633 MOZ_ASSERT(mDocument->GetActiveViewTransition() == this); 1634 1635 // Ensure that any styles associated with :active-view-transition no longer 1636 // apply. 1637 if (auto* root = mDocument->GetRootElement()) { 1638 root->RemoveStates(ElementState::ACTIVE_VIEW_TRANSITION); 1639 } 1640 1641 // Step 3 1642 ClearNamedElements(); 1643 1644 // Step 4: Clear show transition tree flag (we just destroy the pseudo tree, 1645 // see SetupTransitionPseudoElements). 1646 if (mSnapshotContainingBlock) { 1647 nsAutoScriptBlocker scriptBlocker; 1648 if (mDocument->DevToolsAnonymousAndShadowEventsEnabled()) { 1649 mSnapshotContainingBlock->QueueDevtoolsAnonymousEvent( 1650 /* aIsRemove = */ true); 1651 } 1652 if (PresShell* ps = mDocument->GetPresShell()) { 1653 ps->ContentWillBeRemoved(mSnapshotContainingBlock, {}); 1654 } 1655 mSnapshotContainingBlock->UnbindFromTree(); 1656 mSnapshotContainingBlock = nullptr; 1657 1658 // If the document is being destroyed, we cannot get the animation data 1659 // (e.g. it may crash when using nsINode::GetBoolFlag()), so we have to skip 1660 // this case. It's fine because those animations should still be stopped and 1661 // removed if no frame there. 1662 // 1663 // Another case is that the document is hidden. In that case, we don't setup 1664 // the pseudo elements, so it's fine to skip it as well. 1665 if (!aIsDocumentHidden) { 1666 ClearViewTransitionsAnimationData(mDocument->GetRootElement()); 1667 } 1668 } 1669 mDocument->ClearActiveViewTransition(); 1670 } 1671 1672 void ViewTransition::SkipTransition(SkipTransitionReason aReason) { 1673 SkipTransition(aReason, JS::UndefinedHandleValue); 1674 } 1675 1676 // https://drafts.csswg.org/css-view-transitions-1/#skip-the-view-transition 1677 // https://drafts.csswg.org/css-view-transitions-1/#dom-viewtransition-skiptransition 1678 void ViewTransition::SkipTransition( 1679 SkipTransitionReason aReason, 1680 JS::Handle<JS::Value> aUpdateCallbackRejectReason) { 1681 MOZ_ASSERT(mDocument); 1682 MOZ_ASSERT_IF(aReason != SkipTransitionReason::JS, mPhase != Phase::Done); 1683 MOZ_ASSERT_IF(aReason != SkipTransitionReason::UpdateCallbackRejected, 1684 aUpdateCallbackRejectReason == JS::UndefinedHandleValue); 1685 VT_LOG("ViewTransition::SkipTransition(%d, %d)\n", int(mPhase), int(aReason)); 1686 AUTO_PROFILER_TERMINATING_FLOW_MARKER("ViewTransition::SkipTransition", 1687 LAYOUT, Flow::FromPointer(this)); 1688 if (mPhase == Phase::Done) { 1689 return; 1690 } 1691 // Step 3: If transition’s phase is before "update-callback-called", then 1692 // schedule the update callback for transition. 1693 if (UnderlyingValue(mPhase) < UnderlyingValue(Phase::UpdateCallbackCalled)) { 1694 mDocument->ScheduleViewTransitionUpdateCallback(this); 1695 } 1696 1697 // Step 4: Set rendering suppression for view transitions to false. 1698 mDocument->SetRenderingSuppressedForViewTransitions(false); 1699 1700 // Step 5: If document's active view transition is transition, Clear view 1701 // transition transition. 1702 if (mDocument->GetActiveViewTransition() == this) { 1703 ClearActiveTransition(aReason == SkipTransitionReason::DocumentHidden); 1704 } 1705 1706 // Step 6: Set transition's phase to "done". 1707 mPhase = Phase::Done; 1708 1709 // Step 7: Reject transition's ready promise with reason. 1710 Promise* ucd = GetUpdateCallbackDone(IgnoreErrors()); 1711 if (Promise* readyPromise = GetReady(IgnoreErrors())) { 1712 switch (aReason) { 1713 case SkipTransitionReason::JS: 1714 readyPromise->MaybeRejectWithAbortError( 1715 "Skipped ViewTransition due to skipTransition() call"); 1716 break; 1717 case SkipTransitionReason::ClobberedActiveTransition: 1718 readyPromise->MaybeRejectWithAbortError( 1719 "Skipped ViewTransition due to another transition starting"); 1720 break; 1721 case SkipTransitionReason::DocumentHidden: 1722 readyPromise->MaybeRejectWithInvalidStateError( 1723 "Skipped ViewTransition due to document being hidden"); 1724 break; 1725 case SkipTransitionReason::Timeout: 1726 readyPromise->MaybeRejectWithTimeoutError( 1727 "Skipped ViewTransition due to timeout"); 1728 break; 1729 case SkipTransitionReason::DuplicateTransitionNameCapturingOldState: 1730 readyPromise->MaybeRejectWithInvalidStateError( 1731 "Duplicate view-transition-name value while capturing old state"); 1732 break; 1733 case SkipTransitionReason::DuplicateTransitionNameCapturingNewState: 1734 readyPromise->MaybeRejectWithInvalidStateError( 1735 "Duplicate view-transition-name value while capturing new state"); 1736 break; 1737 case SkipTransitionReason::RootRemoved: 1738 readyPromise->MaybeRejectWithInvalidStateError( 1739 "Skipped view transition due to root element going away"); 1740 break; 1741 case SkipTransitionReason::PageSwap: 1742 readyPromise->MaybeRejectWithInvalidStateError( 1743 "Skipped view transition due to page swap"); 1744 break; 1745 case SkipTransitionReason::Resize: 1746 readyPromise->MaybeRejectWithInvalidStateError( 1747 "Skipped view transition due to viewport resize"); 1748 break; 1749 case SkipTransitionReason::PseudoUpdateFailure: 1750 readyPromise->MaybeRejectWithInvalidStateError( 1751 "Skipped view transition due to hidden new element"); 1752 break; 1753 case SkipTransitionReason::ResetRendering: 1754 readyPromise->MaybeRejectWithInvalidStateError( 1755 "Skipped view transition due to graphics process or device reset"); 1756 break; 1757 case SkipTransitionReason::UpdateCallbackRejected: 1758 readyPromise->MaybeReject(aUpdateCallbackRejectReason); 1759 1760 // Step 8, The case we have to reject the finished promise. Do this here 1761 // to make sure it reacts to UpdateCallbackRejected. 1762 // 1763 // Note: we intentionally reject the finished promise after the ready 1764 // promise to make sure the order of promise callbacks is correct in 1765 // script. 1766 if (ucd) { 1767 MOZ_ASSERT(ucd->State() == Promise::PromiseState::Rejected); 1768 if (Promise* finished = GetFinished(IgnoreErrors())) { 1769 // Since the rejection of transition’s update callback done promise 1770 // isn’t explicitly handled here, if transition’s update callback 1771 // done promise rejects, then transition’s finished promise will 1772 // reject with the same reason. 1773 finished->MaybeReject(aUpdateCallbackRejectReason); 1774 } 1775 } 1776 break; 1777 } 1778 } 1779 1780 // Step 8: Resolve transition's finished promise with the result of reacting 1781 // to transition's update callback done promise: 1782 // Note: It is not guaranteed that |mPhase| is Done in CallUpdateCallback(). 1783 // There are two possible cases: 1784 // 1. If we skip the view transitions before updateCallbackDone callback 1785 // is dispatched, we come here first. In this case we don't have to resolve 1786 // the finsihed promise because CallUpdateCallback() will do it. 1787 // 2. If we skip the view transitions after updateCallbackDone callback, the 1788 // finished promise hasn't been resolved because |mPhase| is not Done (i.e. 1789 // |mPhase| is UpdateCallbackCalled) when we handle updateCallbackDone 1790 // callback. Therefore, we have to resolve the finished promise based on 1791 // the PromiseState of |mUpdateCallbackDone|. 1792 if (ucd && ucd->State() == Promise::PromiseState::Resolved) { 1793 if (Promise* finished = GetFinished(IgnoreErrors())) { 1794 // If the promise was fulfilled, then return undefined. 1795 finished->MaybeResolveWithUndefined(); 1796 } 1797 } 1798 } 1799 1800 Maybe<uint64_t> ViewTransition::GetElementIdentifier(Element* aElement) const { 1801 return mElementIdentifiers.MaybeGet(aElement); 1802 } 1803 1804 uint64_t ViewTransition::EnsureElementIdentifier(Element* aElement) { 1805 static uint64_t sLastIdentifier = 0; 1806 return mElementIdentifiers.WithEntryHandle(aElement, [&](auto&& entry) { 1807 return entry.OrInsertWith([&]() { return sLastIdentifier++; }); 1808 }); 1809 } 1810 1811 already_AddRefed<nsAtom> ViewTransition::DocumentScopedTransitionNameFor( 1812 nsIFrame* aFrame) { 1813 // TODO(emilio): Bug 1970954. These aren't quite correct, per spec we're 1814 // supposed to only honor names and classes coming from the document, but 1815 // that's quite some magic, and it's getting actively discussed, see: 1816 // https://github.com/w3c/csswg-drafts/issues/10808 and related 1817 // https://drafts.csswg.org/css-view-transitions-1/#document-scoped-view-transition-name 1818 // https://drafts.csswg.org/css-view-transitions-2/#additions-to-vt-name 1819 // 1. Let computed be the computed value of view-transition-name. 1820 const auto& computed = aFrame->StyleUIReset()->mViewTransitionName; 1821 1822 // 2. If computed is none, return null. 1823 if (computed.IsNone()) { 1824 return nullptr; 1825 } 1826 1827 // As a special case, if we're a <table> element, the table wrapper is what's 1828 // captured. 1829 if (aFrame->IsTableFrame()) { 1830 return nullptr; 1831 } 1832 1833 // 3. If computed is a <custom-ident>, return computed. 1834 if (computed.IsIdent()) { 1835 return RefPtr<nsAtom>{computed.AsIdent().AsAtom()}.forget(); 1836 } 1837 1838 // 4. Assert: computed is auto or match-element. 1839 // TODO: Bug 1918218. Implement auto or others, depending on the spec issue. 1840 // https://github.com/w3c/csswg-drafts/issues/12091 1841 MOZ_ASSERT(computed.IsMatchElement()); 1842 1843 // 5. If computed is auto, element has an associated id, and computed is 1844 // associated with the same root as element’s root, then return a unique 1845 // string starting with "-ua-". Two elements with the same id must return the 1846 // same string, regardless of their node document. 1847 // TODO: Bug 1918218. auto keyword may be changed. See the spec issue 1848 // mentioned above.. 1849 1850 // 6. Return a unique string starting with "-ua-". The string should remain 1851 // consistent and unique for this element and Document, at least for the 1852 // lifetime of element’s node document’s active view transition. 1853 nsIContent* content = aFrame->GetContent(); 1854 if (MOZ_UNLIKELY(!content || !content->IsElement())) { 1855 return nullptr; 1856 } 1857 1858 uint64_t id = EnsureElementIdentifier(content->AsElement()); 1859 1860 // FIXME: We may have to revist here when working on cross document because we 1861 // may have to return a warning and nullptr, per the comment in the design 1862 // review. 1863 // https://github.com/w3ctag/design-reviews/issues/1001#issuecomment-2750966335 1864 nsCString name; 1865 // Note: Add the "view-transition-name" in the prefix so we know this is for 1866 // auto-generated view-transition-name. 1867 name.AppendLiteral("-ua-view-transition-name-"); 1868 name.AppendInt(id); 1869 return NS_Atomize(name); 1870 } 1871 1872 JSObject* ViewTransition::WrapObject(JSContext* aCx, 1873 JS::Handle<JSObject*> aGivenProto) { 1874 return ViewTransition_Binding::Wrap(aCx, this, aGivenProto); 1875 } 1876 1877 static void ComputeActiveRect1D(nscoord aViewMin, nscoord aViewSize, 1878 nscoord& aCaptureMin, nscoord& aCaptureSize) { 1879 nscoord captureMax = aCaptureMin + aCaptureSize; 1880 nscoord viewMax = aViewMin + aViewSize; 1881 1882 nscoord min; 1883 nscoord max; 1884 1885 if (aCaptureSize < aViewSize) { 1886 // The snapshot area is small enough on this axis, don't clip it. 1887 min = aCaptureMin; 1888 max = min + aCaptureSize; 1889 } else if (aViewMin < aCaptureMin) { 1890 // The view is before the capture area. Restrict the cpature size while 1891 // snaping it to the beginning of its range. 1892 min = aCaptureMin; 1893 max = min + aViewSize; 1894 } else if (viewMax > captureMax) { 1895 // The view is after the capture area. Restrict the cpature size while 1896 // snaping it to the end of its range. 1897 max = captureMax; 1898 min = max - aViewSize; 1899 } else { 1900 // The snapshot area extends beyond the viewport on both sides, 1901 // set it to the viewport. 1902 min = aViewMin; 1903 max = viewMax; 1904 } 1905 1906 aCaptureMin = min; 1907 aCaptureSize = max - min; 1908 } 1909 1910 void ViewTransition::UpdateActiveRectForCapturedFrame( 1911 nsIFrame* aCapturedFrame, const gfx::MatrixScales& aInheritedScale, 1912 nsRect& aOutCaptureRect) { 1913 nsAtom* name = aCapturedFrame->GetProperty(ViewTransitionCaptureName()); 1914 if (NS_WARN_IF(!name)) { 1915 return; 1916 } 1917 1918 auto* el = mNamedElements.Get(name); 1919 if (NS_WARN_IF(!el)) { 1920 return; 1921 } 1922 1923 const bool isOld = mPhase < Phase::Animating; 1924 1925 // The active rect to update (old or new). 1926 Maybe<nsRect>* activeRect; 1927 if (isOld) { 1928 activeRect = &el->mOldActiveRect; 1929 // We don't rely on it, but as a sanity check, since we only capture 1930 // the old state once so we aren't expecting it to already contain an 1931 // active rect. 1932 MOZ_ASSERT(activeRect->isNothing()); 1933 } else { 1934 activeRect = &el->mNewActiveRect; 1935 } 1936 1937 // Reset the active rect in case we early out and had previously 1938 // updated it. 1939 activeRect->reset(); 1940 1941 auto presShell = aCapturedFrame->PresShell(); 1942 if (!presShell->IsVisualViewportSizeSet()) { 1943 return; 1944 } 1945 1946 nsPresContext* pc = aCapturedFrame->PresContext(); 1947 1948 auto rootViewportSize = presShell->GetVisualViewportSize(); 1949 auto auPerDevPx = pc->AppUnitsPerDevPixel(); 1950 auto vvpSize = LayoutDeviceSize::FromAppUnits(rootViewportSize, auPerDevPx); 1951 auto capSize = 1952 LayoutDeviceSize::FromAppUnits(aOutCaptureRect.Size(), auPerDevPx); 1953 capSize.width *= aInheritedScale.xScale; 1954 capSize.height *= aInheritedScale.yScale; 1955 1956 // If the capture size is smaller than the visual viewport, it's a good 1957 // indication that it is not unreasonably large, so we can early out. 1958 if (capSize.width < vvpSize.width && capSize.height < vvpSize.height) { 1959 return; 1960 } 1961 1962 // viewport is relative to the root frame. 1963 // auto rootViewportOrigin = presShell->GetVisualViewportOffset(); 1964 auto rootViewportOrigin = nsPoint(0, 0); 1965 nsRect viewport = nsRect(rootViewportOrigin, rootViewportSize); 1966 1967 // Inflate the viewport rect to give a bit of extra headroom in case the user 1968 // scrolls a bit during the transition or the transition has some motion to 1969 // it. But at the same time we want to avoid the margin pushing the snapshot 1970 // size over 4k pixels since that will cause WebRender to render it 1971 // downscaled. 1972 float scale = std::max(aInheritedScale.xScale, aInheritedScale.yScale); 1973 nscoord margin = NSFloatPixelsToAppUnits(512.0 / scale, auPerDevPx); 1974 nscoord maxSize = NSFloatPixelsToAppUnits(4096.0 / scale, auPerDevPx); 1975 margin = std::min( 1976 margin, 1977 std::max(0, maxSize - std::max(viewport.width, viewport.height)) / 2); 1978 1979 viewport.Inflate(margin); 1980 1981 nsIFrame* rootFrame = pc->GetPresShell()->GetRootFrame(); 1982 1983 const auto SUCCESS = nsLayoutUtils::TransformResult::TRANSFORM_SUCCEEDED; 1984 if (!rootFrame || nsLayoutUtils::TransformRect(rootFrame, aCapturedFrame, 1985 viewport) != SUCCESS) { 1986 return; 1987 } 1988 // viewport is now relative to aCapturedFrame. 1989 1990 ComputeActiveRect1D(viewport.x, viewport.width, aOutCaptureRect.x, 1991 aOutCaptureRect.width); 1992 ComputeActiveRect1D(viewport.y, viewport.height, aOutCaptureRect.y, 1993 aOutCaptureRect.height); 1994 1995 // Store the active rect for later when we create the image items. 1996 *activeRect = Some(aOutCaptureRect); 1997 } 1998 1999 Maybe<nsRect> ViewTransition::GetOldActiveRect(nsAtom* aName) const { 2000 auto* el = mNamedElements.Get(aName); 2001 if (NS_WARN_IF(!el)) { 2002 return Nothing(); 2003 } 2004 2005 return el->mOldActiveRect; 2006 } 2007 2008 Maybe<nsRect> ViewTransition::GetNewActiveRect(nsAtom* aName) const { 2009 auto* el = mNamedElements.Get(aName); 2010 if (NS_WARN_IF(!el)) { 2011 return Nothing(); 2012 } 2013 2014 return el->mNewActiveRect; 2015 } 2016 2017 ViewTransitionTypeSet* ViewTransition::Types() { 2018 if (!mTypes) { 2019 mTypes = new ViewTransitionTypeSet(*this); 2020 for (const auto& type : mTypeList) { 2021 ViewTransitionTypeSet_Binding::SetlikeHelpers::Add( 2022 mTypes, nsDependentAtomString(type), IgnoreErrors()); 2023 } 2024 } 2025 return mTypes; 2026 } 2027 2028 }; // namespace mozilla::dom