Files
tubestation/dom/view-transitions/ViewTransition.cpp
Boris Chiou e845887837 Bug 1914322 - Perform pending operations when the view transition is animating. r=view-transitions-reviewers,emilio
Basically, we have to schedule the performing pending operations after
activating the view transitions, and stop to do that once we don't have
active animations.

There is one known issue: we always timeout even if we activate the view
transitions successfully, so I clear the timeout once we start the animations.
Otherwise, we may always interrupt the long animations whose
durations are longer than `dom.viewTransitions.timeout-ms`. However, I
guess we may still need a timeout for it. We probably need to figure out
what to do in the future.

Note that `/css/css-view-transitions/paused-animation-at-end.html.ini`
is failed because we don't have any animations. This is probably blocked by
the dynamic UA stylesheet (Bug 1914324).

Also, update some other tests to avoid getting javascript errors:
`JavaScript error: , line 0: uncaught exception: Object`.

Differential Revision: https://phabricator.services.mozilla.com/D236215
2025-01-31 02:32:03 +00:00

902 lines
33 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* 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 <transform-function> 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<gfx::DataSourceSurface> 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<gfx::DrawTarget> 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<gfx::SourceSurface> surf = dt->GetBackingSurface();
if (NS_WARN_IF(!surf)) {
return nullptr;
}
return surf->GetDataSurface();
}
struct CapturedElementOldState {
RefPtr<gfx::DataSourceSurface> mImage;
// Encompasses width and height.
nsSize mSize;
CSSToCSSMatrix4x4Flagged mTransform;
// Encompasses writing-mode / direction / text-orientation.
WritingMode mWritingMode;
StyleBlend mMixBlendMode = StyleBlend::Normal;
StyleOwnedSlice<StyleFilter> mBackdropFilters;
StyleColorSchemeFlags mColorScheme{0};
CapturedElementOldState(nsIFrame* aFrame,
const nsSize& aSnapshotContainingBlockSize)
: mImage(CaptureFallbackSnapshot(aFrame)),
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<Element> mNewElement;
CapturedElement() = default;
CapturedElement(nsIFrame* aFrame, const nsSize& aSnapshotContainingBlockSize)
: mOldState(aFrame, aSnapshotContainingBlockSize) {}
// TODO: Style definitions as per:
// https://drafts.csswg.org/css-view-transitions/#captured-element-style-definitions
};
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<Promise> callbackPromise;
if (!mUpdateCallback) {
// Step 3: If transition's update callback is null, then set callbackPromise
// to a promise resolved with undefined, in transitions relevant Realm.
callbackPromise =
Promise::CreateResolvedWithUndefined(GetParentObject(), aRv);
} else {
// Step 4: Otherwise set callbackPromise to the result of invoking
// transitions 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<JS::Value>, 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();
}
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<JS::Value> 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<ViewTransition*>(aClosure);
MOZ_DIAGNOSTIC_ASSERT(aTimer == vt->mTimeoutTimer);
vt->Timeout();
}
void ViewTransition::Timeout() {
ClearTimeoutTimer();
if (mPhase != Phase::Done && mDocument) {
SkipTransition(SkipTransitionReason::Timeout);
}
}
static already_AddRefed<Element> MakePseudo(Document& aDoc,
PseudoStyleType aType,
nsAtom* aName) {
RefPtr<Element> 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();
}
// 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<void*>(true));
#endif
// Step 3: For each transitionName -> capturedElement of transitions named
// elements:
for (auto& entry : mNamedElements) {
// We don't need to notify while constructing the tree.
constexpr bool kNotify = false;
nsAtom* transitionName = entry.GetKey();
const CapturedElement& capturedElement = *entry.GetData();
// Let group be a new ::view-transition-group(), with its view transition
// name set to transitionName.
RefPtr<Element> group = MakePseudo(
*mDocument, PseudoStyleType::viewTransitionGroup, transitionName);
// Append group to transitions 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<Element> 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.mImage) {
// 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<Element> old = MakePseudo(
*mDocument, PseudoStyleType::viewTransitionOld, transitionName);
// Append old to imagePair.
imagePair->AppendChildTo(old, kNotify, IgnoreErrors());
}
// 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<Element> new_ = MakePseudo(
*mDocument, PseudoStyleType::viewTransitionNew, transitionName);
// Append new to imagePair.
imagePair->AppendChildTo(new_, kNotify, IgnoreErrors());
}
// TODO(emilio): Dynamic UA sheet shenanigans. Seems we could have a custom
// element class with a transition-specific
}
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/#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();
// TODO(emilio): Step 7.
// 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();
}
// FIXME(emilio): This should actually iterate in paint order.
template <typename Callback>
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 <typename Callback>
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<SkipTransitionReason> ViewTransition::CaptureOldState() {
MOZ_ASSERT(mNamedElements.IsEmpty());
// Steps 1/2 are variable declarations.
// Step 3: Let usedTransitionNames be a new set of strings.
nsTHashSet<nsAtom*> usedTransitionNames;
// Step 4: Let captureElements be a new list of elements.
AutoTArray<std::pair<nsIFrame*, nsAtom*>, 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<SkipTransitionReason> 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<CapturedElement>(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<SkipTransitionReason> ViewTransition::CaptureNewState() {
nsTHashSet<nsAtom*> usedTransitionNames;
Maybe<SkipTransitionReason> 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>(); });
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 transitions 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<CallIfDone>(
"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;
} else {
// If the view tranimation is still animating after HandleFrame(),
// we have to periodically perform operations to check if it is still
// animating in the following ticks.
mDocument->EnsureViewTransitionOperationsHappen();
}
// TODO(emilio): Steps 5-6 (check CB size, update pseudo styles).
}
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:
// * animations play state is paused or running.
// * documents 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. animations play state is paused or running.
// 2. documents 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
// transitions transition root pseudo-elements 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<JS::Value> 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
// transitions relevant global object, to call the update callback of
// transition.
if (UnderlyingValue(mPhase) < UnderlyingValue(Phase::UpdateCallbackCalled)) {
mDocument->Dispatch(NewRunnableMethod<CallIfDone>(
"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.
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::UpdateCallbackRejected:
readyPromise->MaybeReject(aUpdateCallbackRejectReason);
break;
}
}
// Step 8: Resolve transition's finished promise with the result of reacting
// to transition's update callback done promise.
//
// This is done in CallUpdateCallback()
}
JSObject* ViewTransition::WrapObject(JSContext* aCx,
JS::Handle<JSObject*> aGivenProto) {
return ViewTransition_Binding::Wrap(aCx, this, aGivenProto);
}
}; // namespace mozilla::dom