/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "ViewTransition.h" #include "mozilla/gfx/2D.h" #include "mozilla/dom/BindContext.h" #include "mozilla/dom/DocumentInlines.h" #include "mozilla/dom/DocumentTimeline.h" #include "mozilla/dom/Promise-inl.h" #include "mozilla/dom/ViewTransitionBinding.h" #include "mozilla/webrender/WebRenderAPI.h" #include "mozilla/AnimationEventDispatcher.h" #include "mozilla/EffectSet.h" #include "mozilla/ElementAnimationData.h" #include "mozilla/ServoStyleConsts.h" #include "mozilla/SVGIntegrationUtils.h" #include "mozilla/WritingModes.h" #include "nsDisplayList.h" #include "nsITimer.h" #include "nsLayoutUtils.h" #include "nsPresContext.h" #include "Units.h" namespace mozilla::dom { // Set capture's old transform to a that would map // element's border box from the snapshot containing block origin to its // current visual position. // // Since we're using viewport as the snapshot origin, we can use // GetBoundingClientRect() effectively... // // TODO(emilio): This might need revision. static CSSToCSSMatrix4x4Flagged EffectiveTransform(nsIFrame* aFrame) { CSSToCSSMatrix4x4Flagged matrix; if (aFrame->GetSize().IsEmpty() || aFrame->Style()->IsRootElementStyle()) { return matrix; } CSSSize untransformedSize = CSSSize::FromAppUnits(aFrame->GetSize()); CSSRect boundingRect = CSSRect::FromAppUnits(aFrame->GetBoundingClientRect()); if (boundingRect.Size() != untransformedSize) { float sx = boundingRect.width / untransformedSize.width; float sy = boundingRect.height / untransformedSize.height; matrix = CSSToCSSMatrix4x4Flagged::Scaling(sx, sy, 0.0f); } if (boundingRect.TopLeft() != CSSPoint()) { matrix.PostTranslate(boundingRect.x, boundingRect.y, 0.0f); } return matrix; } static RefPtr CaptureFallbackSnapshot( nsIFrame* aFrame) { const nsRect rect = aFrame->InkOverflowRectRelativeToSelf(); const auto surfaceRect = LayoutDeviceIntRect::FromAppUnitsToOutside( rect, aFrame->PresContext()->AppUnitsPerDevPixel()); // TODO: Should we use the DrawTargetRecorder infra or what not? const auto format = gfx::SurfaceFormat::B8G8R8A8; RefPtr dt = gfx::Factory::CreateDrawTarget( gfxPlatform::GetPlatform()->GetSoftwareBackend(), surfaceRect.Size().ToUnknownSize(), format); if (NS_WARN_IF(!dt) || NS_WARN_IF(!dt->IsValid())) { return nullptr; } { using PaintFrameFlags = nsLayoutUtils::PaintFrameFlags; gfxContext thebes(dt); // TODO: This matches the drawable code we use for -moz-element(), but is // this right? const PaintFrameFlags flags = PaintFrameFlags::InTransform; nsLayoutUtils::PaintFrame(&thebes, aFrame, rect, NS_RGBA(0, 0, 0, 0), nsDisplayListBuilderMode::Painting, flags); } RefPtr surf = dt->GetBackingSurface(); if (NS_WARN_IF(!surf)) { return nullptr; } return surf->GetDataSurface(); } struct CapturedElementOldState { RefPtr mImage; // Whether we tried to capture an image. Note we might fail to get a // snapshot, so this might not be the same as !!mImage. bool mTriedImage = false; // Encompasses width and height. nsSize mSize; CSSToCSSMatrix4x4Flagged mTransform; // Encompasses writing-mode / direction / text-orientation. WritingMode mWritingMode; StyleBlend mMixBlendMode = StyleBlend::Normal; StyleOwnedSlice mBackdropFilters; StyleColorSchemeFlags mColorScheme{0}; CapturedElementOldState(nsIFrame* aFrame, const nsSize& aSnapshotContainingBlockSize) : mImage(CaptureFallbackSnapshot(aFrame)), mTriedImage(true), mSize(aFrame->Style()->IsRootElementStyle() ? aSnapshotContainingBlockSize : aFrame->GetRect().Size()), mTransform(EffectiveTransform(aFrame)), mWritingMode(aFrame->GetWritingMode()), mMixBlendMode(aFrame->StyleEffects()->mMixBlendMode), mBackdropFilters(aFrame->StyleEffects()->mBackdropFilters), mColorScheme(aFrame->StyleUI()->mColorScheme.bits) {} CapturedElementOldState() = default; }; // https://drafts.csswg.org/css-view-transitions/#captured-element struct ViewTransition::CapturedElement { CapturedElementOldState mOldState; RefPtr mNewElement; CapturedElement() = default; CapturedElement(nsIFrame* aFrame, const nsSize& aSnapshotContainingBlockSize) : mOldState(aFrame, aSnapshotContainingBlockSize) {} // https://drafts.csswg.org/css-view-transitions-1/#captured-element-style-definitions // The group animation-name rule and group styles rule, merged into one. RefPtr mGroupRule; // The image pair isolation rule. RefPtr mImagePairRule; // The rules for ::view-transition-old(). RefPtr mOldRule; // The rules for ::view-transition-new(). RefPtr mNewRule; }; static inline void ImplCycleCollectionTraverse( nsCycleCollectionTraversalCallback& aCb, const ViewTransition::CapturedElement& aField, const char* aName, uint32_t aFlags = 0) { ImplCycleCollectionTraverse(aCb, aField.mNewElement, aName, aFlags); } NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(ViewTransition, mDocument, mUpdateCallback, mUpdateCallbackDonePromise, mReadyPromise, mFinishedPromise, mNamedElements, mViewTransitionRoot) NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ViewTransition) NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY NS_INTERFACE_MAP_ENTRY(nsISupports) NS_INTERFACE_MAP_END NS_IMPL_CYCLE_COLLECTING_ADDREF(ViewTransition) NS_IMPL_CYCLE_COLLECTING_RELEASE(ViewTransition) ViewTransition::ViewTransition(Document& aDoc, ViewTransitionUpdateCallback* aCb) : mDocument(&aDoc), mUpdateCallback(aCb) {} ViewTransition::~ViewTransition() { ClearTimeoutTimer(); } gfx::DataSourceSurface* ViewTransition::GetOldSurface(nsAtom* aName) const { auto* el = mNamedElements.Get(aName); if (NS_WARN_IF(!el)) { return nullptr; } return el->mOldState.mImage; } nsIGlobalObject* ViewTransition::GetParentObject() const { return mDocument ? mDocument->GetParentObject() : nullptr; } Promise* ViewTransition::GetUpdateCallbackDone(ErrorResult& aRv) { if (!mUpdateCallbackDonePromise) { mUpdateCallbackDonePromise = Promise::Create(GetParentObject(), aRv); } return mUpdateCallbackDonePromise; } Promise* ViewTransition::GetReady(ErrorResult& aRv) { if (!mReadyPromise) { mReadyPromise = Promise::Create(GetParentObject(), aRv); } return mReadyPromise; } Promise* ViewTransition::GetFinished(ErrorResult& aRv) { if (!mFinishedPromise) { mFinishedPromise = Promise::Create(GetParentObject(), aRv); } return mFinishedPromise; } void ViewTransition::CallUpdateCallbackIgnoringErrors(CallIfDone aCallIfDone) { if (aCallIfDone == CallIfDone::No && mPhase == Phase::Done) { return; } CallUpdateCallback(IgnoreErrors()); } // https://drafts.csswg.org/css-view-transitions-1/#call-the-update-callback void ViewTransition::CallUpdateCallback(ErrorResult& aRv) { MOZ_ASSERT(mDocument); // Step 1: Assert: transition's phase is "done", or before // "update-callback-called". MOZ_ASSERT(mPhase == Phase::Done || UnderlyingValue(mPhase) < UnderlyingValue(Phase::UpdateCallbackCalled)); // Step 5: If transition's phase is not "done", then set transition's phase // to "update-callback-called". // // NOTE(emilio): This is swapped with the spec because the spec is broken, // see https://github.com/w3c/csswg-drafts/issues/10822 if (mPhase != Phase::Done) { mPhase = Phase::UpdateCallbackCalled; } // Step 2: Let callbackPromise be null. RefPtr callbackPromise; if (!mUpdateCallback) { // Step 3: If transition's update callback is null, then set callbackPromise // to a promise resolved with undefined, in transition’s relevant Realm. callbackPromise = Promise::CreateResolvedWithUndefined(GetParentObject(), aRv); } else { // Step 4: Otherwise set callbackPromise to the result of invoking // transition’s update callback. MOZ_KnownLive because the callback can only // go away when we get CCd. callbackPromise = MOZ_KnownLive(mUpdateCallback)->Call(aRv); } if (aRv.Failed()) { // TODO(emilio): Do we need extra error handling here? return; } MOZ_ASSERT(callbackPromise); // Step 8: React to callbackPromise with fulfillSteps and rejectSteps. callbackPromise->AddCallbacksWithCycleCollectedArgs( [](JSContext*, JS::Handle, ErrorResult& aRv, ViewTransition* aVt) { // We clear the timeout when we are ready to activate. Otherwise, any // animations with the duration longer than // StaticPrefs::dom_viewTransitions_timeout_ms() will be interrupted. // FIXME: We may need a better solution to tweak the timeout, e.g. reset // the timeout to a longer value or so on. aVt->ClearTimeoutTimer(); // Step 6: Let fulfillSteps be to following steps: if (Promise* ucd = aVt->GetUpdateCallbackDone(aRv)) { // 6.1: Resolve transition's update callback done promise with // undefined. ucd->MaybeResolveWithUndefined(); } // Unlike other timings, this is not guaranteed to happen with clean // layout, and Activate() needs to look at the frame tree to capture the // new state, so we need to flush frames. Do it here so that we deal // with other potential script execution skipping the transition or // what not in a consistent way. aVt->mDocument->FlushPendingNotifications(FlushType::Frames); if (aVt->mPhase == Phase::Done) { // "Skip a transition" step 8. We need to resolve "finished" after // update-callback-done. if (Promise* finished = aVt->GetFinished(aRv)) { finished->MaybeResolveWithUndefined(); } } aVt->Activate(); }, [](JSContext*, JS::Handle aReason, ErrorResult& aRv, ViewTransition* aVt) { // Clear the timeout because we are ready to skip the view transitions. aVt->ClearTimeoutTimer(); // Step 7: Let rejectSteps be to following steps: if (Promise* ucd = aVt->GetUpdateCallbackDone(aRv)) { // 7.1: Reject transition's update callback done promise with reason. ucd->MaybeReject(aReason); } // 7.2: If transition's phase is "done", then return. if (aVt->mPhase == Phase::Done) { // "Skip a transition" step 8. We need to resolve "finished" after // update-callback-done. if (Promise* finished = aVt->GetFinished(aRv)) { finished->MaybeReject(aReason); } return; } // 7.3: Mark as handled transition's ready promise. if (Promise* ready = aVt->GetReady(aRv)) { MOZ_ALWAYS_TRUE(ready->SetAnyPromiseIsHandled()); } aVt->SkipTransition(SkipTransitionReason::UpdateCallbackRejected, aReason); }, RefPtr(this)); // Step 9: To skip a transition after a timeout, the user agent may perform // the following steps in parallel: MOZ_ASSERT(!mTimeoutTimer); ClearTimeoutTimer(); // Be safe just in case. mTimeoutTimer = NS_NewTimer(); mTimeoutTimer->InitWithNamedFuncCallback( TimeoutCallback, this, StaticPrefs::dom_viewTransitions_timeout_ms(), nsITimer::TYPE_ONE_SHOT, "ViewTransition::TimeoutCallback"); } void ViewTransition::ClearTimeoutTimer() { if (mTimeoutTimer) { mTimeoutTimer->Cancel(); mTimeoutTimer = nullptr; } } void ViewTransition::TimeoutCallback(nsITimer* aTimer, void* aClosure) { RefPtr vt = static_cast(aClosure); MOZ_DIAGNOSTIC_ASSERT(aTimer == vt->mTimeoutTimer); vt->Timeout(); } void ViewTransition::Timeout() { ClearTimeoutTimer(); if (mPhase != Phase::Done && mDocument) { SkipTransition(SkipTransitionReason::Timeout); } } static already_AddRefed MakePseudo(Document& aDoc, PseudoStyleType aType, nsAtom* aName) { RefPtr el = aDoc.CreateHTMLElement(nsGkAtoms::div); if (!aName) { MOZ_ASSERT(aType == PseudoStyleType::viewTransition); el->SetIsNativeAnonymousRoot(); } el->SetPseudoElementType(aType); if (aName) { el->SetAttr(nsGkAtoms::name, nsDependentAtomString(aName), IgnoreErrors()); } // This is not needed, but useful for debugging. el->SetAttr(nsGkAtoms::type, nsDependentAtomString(nsCSSPseudoElements::GetPseudoAtom(aType)), IgnoreErrors()); return el.forget(); } static bool SetProp(StyleLockedDeclarationBlock* aDecls, Document* aDoc, nsCSSPropertyID aProp, const nsACString& aValue) { return Servo_DeclarationBlock_SetPropertyById( aDecls, aProp, &aValue, /* is_important = */ false, aDoc->DefaultStyleAttrURLData(), StyleParsingMode::DEFAULT, eCompatibility_FullStandards, aDoc->CSSLoader(), StyleCssRuleType::Style, {}); } static bool SetProp(StyleLockedDeclarationBlock* aDecls, Document*, nsCSSPropertyID aProp, float aLength, nsCSSUnit aUnit) { return Servo_DeclarationBlock_SetLengthValue(aDecls, aProp, aLength, aUnit); } static bool SetProp(StyleLockedDeclarationBlock* aDecls, Document*, nsCSSPropertyID aProp, const CSSToCSSMatrix4x4Flagged& aM) { MOZ_ASSERT(aProp == eCSSProperty_transform); AutoTArray ops; ops.AppendElement( StyleTransformOperation::Matrix3D(StyleGenericMatrix3D{ aM._11, aM._12, aM._13, aM._14, aM._21, aM._22, aM._23, aM._24, aM._31, aM._32, aM._33, aM._34, aM._41, aM._42, aM._43, aM._44})); return Servo_DeclarationBlock_SetTransform(aDecls, aProp, &ops); } static StyleLockedDeclarationBlock* EnsureRule( RefPtr& aRule) { if (!aRule) { aRule = Servo_DeclarationBlock_CreateEmpty().Consume(); } return aRule.get(); } // https://drafts.csswg.org/css-view-transitions-1/#setup-transition-pseudo-elements void ViewTransition::SetupTransitionPseudoElements() { MOZ_ASSERT(!mViewTransitionRoot); nsAutoScriptBlocker scriptBlocker; RefPtr docElement = mDocument->GetRootElement(); if (!docElement) { return; } // Step 1 is a declaration. // Step 2: Set document's show view transition tree to true. // (we lazily create this pseudo-element so we don't need the flag for now at // least). mViewTransitionRoot = MakePseudo(*mDocument, PseudoStyleType::viewTransition, nullptr); #ifdef DEBUG // View transition pseudos don't care about frame tree ordering, so can be // restyled just fine. mViewTransitionRoot->SetProperty(nsGkAtoms::restylableAnonymousNode, reinterpret_cast(true)); #endif // Step 3: For each transitionName -> capturedElement of transition’s named // elements: for (auto& entry : mNamedElements) { // We don't need to notify while constructing the tree. constexpr bool kNotify = false; nsAtom* transitionName = entry.GetKey(); CapturedElement& capturedElement = *entry.GetData(); // Let group be a new ::view-transition-group(), with its view transition // name set to transitionName. RefPtr group = MakePseudo( *mDocument, PseudoStyleType::viewTransitionGroup, transitionName); // Append group to transition’s transition root pseudo-element. mViewTransitionRoot->AppendChildTo(group, kNotify, IgnoreErrors()); // Let imagePair be a new ::view-transition-image-pair(), with its view // transition name set to transitionName. RefPtr imagePair = MakePseudo( *mDocument, PseudoStyleType::viewTransitionImagePair, transitionName); // Append imagePair to group. group->AppendChildTo(imagePair, kNotify, IgnoreErrors()); // If capturedElement's old image is not null, then: if (capturedElement.mOldState.mTriedImage) { // Let old be a new ::view-transition-old(), with its view transition // name set to transitionName, displaying capturedElement's old image as // its replaced content. RefPtr old = MakePseudo( *mDocument, PseudoStyleType::viewTransitionOld, transitionName); // Append old to imagePair. imagePair->AppendChildTo(old, kNotify, IgnoreErrors()); } else { // Moved around for simplicity. If capturedElement's old image is null, // then: Assert: capturedElement's new element is not null. MOZ_ASSERT(capturedElement.mNewElement); // Set capturedElement's image animation name rule to a new ... auto* rule = EnsureRule(capturedElement.mNewRule); SetProp(rule, mDocument, eCSSProperty_animation_name, "-ua-view-transition-fade-in"_ns); } // If capturedElement's new element is not null, then: if (capturedElement.mNewElement) { // Let new be a new ::view-transition-new(), with its view transition // name set to transitionName. RefPtr new_ = MakePseudo( *mDocument, PseudoStyleType::viewTransitionNew, transitionName); // Append new to imagePair. imagePair->AppendChildTo(new_, kNotify, IgnoreErrors()); } else { // Moved around from the next step for simplicity. // Assert: capturedElement's old image is not null. // Set capturedElement's image animation name rule to a new CSSStyleRule // representing the following CSS, and append it to document’s dynamic // view transition style sheet: MOZ_ASSERT(capturedElement.mOldState.mTriedImage); SetProp(EnsureRule(capturedElement.mOldRule), mDocument, eCSSProperty_animation_name, "-ua-view-transition-fade-out"_ns); // Moved around from "update pseudo-element styles" because it's a one // time operation. auto* rule = EnsureRule(capturedElement.mGroupRule); auto oldRect = CSSPixel::FromAppUnits(capturedElement.mOldState.mSize); SetProp(rule, mDocument, eCSSProperty_width, oldRect.width, eCSSUnit_Pixel); SetProp(rule, mDocument, eCSSProperty_height, oldRect.height, eCSSUnit_Pixel); SetProp(rule, mDocument, eCSSProperty_transform, capturedElement.mOldState.mTransform); // TODO: writing-mode, direction, text-orientation, mix-blend-mode, // backdrop-filter, color-scheme. } // If both of capturedElement's old image and new element are not null, // then: if (capturedElement.mOldState.mTriedImage && capturedElement.mNewElement) { nsAutoCString dynamicAnimationName; transitionName->ToUTF8String(dynamicAnimationName); dynamicAnimationName.InsertLiteral("-ua-view-transition-group-anim-", 0); // TODO(emilio): Group keyframes. // Set capturedElement's group animation name rule to ... SetProp(EnsureRule(capturedElement.mGroupRule), mDocument, eCSSProperty_animation_name, dynamicAnimationName); // Set capturedElement's image pair isolation rule to ... SetProp(EnsureRule(capturedElement.mImagePairRule), mDocument, eCSSProperty_isolation, "isolate"_ns); // Set capturedElement's image animation name rule to ... SetProp( EnsureRule(capturedElement.mOldRule), mDocument, eCSSProperty_animation_name, "-ua-view-transition-fade-out, -ua-mix-blend-mode-plus-lighter"_ns); SetProp( EnsureRule(capturedElement.mNewRule), mDocument, eCSSProperty_animation_name, "-ua-view-transition-fade-in, -ua-mix-blend-mode-plus-lighter"_ns); } } BindContext context(*docElement, BindContext::ForNativeAnonymous); if (NS_FAILED(mViewTransitionRoot->BindToTree(context, *docElement))) { mViewTransitionRoot->UnbindFromTree(); mViewTransitionRoot = nullptr; return; } if (PresShell* ps = mDocument->GetPresShell()) { ps->ContentAppended(mViewTransitionRoot); } } // https://drafts.csswg.org/css-view-transitions-1/#style-transition-pseudo-elements-algorithm bool ViewTransition::UpdatePseudoElementStyles(bool aNeedsInvalidation) { // 1. For each transitionName -> capturedElement of transition's "named // elements". for (auto& entry : mNamedElements) { nsAtom* transitionName = entry.GetKey(); CapturedElement& capturedElement = *entry.GetData(); // If capturedElement's new element is null, then: // We already did this in SetupTransitionPseudoElements(). if (!capturedElement.mNewElement) { continue; } // Otherwise. // Return failure if any of the following conditions is true: // * capturedElement's new element has a flat tree ancestor that skips its // contents. // * capturedElement's new element is not rendered. // * capturedElement has more than one box fragment. nsIFrame* frame = capturedElement.mNewElement->GetPrimaryFrame(); if (!frame || frame->IsHiddenByContentVisibilityOnAnyAncestor() || frame->GetPrevContinuation() || frame->GetNextContinuation()) { return false; } auto* rule = EnsureRule(capturedElement.mGroupRule); // Let newRect be snapshot containing block if capturedElement is the // document element, otherwise, capturedElement’s border box. auto newRect = frame->Style()->IsRootElementStyle() ? SnapshotContainingBlockRect() : frame->GetRect(); auto size = CSSPixel::FromAppUnits(newRect); // NOTE(emilio): Intentionally not short-circuiting. Int cast is needed to // silence warning. bool changed = int(SetProp(rule, mDocument, eCSSProperty_width, size.width, eCSSUnit_Pixel)) | SetProp(rule, mDocument, eCSSProperty_height, size.height, eCSSUnit_Pixel) | SetProp(rule, mDocument, eCSSProperty_transform, EffectiveTransform(frame)); // TODO: writing-mode, direction, text-orientation, mix-blend-mode, // backdrop-filter, color-scheme. if (changed && aNeedsInvalidation) { auto* pseudo = FindPseudo(PseudoStyleRequest( PseudoStyleType::viewTransitionGroup, transitionName)); MOZ_ASSERT(pseudo); // TODO(emilio): Maybe we need something more than recascade? But I don't // see how off-hand. nsLayoutUtils::PostRestyleEvent(pseudo, RestyleHint::RECASCADE_SELF, nsChangeHint(0)); } // 5. TODO(emilio): Live capturing (probably nothing to do here) } return true; } // https://drafts.csswg.org/css-view-transitions-1/#activate-view-transition void ViewTransition::Activate() { // Step 1: If transition's phase is "done", then return. if (mPhase == Phase::Done) { return; } // TODO(emilio): Step 2: Set rendering suppression for view transitions to // false. // Step 3: If transition's initial snapshot containing block size is not // equal to the snapshot containing block size, then skip the view transition // for transition, and return. if (mInitialSnapshotContainingBlockSize != SnapshotContainingBlockRect().Size()) { return SkipTransition(SkipTransitionReason::Resize); } // Step 4: Capture the new state for transition. if (auto skipReason = CaptureNewState()) { // If failure is returned, then skip the view transition for transition... return SkipTransition(*skipReason); } // TODO(emilio): Step 5. // Step 6: Setup transition pseudo-elements for transition. SetupTransitionPseudoElements(); // Step 7: Update pseudo-element styles for transition. // We don't need to invalidate the pseudo-element styles since we just // generated them. if (!UpdatePseudoElementStyles(/* aNeedsInvalidation = */ false)) { // If failure is returned, then skip the view transition for transition // with an "InvalidStateError" DOMException in transition's relevant Realm, // and return. return SkipTransition(SkipTransitionReason::PseudoUpdateFailure); } // Step 8: Set transition's phase to "animating". mPhase = Phase::Animating; // Step 9: Resolve transition's ready promise. if (Promise* ready = GetReady(IgnoreErrors())) { ready->MaybeResolveWithUndefined(); } // Once this view transition is activated, we have to perform the pending // operations periodically. MOZ_ASSERT(mDocument); mDocument->EnsureViewTransitionOperationsHappen(); } // https://drafts.csswg.org/css-view-transitions/#perform-pending-transition-operations void ViewTransition::PerformPendingOperations() { MOZ_ASSERT(mDocument); MOZ_ASSERT(mDocument->GetActiveViewTransition() == this); switch (mPhase) { case Phase::PendingCapture: return Setup(); case Phase::Animating: return HandleFrame(); default: break; } } // https://drafts.csswg.org/css-view-transitions/#snapshot-containing-block nsRect ViewTransition::SnapshotContainingBlockRect() const { nsPresContext* pc = mDocument->GetPresContext(); // TODO(emilio): Ensure this is correct with Android's dynamic toolbar and // scrollbars. return pc ? pc->GetVisibleArea() : nsRect(); } Element* ViewTransition::FindPseudo(const PseudoStyleRequest& aRequest) const { Element* root = GetRoot(); if (!root) { return nullptr; } if (aRequest.mType == PseudoStyleType::viewTransition) { return root; } // Linear search ::view-transition-group by |aRequest.mIdentifier|. // Note: perhaps we can add a hashtable to improve the performance if it's // common that there are a lot of view-transition-names. Element* group = root->GetFirstElementChild(); for (; group; group = group->GetNextElementSibling()) { MOZ_ASSERT(group->HasName(), "The generated ::view-transition-group() should have a name"); nsAtom* name = group->GetParsedAttr(nsGkAtoms::name)->GetAtomValue(); if (name == aRequest.mIdentifier) { break; } } // No one specifies view-transition-name or we mismatch all names. if (!group) { return nullptr; } if (aRequest.mType == PseudoStyleType::viewTransitionGroup) { return group; } Element* imagePair = group->GetFirstElementChild(); MOZ_ASSERT(imagePair, "::view-transition-image-pair() should exist always"); if (aRequest.mType == PseudoStyleType::viewTransitionImagePair) { return imagePair; } Element* child = imagePair->GetFirstElementChild(); // Neither ::view-transition-old() nor ::view-transition-new() doesn't exist. if (!child) { return nullptr; } // Check if the first element matches our request. const PseudoStyleType type = child->GetPseudoElementType(); if (type == aRequest.mType) { return child; } // Since the second child is either ::view-transition-new() or nullptr, so we // can reject viewTransitionOld request here. if (aRequest.mType == PseudoStyleType::viewTransitionOld) { return nullptr; } child = child->GetNextElementSibling(); MOZ_ASSERT(aRequest.mType == PseudoStyleType::viewTransitionNew); MOZ_ASSERT(!child || !child->GetNextElementSibling(), "No more psuedo elements in this subtree"); return child; } const StyleLockedDeclarationBlock* ViewTransition::GetDynamicRuleFor( const Element& aElement) const { if (!aElement.HasName()) { return nullptr; } nsAtom* name = aElement.GetParsedAttr(nsGkAtoms::name)->GetAtomValue(); auto* capture = mNamedElements.Get(name); if (!capture) { return nullptr; } switch (aElement.GetPseudoElementType()) { case PseudoStyleType::viewTransitionNew: return capture->mNewRule.get(); case PseudoStyleType::viewTransitionOld: return capture->mOldRule.get(); case PseudoStyleType::viewTransitionImagePair: return capture->mImagePairRule.get(); case PseudoStyleType::viewTransitionGroup: return capture->mGroupRule.get(); default: return nullptr; } } // FIXME(emilio): This should actually iterate in paint order. template static bool ForEachChildFrame(nsIFrame* aFrame, const Callback& aCb) { if (!aCb(aFrame)) { return false; } for (auto& [list, id] : aFrame->ChildLists()) { for (nsIFrame* f : list) { if (!ForEachChildFrame(f, aCb)) { return false; } } } return true; } template static void ForEachFrame(Document* aDoc, const Callback& aCb) { PresShell* ps = aDoc->GetPresShell(); if (!ps) { return; } nsIFrame* root = ps->GetRootFrame(); if (!root) { return; } ForEachChildFrame(root, aCb); } // https://drafts.csswg.org/css-view-transitions-1/#document-scoped-view-transition-name static nsAtom* DocumentScopedTransitionNameFor(nsIFrame* aFrame) { auto* name = aFrame->StyleUIReset()->mViewTransitionName._0.AsAtom(); if (name->IsEmpty()) { return nullptr; } // TODO(emilio): This isn't quite correct, per spec we're supposed to only // honor names coming from the document, but that's quite some magic, // and it's getting actively discussed, see: // https://github.com/w3c/csswg-drafts/issues/10808 and related return name; } // https://drafts.csswg.org/css-view-transitions/#capture-the-old-state Maybe ViewTransition::CaptureOldState() { MOZ_ASSERT(mNamedElements.IsEmpty()); // Steps 1/2 are variable declarations. // Step 3: Let usedTransitionNames be a new set of strings. nsTHashSet usedTransitionNames; // Step 4: Let captureElements be a new list of elements. AutoTArray, 32> captureElements; // Step 5: If the snapshot containing block size exceeds an // implementation-defined maximum, then return failure. // TODO(emilio): Implement a maximum if we deem it needed. // // Step 6: Set transition's initial snapshot containing block size to the // snapshot containing block size. mInitialSnapshotContainingBlockSize = SnapshotContainingBlockRect().Size(); // Step 7: For each element of every element that is connected, and has a node // document equal to document, in paint order: Maybe result; ForEachFrame(mDocument, [&](nsIFrame* aFrame) { auto* name = DocumentScopedTransitionNameFor(aFrame); if (!name) { // As a fast path we check for v-t-n first. // If transitionName is none, or element is not rendered, then continue. return true; } if (aFrame->IsHiddenByContentVisibilityOnAnyAncestor()) { // If any flat tree ancestor of this element skips its contents, then // continue. return true; } if (aFrame->GetPrevContinuation() || aFrame->GetNextContinuation()) { // If element has more than one box fragment, then continue. return true; } if (!usedTransitionNames.EnsureInserted(name)) { // If usedTransitionNames contains transitionName, then return failure. result.emplace( SkipTransitionReason::DuplicateTransitionNameCapturingOldState); return false; } // TODO: Set element's captured in a view transition to true. // (but note https://github.com/w3c/csswg-drafts/issues/11058). captureElements.AppendElement(std::make_pair(aFrame, name)); return true; }); if (result) { return result; } // Step 8: For each element in captureElements: for (auto& [f, name] : captureElements) { MOZ_ASSERT(f); MOZ_ASSERT(f->GetContent()->IsElement()); auto capture = MakeUnique(f, mInitialSnapshotContainingBlockSize); mNamedElements.InsertOrUpdate(name, std::move(capture)); } // TODO step 9: For each element in captureElements, set element's captured // in a view transition to false. return result; } // https://drafts.csswg.org/css-view-transitions-1/#capture-the-new-state Maybe ViewTransition::CaptureNewState() { nsTHashSet usedTransitionNames; Maybe result; ForEachFrame(mDocument, [&](nsIFrame* aFrame) { // As a fast path we check for v-t-n first. auto* name = DocumentScopedTransitionNameFor(aFrame); if (!name) { return true; } if (aFrame->IsHiddenByContentVisibilityOnAnyAncestor()) { // If any flat tree ancestor of this element skips its contents, then // continue. return true; } if (aFrame->GetPrevContinuation() || aFrame->GetNextContinuation()) { // If element has more than one box fragment, then continue. return true; } if (!usedTransitionNames.EnsureInserted(name)) { result.emplace( SkipTransitionReason::DuplicateTransitionNameCapturingNewState); return false; } auto& capturedElement = mNamedElements.LookupOrInsertWith( name, [&] { return MakeUnique(); }); capturedElement->mNewElement = aFrame->GetContent()->AsElement(); return true; }); return result; } // https://drafts.csswg.org/css-view-transitions/#setup-view-transition void ViewTransition::Setup() { // Step 2: Capture the old state for transition. if (auto skipReason = CaptureOldState()) { // If failure is returned, then skip the view transition for transition // with an "InvalidStateError" DOMException in transition’s relevant Realm, // and return. return SkipTransition(*skipReason); } // TODO Step 3: Set document's rendering suppression for view transitions to // true. // Step 4: Queue a global task on the DOM manipulation task source, given // transition's relevant global object, to perform the following steps: // 4.1: If transition's phase is "done", then abort these steps. That is // achieved via CallIfDone::No. // 4.2: call the update callback. mDocument->Dispatch(NewRunnableMethod( "ViewTransition::CallUpdateCallbackFromSetup", this, &ViewTransition::CallUpdateCallbackIgnoringErrors, CallIfDone::No)); } // https://drafts.csswg.org/css-view-transitions-1/#handle-transition-frame void ViewTransition::HandleFrame() { // Steps 1-3: Steps 1-3: Compute active animations. bool hasActiveAnimations = CheckForActiveAnimations(); // Step 4: If hasActiveAnimations is false: if (!hasActiveAnimations) { // 4.1: Set transition's phase to "done". mPhase = Phase::Done; // 4.2: Clear view transition transition. ClearActiveTransition(false); // 4.3: Resolve transition's finished promise. if (Promise* finished = GetFinished(IgnoreErrors())) { finished->MaybeResolveWithUndefined(); } return; } // Step 5: If transition’s initial snapshot containing block size is not equal // to the snapshot containing block size, then skip the view transition for // transition with an "InvalidStateError" DOMException in transition’s // relevant Realm, and return. if (SnapshotContainingBlockRect().Size() != mInitialSnapshotContainingBlockSize) { SkipTransition(SkipTransitionReason::Resize); return; } // Step 6: Update pseudo-element styles for transition. if (!UpdatePseudoElementStyles(/* aNeedsInvalidation= */ true)) { // If failure is returned, then skip the view transition for transition // with an "InvalidStateError" DOMException in transition's relevant Realm, // and return. return SkipTransition(SkipTransitionReason::PseudoUpdateFailure); } // If the view transition is still animating after HandleFrame(), we have to // periodically perform operations to check if it is still animating in the // following ticks. mDocument->EnsureViewTransitionOperationsHappen(); } static bool CheckForActiveAnimationsForEachPseudo( const Element& aRoot, const AnimationTimeline& aDocTimeline, const AnimationEventDispatcher& aDispatcher, PseudoStyleRequest&& aRequest) { // Check EffectSet because an Animation (either a CSS Animations or a // script animation) is associated with a KeyframeEffect. If the animation // doesn't have an associated effect, we can skip it per spec. // If the effect target is not the element we request, it shouldn't be in // |effects| either. EffectSet* effects = EffectSet::Get(&aRoot, aRequest); if (!effects) { return false; } for (const auto* effect : *effects) { // 3.1: For each animation whose timeline is a document timeline associated // with document, and contains at least one associated effect whose effect // target is element, set hasActiveAnimations to true if any of the // following conditions is true: // * animation’s play state is paused or running. // * document’s pending animation event queue has any events associated // with animation. MOZ_ASSERT(effect && effect->GetAnimation(), "Only effects associated with an animation should be " "added to an element's effect set"); const Animation* anim = effect->GetAnimation(); // The animation's timeline is not the document timeline. if (anim->GetTimeline() != &aDocTimeline) { continue; } // Return true if any of the following conditions is true: // 1. animation’s play state is paused or running. // 2. document’s pending animation event queue has any events associated // with animation. const auto playState = anim->PlayState(); if (playState != AnimationPlayState::Paused && playState != AnimationPlayState::Running && !aDispatcher.HasQueuedEventsFor(anim)) { continue; } return true; } return false; } // This is the implementation of step 3 in HandleFrame(). For each element of // transition’s transition root pseudo-element’s inclusive descendants, we check // if it has active animations. bool ViewTransition::CheckForActiveAnimations() const { MOZ_ASSERT(mDocument); const Element* root = mDocument->GetRootElement(); if (!root) { // The documentElement could be removed during animating via script. return false; } const AnimationTimeline* timeline = mDocument->Timeline(); if (!timeline) { return false; } nsPresContext* presContext = mDocument->GetPresContext(); if (!presContext) { return false; } const AnimationEventDispatcher* dispatcher = presContext->AnimationEventDispatcher(); MOZ_ASSERT(dispatcher); auto checkForEachPseudo = [&](PseudoStyleRequest&& aRequest) { return CheckForActiveAnimationsForEachPseudo(*root, *timeline, *dispatcher, std::move(aRequest)); }; bool hasActiveAnimations = checkForEachPseudo(PseudoStyleRequest(PseudoStyleType::viewTransition)); for (nsAtom* name : mNamedElements.Keys()) { if (hasActiveAnimations) { break; } hasActiveAnimations = checkForEachPseudo({PseudoStyleType::viewTransitionGroup, name}) || checkForEachPseudo({PseudoStyleType::viewTransitionImagePair, name}) || checkForEachPseudo({PseudoStyleType::viewTransitionOld, name}) || checkForEachPseudo({PseudoStyleType::viewTransitionNew, name}); } return hasActiveAnimations; } void ViewTransition::ClearNamedElements() { // TODO(emilio): TODO: Set element's captured in a view transition to false. mNamedElements.Clear(); } static void ClearViewTransitionsAnimationData(Element* aRoot) { if (!aRoot) { return; } auto* data = aRoot->GetAnimationData(); if (!data) { return; } data->ClearViewTransitionPseudos(); } // https://drafts.csswg.org/css-view-transitions-1/#clear-view-transition void ViewTransition::ClearActiveTransition(bool aIsDocumentHidden) { // Steps 1-2 MOZ_ASSERT(mDocument); MOZ_ASSERT(mDocument->GetActiveViewTransition() == this); // Step 3 ClearNamedElements(); // Step 4: Clear show transition tree flag (we just destroy the pseudo tree, // see SetupTransitionPseudoElements). if (mViewTransitionRoot) { nsAutoScriptBlocker scriptBlocker; if (PresShell* ps = mDocument->GetPresShell()) { ps->ContentWillBeRemoved(mViewTransitionRoot, nullptr); } mViewTransitionRoot->UnbindFromTree(); mViewTransitionRoot = nullptr; // If the doucment is being destroyed, we cannot get the animation data // (e.g. it may crash when using nsINode::GetBoolFlag()), so we have to skip // this case. It's fine because those animations should still be stopped and // removed if no frame there. // // Another case is that the document is hidden. In that case, we don't setup // the pseudo elements, so it's fine to skip it as well. if (!aIsDocumentHidden) { ClearViewTransitionsAnimationData(mDocument->GetRootElement()); } } mDocument->ClearActiveViewTransition(); } void ViewTransition::SkipTransition(SkipTransitionReason aReason) { SkipTransition(aReason, JS::UndefinedHandleValue); } // https://drafts.csswg.org/css-view-transitions-1/#skip-the-view-transition // https://drafts.csswg.org/css-view-transitions-1/#dom-viewtransition-skiptransition void ViewTransition::SkipTransition( SkipTransitionReason aReason, JS::Handle aUpdateCallbackRejectReason) { MOZ_ASSERT(mDocument); MOZ_ASSERT_IF(aReason != SkipTransitionReason::JS, mPhase != Phase::Done); MOZ_ASSERT_IF(aReason != SkipTransitionReason::UpdateCallbackRejected, aUpdateCallbackRejectReason == JS::UndefinedHandleValue); if (mPhase == Phase::Done) { return; } // Step 3: If transition's phase is before "update-callback-called", then // queue a global task on the DOM manipulation task source, given // transition’s relevant global object, to call the update callback of // transition. if (UnderlyingValue(mPhase) < UnderlyingValue(Phase::UpdateCallbackCalled)) { mDocument->Dispatch(NewRunnableMethod( "ViewTransition::CallUpdateCallbackFromSkip", this, &ViewTransition::CallUpdateCallbackIgnoringErrors, CallIfDone::Yes)); } // Step 4: Set rendering suppression for view transitions to false. // TODO(emilio): We don't have that flag yet. // Step 5: If document's active view transition is transition, Clear view // transition transition. if (mDocument->GetActiveViewTransition() == this) { ClearActiveTransition(aReason == SkipTransitionReason::DocumentHidden); } // Step 6: Set transition's phase to "done". mPhase = Phase::Done; // Step 7: Reject transition's ready promise with reason. Promise* ucd = GetUpdateCallbackDone(IgnoreErrors()); if (Promise* readyPromise = GetReady(IgnoreErrors())) { switch (aReason) { case SkipTransitionReason::JS: readyPromise->MaybeRejectWithAbortError( "Skipped ViewTransition due to skipTransition() call"); break; case SkipTransitionReason::ClobberedActiveTransition: readyPromise->MaybeRejectWithAbortError( "Skipped ViewTransition due to another transition starting"); break; case SkipTransitionReason::DocumentHidden: readyPromise->MaybeRejectWithAbortError( "Skipped ViewTransition due to document being hidden"); break; case SkipTransitionReason::Timeout: readyPromise->MaybeRejectWithAbortError( "Skipped ViewTransition due to timeout"); break; case SkipTransitionReason::DuplicateTransitionNameCapturingOldState: readyPromise->MaybeRejectWithInvalidStateError( "Duplicate view-transition-name value while capturing old state"); break; case SkipTransitionReason::DuplicateTransitionNameCapturingNewState: readyPromise->MaybeRejectWithInvalidStateError( "Duplicate view-transition-name value while capturing new state"); break; case SkipTransitionReason::Resize: readyPromise->MaybeRejectWithInvalidStateError( "Skipped view transition due to viewport resize"); break; case SkipTransitionReason::PseudoUpdateFailure: readyPromise->MaybeRejectWithInvalidStateError( "Skipped view transition due to hidden new element"); break; case SkipTransitionReason::UpdateCallbackRejected: readyPromise->MaybeReject(aUpdateCallbackRejectReason); // Step 8, The case we have to reject the finished promise. Do this here // to make sure it reacts to UpdateCallbackRejected. // // Note: we intentionally reject the finished promise after the ready // promise to make sure the order of promise callbacks is correct in // script. if (ucd) { MOZ_ASSERT(ucd->State() == Promise::PromiseState::Rejected); if (Promise* finished = GetFinished(IgnoreErrors())) { // Since the rejection of transition’s update callback done promise // isn’t explicitly handled here, if transition’s update callback // done promise rejects, then transition’s finished promise will // reject with the same reason. finished->MaybeReject(aUpdateCallbackRejectReason); } } break; } } // Step 8: Resolve transition's finished promise with the result of reacting // to transition's update callback done promise: // Note: It is not guaranteed that |mPhase| is Done in CallUpdateCallback(). // There are two possible cases: // 1. If we skip the view transitions before updateCallbackDone callback // is dispatched, we come here first. In this case we don't have to resolve // the finsihed promise because CallUpdateCallback() will do it. // 2. If we skip the view transitions after updateCallbackDone callback, the // finished promise hasn't been resolved because |mPhase| is not Done (i.e. // |mPhase| is UpdateCallbackCalled) when we handle updateCallbackDone // callback. Therefore, we have to resolve the finished promise based on // the PromiseState of |mUpdateCallbackDone|. if (ucd && ucd->State() == Promise::PromiseState::Resolved) { if (Promise* finished = GetFinished(IgnoreErrors())) { // If the promise was fulfilled, then return undefined. finished->MaybeResolveWithUndefined(); } } } JSObject* ViewTransition::WrapObject(JSContext* aCx, JS::Handle aGivenProto) { return ViewTransition_Binding::Wrap(aCx, this, aGivenProto); } }; // namespace mozilla::dom