Files
tubestation/dom/view-transitions/ViewTransition.cpp
2025-02-26 12:29:47 +00:00

1211 lines
46 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;
// 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<StyleFilter> 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<Element> 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<StyleLockedDeclarationBlock> mGroupRule;
// The image pair isolation rule.
RefPtr<StyleLockedDeclarationBlock> mImagePairRule;
// The rules for ::view-transition-old(<name>).
RefPtr<StyleLockedDeclarationBlock> mOldRule;
// The rules for ::view-transition-new(<name>).
RefPtr<StyleLockedDeclarationBlock> 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<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();
}
// 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<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();
}
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<StyleTransformOperation, 1> ops;
ops.AppendElement(
StyleTransformOperation::Matrix3D(StyleGenericMatrix3D<StyleNumber>{
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<StyleLockedDeclarationBlock>& 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<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();
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.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<Element> 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<Element> 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 documents 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, capturedElements 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 <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.
const 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 transitions 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 transitions
// 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:
// * 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);
if (StaticPrefs::dom_viewTransitions_remain_active()) {
return true;
}
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.
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->MaybeRejectWithTimeoutError(
"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 transitions update callback done promise
// isnt explicitly handled here, if transitions update callback
// done promise rejects, then transitions 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<JSObject*> aGivenProto) {
return ViewTransition_Binding::Wrap(aCx, this, aGivenProto);
}
}; // namespace mozilla::dom