Bug 1914321 - Build view transition pseudo-element tree. r=view-transitions-reviewers,boris
Reuse the editor's manual NAC machinery for now, and hook it into StyleChildrenIterator and co. We might need to slightly tweak the setup for selector-matching, not sure yet, but that should be fine. Differential Revision: https://phabricator.services.mozilla.com/D228255
This commit is contained in:
@@ -11868,6 +11868,10 @@ void Document::Destroy() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (RefPtr transition = mActiveViewTransition) {
|
||||
transition->SkipTransition(SkipTransitionReason::DocumentHidden);
|
||||
}
|
||||
|
||||
ReportDocumentUseCounters();
|
||||
ReportLCP();
|
||||
SetDevToolsWatchingDOMMutations(false);
|
||||
|
||||
@@ -200,6 +200,7 @@
|
||||
#include "mozilla/dom/ShadowRoot.h"
|
||||
#include "mozilla/dom/Text.h"
|
||||
#include "mozilla/dom/UserActivation.h"
|
||||
#include "mozilla/dom/ViewTransition.h"
|
||||
#include "mozilla/dom/WindowContext.h"
|
||||
#include "mozilla/dom/WorkerCommon.h"
|
||||
#include "mozilla/dom/WorkerPrivate.h"
|
||||
@@ -10487,6 +10488,15 @@ void nsContentUtils::AppendNativeAnonymousChildren(const nsIContent* aContent,
|
||||
}
|
||||
}
|
||||
|
||||
// View transition pseudos.
|
||||
if (aContent->IsRootElement()) {
|
||||
if (auto* vt = aContent->OwnerDoc()->GetActiveViewTransition()) {
|
||||
if (auto* root = vt->GetRoot()) {
|
||||
aKids.AppendElement(root);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get manually created NAC (editor resize handles, etc.).
|
||||
if (auto nac = static_cast<ManualNACArray*>(
|
||||
aContent->GetProperty(nsGkAtoms::manualNACProperty))) {
|
||||
@@ -10497,7 +10507,7 @@ void nsContentUtils::AppendNativeAnonymousChildren(const nsIContent* aContent,
|
||||
// The root scroll frame is not the primary frame of the root element.
|
||||
// Detect and handle this case.
|
||||
if (!(aFlags & nsIContent::eSkipDocumentLevelNativeAnonymousContent) &&
|
||||
aContent == aContent->OwnerDoc()->GetRootElement()) {
|
||||
aContent->IsRootElement()) {
|
||||
AppendDocumentLevelNativeAnonymousContentTo(aContent->OwnerDoc(), aKids);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -261,6 +261,13 @@ void nsINode::AssertInvariantsOnNodeInfoChange() {
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef DEBUG
|
||||
void nsINode::AssertIsRootElementSlow(bool aIsRoot) const {
|
||||
const bool isRootSlow = this == OwnerDoc()->GetRootElement();
|
||||
MOZ_ASSERT(aIsRoot == isRootSlow);
|
||||
}
|
||||
#endif
|
||||
|
||||
void* nsINode::GetProperty(const nsAtom* aPropertyName,
|
||||
nsresult* aStatus) const {
|
||||
if (!HasProperties()) { // a fast HasFlag() test
|
||||
|
||||
@@ -1662,6 +1662,20 @@ class nsINode : public mozilla::dom::EventTarget {
|
||||
bool IsSelected(uint32_t aStartOffset, uint32_t aEndOffset,
|
||||
mozilla::dom::SelectionNodeCache* aCache = nullptr) const;
|
||||
|
||||
#ifdef DEBUG
|
||||
void AssertIsRootElementSlow(bool) const;
|
||||
#endif
|
||||
|
||||
/** Returns whether we're the root element of our document. */
|
||||
bool IsRootElement() const {
|
||||
// This should be faster than pointer-chasing in the common cases.
|
||||
const bool isRoot = !GetParent() && IsInUncomposedDoc() && IsElement();
|
||||
#ifdef DEBUG
|
||||
AssertIsRootElementSlow(isRoot);
|
||||
#endif
|
||||
return isRoot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the root element of the text editor associated with this node or the
|
||||
* root element of the text editor of the ancestor 'TextControlElement' if
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include "ViewTransition.h"
|
||||
#include "nsPresContext.h"
|
||||
#include "mozilla/dom/BindContext.h"
|
||||
#include "mozilla/dom/DocumentInlines.h"
|
||||
#include "mozilla/dom/Promise-inl.h"
|
||||
#include "mozilla/dom/ViewTransitionBinding.h"
|
||||
@@ -49,6 +50,7 @@ static CSSToCSSMatrix4x4Flagged EffectiveTransform(nsIFrame* aFrame) {
|
||||
|
||||
struct CapturedElementOldState {
|
||||
// TODO: mImage
|
||||
bool mHasImage = false;
|
||||
|
||||
// Encompasses width and height.
|
||||
nsSize mSize;
|
||||
@@ -61,7 +63,8 @@ struct CapturedElementOldState {
|
||||
|
||||
CapturedElementOldState(nsIFrame* aFrame,
|
||||
const nsSize& aSnapshotContainingBlockSize)
|
||||
: mSize(aFrame->Style()->IsRootElementStyle()
|
||||
: mHasImage(true),
|
||||
mSize(aFrame->Style()->IsRootElementStyle()
|
||||
? aSnapshotContainingBlockSize
|
||||
: aFrame->GetRect().Size()),
|
||||
mTransform(EffectiveTransform(aFrame)),
|
||||
@@ -69,6 +72,8 @@ struct CapturedElementOldState {
|
||||
mMixBlendMode(aFrame->StyleEffects()->mMixBlendMode),
|
||||
mBackdropFilters(aFrame->StyleEffects()->mBackdropFilters),
|
||||
mColorScheme(aFrame->StyleUI()->mColorScheme.bits) {}
|
||||
|
||||
CapturedElementOldState() = default;
|
||||
};
|
||||
|
||||
// https://drafts.csswg.org/css-view-transitions/#captured-element
|
||||
@@ -76,6 +81,8 @@ struct ViewTransition::CapturedElement {
|
||||
CapturedElementOldState mOldState;
|
||||
RefPtr<Element> mNewElement;
|
||||
|
||||
CapturedElement() = default;
|
||||
|
||||
CapturedElement(nsIFrame* aFrame, const nsSize& aSnapshotContainingBlockSize)
|
||||
: mOldState(aFrame, aSnapshotContainingBlockSize) {}
|
||||
|
||||
@@ -93,7 +100,8 @@ static inline void ImplCycleCollectionTraverse(
|
||||
NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(ViewTransition, mDocument,
|
||||
mUpdateCallback,
|
||||
mUpdateCallbackDonePromise, mReadyPromise,
|
||||
mFinishedPromise, mNamedElements)
|
||||
mFinishedPromise, mNamedElements,
|
||||
mViewTransitionRoot)
|
||||
|
||||
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ViewTransition)
|
||||
NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
|
||||
@@ -253,6 +261,102 @@ void ViewTransition::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 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();
|
||||
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 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<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.mHasImage) {
|
||||
// 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::viewTransitionOld, 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.
|
||||
@@ -277,7 +381,12 @@ void ViewTransition::Activate() {
|
||||
return SkipTransition(*skipReason);
|
||||
}
|
||||
|
||||
// TODO(emilio): Steps 5-7:
|
||||
// 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;
|
||||
@@ -443,12 +552,8 @@ Maybe<SkipTransitionReason> ViewTransition::CaptureNewState() {
|
||||
SkipTransitionReason::DuplicateTransitionNameCapturingNewState);
|
||||
return false;
|
||||
}
|
||||
auto& capturedElement = mNamedElements.LookupOrInsertWith(name, [&] {
|
||||
// TODO(emilio): See if we need to store something different here (rather
|
||||
// than the properties of the new element). Maybe identity / null / etc?
|
||||
return MakeUnique<CapturedElement>(aFrame,
|
||||
mInitialSnapshotContainingBlockSize);
|
||||
});
|
||||
auto& capturedElement = mNamedElements.LookupOrInsertWith(
|
||||
name, [&] { return MakeUnique<CapturedElement>(); });
|
||||
capturedElement->mNewElement = aFrame->GetContent()->AsElement();
|
||||
return true;
|
||||
});
|
||||
@@ -511,7 +616,16 @@ void ViewTransition::ClearActiveTransition() {
|
||||
// Step 3
|
||||
ClearNamedElements();
|
||||
|
||||
// TODO(emilio): Step 4 (clear show transition tree flag)
|
||||
// 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->ContentRemoved(mViewTransitionRoot, nullptr);
|
||||
}
|
||||
mViewTransitionRoot->UnbindFromTree();
|
||||
mViewTransitionRoot = nullptr;
|
||||
}
|
||||
mDocument->ClearActiveViewTransition();
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ class ErrorResult;
|
||||
namespace dom {
|
||||
|
||||
class Document;
|
||||
class Element;
|
||||
class Promise;
|
||||
class ViewTransitionUpdateCallback;
|
||||
|
||||
@@ -57,6 +58,8 @@ class ViewTransition final : public nsISupports, public nsWrapperCache {
|
||||
void SkipTransition(SkipTransitionReason = SkipTransitionReason::JS);
|
||||
void PerformPendingOperations();
|
||||
|
||||
Element* GetRoot() const { return mViewTransitionRoot; }
|
||||
|
||||
nsIGlobalObject* GetParentObject() const;
|
||||
JSObject* WrapObject(JSContext*, JS::Handle<JSObject*> aGivenProto) override;
|
||||
|
||||
@@ -73,6 +76,7 @@ class ViewTransition final : public nsISupports, public nsWrapperCache {
|
||||
void Setup();
|
||||
[[nodiscard]] Maybe<SkipTransitionReason> CaptureOldState();
|
||||
[[nodiscard]] Maybe<SkipTransitionReason> CaptureNewState();
|
||||
void SetupTransitionPseudoElements();
|
||||
void ClearNamedElements();
|
||||
void HandleFrame();
|
||||
void SkipTransition(SkipTransitionReason, JS::Handle<JS::Value>);
|
||||
@@ -102,6 +106,7 @@ class ViewTransition final : public nsISupports, public nsWrapperCache {
|
||||
RefPtr<nsITimer> mTimeoutTimer;
|
||||
|
||||
Phase mPhase = Phase::PendingCapture;
|
||||
RefPtr<Element> mViewTransitionRoot;
|
||||
};
|
||||
|
||||
} // namespace dom
|
||||
|
||||
@@ -180,38 +180,25 @@ ManualNACPtr HTMLEditor::CreateAnonymousElement(nsAtom* aTag,
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
nsAutoScriptBlocker scriptBlocker;
|
||||
nsAutoScriptBlocker scriptBlocker;
|
||||
|
||||
// establish parenthood of the element
|
||||
newElement->SetIsNativeAnonymousRoot();
|
||||
BindContext context(*aParentContent.AsElement(),
|
||||
BindContext::ForNativeAnonymous);
|
||||
nsresult rv = newElement->BindToTree(context, aParentContent);
|
||||
if (NS_FAILED(rv)) {
|
||||
NS_WARNING("Element::BindToTree(BindContext::ForNativeAnonymous) failed");
|
||||
newElement->UnbindFromTree();
|
||||
return nullptr;
|
||||
}
|
||||
// establish parenthood of the element
|
||||
newElement->SetIsNativeAnonymousRoot();
|
||||
BindContext context(*aParentContent.AsElement(),
|
||||
BindContext::ForNativeAnonymous);
|
||||
if (NS_FAILED(newElement->BindToTree(context, aParentContent))) {
|
||||
NS_WARNING("Element::BindToTree(BindContext::ForNativeAnonymous) failed");
|
||||
newElement->UnbindFromTree();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ManualNACPtr newNativeAnonymousContent(newElement.forget());
|
||||
|
||||
// Must style the new element, otherwise the PostRecreateFramesFor call
|
||||
// below will do nothing.
|
||||
ServoStyleSet* styleSet = presShell->StyleSet();
|
||||
// Sometimes editor likes to append anonymous content to elements
|
||||
// in display:none subtrees, so avoid styling in those cases.
|
||||
if (ServoStyleSet::MayTraverseFrom(newNativeAnonymousContent)) {
|
||||
styleSet->StyleNewSubtree(newNativeAnonymousContent);
|
||||
}
|
||||
|
||||
auto* observer = new ElementDeletionObserver(newNativeAnonymousContent,
|
||||
aParentContent.AsElement());
|
||||
NS_ADDREF(observer); // NodeWillBeDestroyed releases.
|
||||
|
||||
#ifdef DEBUG
|
||||
// Editor anonymous content gets passed to PostRecreateFramesFor... which
|
||||
// Editor anonymous content gets passed to PostRecreateFramesFor... Which
|
||||
// can't _really_ deal with anonymous content (because it can't get the frame
|
||||
// tree ordering right). But for us the ordering doesn't matter so this is
|
||||
// sort of ok.
|
||||
@@ -220,7 +207,7 @@ ManualNACPtr HTMLEditor::CreateAnonymousElement(nsAtom* aTag,
|
||||
#endif // DEBUG
|
||||
|
||||
// display the element
|
||||
presShell->PostRecreateFramesFor(newNativeAnonymousContent);
|
||||
presShell->ContentAppended(newNativeAnonymousContent);
|
||||
|
||||
return newNativeAnonymousContent;
|
||||
}
|
||||
|
||||
@@ -4620,8 +4620,7 @@ MOZ_CAN_RUN_SCRIPT_BOUNDARY void PresShell::ContentRemoved(
|
||||
// frame reconstruction.
|
||||
nsIContent* oldNextSibling = nullptr;
|
||||
|
||||
// Editor calls into here with NAC via HTMLEditor::DeleteRefToAnonymousNode.
|
||||
// This could be asserted if that caller is fixed.
|
||||
// Editor and view transitions code call into here with NAC.
|
||||
if (MOZ_LIKELY(!aChild->IsRootOfNativeAnonymousSubtree())) {
|
||||
oldNextSibling = aPreviousSibling ? aPreviousSibling->GetNextSibling()
|
||||
: container->GetFirstChild();
|
||||
|
||||
@@ -85,19 +85,25 @@ void RestyleManager::ContentInserted(nsIContent* aChild) {
|
||||
}
|
||||
|
||||
void RestyleManager::ContentAppended(nsIContent* aFirstNewContent) {
|
||||
auto* container = aFirstNewContent->GetParentNode();
|
||||
MOZ_ASSERT(container);
|
||||
MOZ_ASSERT(aFirstNewContent->GetParentNode());
|
||||
|
||||
#ifdef DEBUG
|
||||
{
|
||||
for (nsIContent* cur = aFirstNewContent; cur; cur = cur->GetNextSibling()) {
|
||||
NS_ASSERTION(!cur->IsRootOfNativeAnonymousSubtree(),
|
||||
"anonymous nodes should not be in child lists");
|
||||
}
|
||||
for (nsIContent* cur = aFirstNewContent; cur; cur = cur->GetNextSibling()) {
|
||||
NS_ASSERTION(cur->IsRootOfNativeAnonymousSubtree() ==
|
||||
aFirstNewContent->IsRootOfNativeAnonymousSubtree(),
|
||||
"anonymous nodes should not be in child lists");
|
||||
}
|
||||
#endif
|
||||
|
||||
// We get called explicitly with NAC by editor and view transitions code, but
|
||||
// in those cases we don't need to do any invalidation.
|
||||
if (MOZ_UNLIKELY(aFirstNewContent->IsRootOfNativeAnonymousSubtree())) {
|
||||
return;
|
||||
}
|
||||
|
||||
StyleSet()->MaybeInvalidateForElementAppend(*aFirstNewContent);
|
||||
|
||||
auto* container = aFirstNewContent->GetParentNode();
|
||||
const auto selectorFlags = container->GetSelectorFlags() &
|
||||
NodeSelectorFlags::AllSimpleRestyleFlagsForAppend;
|
||||
if (!selectorFlags) {
|
||||
@@ -441,6 +447,17 @@ void RestyleManager::ContentRemoved(nsIContent* aOldChild,
|
||||
// invalidated.
|
||||
IncrementUndisplayedRestyleGeneration();
|
||||
}
|
||||
|
||||
// This is called with anonymous nodes explicitly by editor and view
|
||||
// transitions code, which manage anon content manually.
|
||||
// See similar code in ContentAppended.
|
||||
if (MOZ_UNLIKELY(aOldChild->IsRootOfNativeAnonymousSubtree())) {
|
||||
MOZ_ASSERT(!aFollowingSibling, "NAC doesn't have siblings");
|
||||
MOZ_ASSERT(aOldChild->GetProperty(nsGkAtoms::restylableAnonymousNode),
|
||||
"anonymous nodes should not be in child lists (bug 439258)");
|
||||
return;
|
||||
}
|
||||
|
||||
if (aOldChild->IsElement()) {
|
||||
StyleSet()->MaybeInvalidateForElementRemove(*aOldChild->AsElement(),
|
||||
aFollowingSibling);
|
||||
@@ -452,14 +469,6 @@ void RestyleManager::ContentRemoved(nsIContent* aOldChild,
|
||||
return;
|
||||
}
|
||||
|
||||
if (aOldChild->IsRootOfNativeAnonymousSubtree()) {
|
||||
// This should be an assert, but this is called incorrectly in
|
||||
// HTMLEditor::DeleteRefToAnonymousNode and the assertions were clogging
|
||||
// up the logs. Make it an assert again when that's fixed.
|
||||
MOZ_ASSERT(aOldChild->GetProperty(nsGkAtoms::restylableAnonymousNode),
|
||||
"anonymous nodes should not be in child lists (bug 439258)");
|
||||
}
|
||||
|
||||
// The container cannot be a document.
|
||||
MOZ_ASSERT(container->IsElement() || container->IsShadowRoot());
|
||||
|
||||
|
||||
Reference in New Issue
Block a user