diff --git a/dom/base/Document.cpp b/dom/base/Document.cpp index 02ce1c07cd7a..df94cba7452c 100644 --- a/dom/base/Document.cpp +++ b/dom/base/Document.cpp @@ -11868,6 +11868,10 @@ void Document::Destroy() { return; } + if (RefPtr transition = mActiveViewTransition) { + transition->SkipTransition(SkipTransitionReason::DocumentHidden); + } + ReportDocumentUseCounters(); ReportLCP(); SetDevToolsWatchingDOMMutations(false); diff --git a/dom/base/nsContentUtils.cpp b/dom/base/nsContentUtils.cpp index 46795a6eb8aa..0e4ec71e4f1b 100644 --- a/dom/base/nsContentUtils.cpp +++ b/dom/base/nsContentUtils.cpp @@ -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( 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); } } diff --git a/dom/base/nsINode.cpp b/dom/base/nsINode.cpp index 02c6295e9420..8dd6531c0d68 100644 --- a/dom/base/nsINode.cpp +++ b/dom/base/nsINode.cpp @@ -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 diff --git a/dom/base/nsINode.h b/dom/base/nsINode.h index ac57197185bb..e1fb6c2aeb67 100644 --- a/dom/base/nsINode.h +++ b/dom/base/nsINode.h @@ -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 diff --git a/dom/view-transitions/ViewTransition.cpp b/dom/view-transitions/ViewTransition.cpp index 3e884665714d..2488991714a4 100644 --- a/dom/view-transitions/ViewTransition.cpp +++ b/dom/view-transitions/ViewTransition.cpp @@ -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 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 MakePseudo(Document& aDoc, + PseudoStyleType aType, + nsAtom* aName) { + RefPtr el = aDoc.CreateHTMLElement(nsGkAtoms::div); + if (!aName) { + MOZ_ASSERT(aType == PseudoStyleType::viewTransition); + el->SetIsNativeAnonymousRoot(); + } + el->SetPseudoElementType(aType); + if (aName) { + el->SetAttr(nsGkAtoms::name, nsDependentAtomString(aName), IgnoreErrors()); + } + // This is not needed, but useful for debugging. + el->SetAttr(nsGkAtoms::type, + nsDependentAtomString(nsCSSPseudoElements::GetPseudoAtom(aType)), + IgnoreErrors()); + return el.forget(); +} + +// https://drafts.csswg.org/css-view-transitions-1/#setup-transition-pseudo-elements +void ViewTransition::SetupTransitionPseudoElements() { + MOZ_ASSERT(!mViewTransitionRoot); + + nsAutoScriptBlocker scriptBlocker; + + RefPtr docElement = mDocument->GetRootElement(); + if (!docElement) { + return; + } + + // Step 1 is a declaration. + + // Step 2: Set document's show view transition tree to true. + // (we lazily create this pseudo-element so we don't need the flag for now at + // least). + mViewTransitionRoot = + MakePseudo(*mDocument, PseudoStyleType::viewTransition, nullptr); +#ifdef DEBUG + // View transition pseudos don't care about frame tree ordering, so can be + // restyled just fine. + mViewTransitionRoot->SetProperty(nsGkAtoms::restylableAnonymousNode, + reinterpret_cast(true)); +#endif + // Step 3: For each transitionName -> capturedElement of transition’s named + // elements: + for (auto& entry : mNamedElements) { + // We don't need to notify while constructing the tree. + constexpr bool kNotify = false; + + nsAtom* transitionName = entry.GetKey(); + const CapturedElement& capturedElement = *entry.GetData(); + // Let group be a new ::view-transition-group(), with its view transition + // name set to transitionName. + RefPtr group = MakePseudo( + *mDocument, PseudoStyleType::viewTransitionGroup, transitionName); + // Append group to transition’s transition root pseudo-element. + mViewTransitionRoot->AppendChildTo(group, kNotify, IgnoreErrors()); + // Let imagePair be a new ::view-transition-image-pair(), with its view + // transition name set to transitionName. + RefPtr imagePair = MakePseudo( + *mDocument, PseudoStyleType::viewTransitionImagePair, transitionName); + // Append imagePair to group. + group->AppendChildTo(imagePair, kNotify, IgnoreErrors()); + // If capturedElement's old image is not null, then: + if (capturedElement.mOldState.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 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 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 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(aFrame, - mInitialSnapshotContainingBlockSize); - }); + auto& capturedElement = mNamedElements.LookupOrInsertWith( + name, [&] { return MakeUnique(); }); 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(); } diff --git a/dom/view-transitions/ViewTransition.h b/dom/view-transitions/ViewTransition.h index 2810bcb4a7bb..a41b06968d46 100644 --- a/dom/view-transitions/ViewTransition.h +++ b/dom/view-transitions/ViewTransition.h @@ -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 aGivenProto) override; @@ -73,6 +76,7 @@ class ViewTransition final : public nsISupports, public nsWrapperCache { void Setup(); [[nodiscard]] Maybe CaptureOldState(); [[nodiscard]] Maybe CaptureNewState(); + void SetupTransitionPseudoElements(); void ClearNamedElements(); void HandleFrame(); void SkipTransition(SkipTransitionReason, JS::Handle); @@ -102,6 +106,7 @@ class ViewTransition final : public nsISupports, public nsWrapperCache { RefPtr mTimeoutTimer; Phase mPhase = Phase::PendingCapture; + RefPtr mViewTransitionRoot; }; } // namespace dom diff --git a/editor/libeditor/HTMLAnonymousNodeEditor.cpp b/editor/libeditor/HTMLAnonymousNodeEditor.cpp index 36598eee8cc7..c3887d05c310 100644 --- a/editor/libeditor/HTMLAnonymousNodeEditor.cpp +++ b/editor/libeditor/HTMLAnonymousNodeEditor.cpp @@ -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; } diff --git a/layout/base/PresShell.cpp b/layout/base/PresShell.cpp index 30adf756ad78..7df086fcccbf 100644 --- a/layout/base/PresShell.cpp +++ b/layout/base/PresShell.cpp @@ -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(); diff --git a/layout/base/RestyleManager.cpp b/layout/base/RestyleManager.cpp index f48ddce664be..08d5d44db841 100644 --- a/layout/base/RestyleManager.cpp +++ b/layout/base/RestyleManager.cpp @@ -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());