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:
Emilio Cobos Álvarez
2024-11-14 10:17:52 +00:00
parent 27e9f24b97
commit 11a8b7baf2
9 changed files with 201 additions and 52 deletions

View File

@@ -11868,6 +11868,10 @@ void Document::Destroy() {
return;
}
if (RefPtr transition = mActiveViewTransition) {
transition->SkipTransition(SkipTransitionReason::DocumentHidden);
}
ReportDocumentUseCounters();
ReportLCP();
SetDevToolsWatchingDOMMutations(false);

View File

@@ -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);
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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 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.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();
}

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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();

View File

@@ -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());