Issue #2472 - Part 3: Refactor Fullscreen stack to Top Layer stack

Fullscreen stack isn't part of the spec anymore; it's been changed to a more
generic version called Top Layer stack, which is being used by both full-screen
APIs and dialog elements.

This patch refactors it to Top Layer stack so that it can be reused for dialog
elements.

Top Layer stack spec: https://fullscreen.spec.whatwg.org/#new-stacking-layer
This commit is contained in:
Moonchild 2024-03-07 12:24:06 +01:00 committed by roytam1
commit 79c7489bb0
5 changed files with 161 additions and 98 deletions

View file

@ -8481,7 +8481,7 @@ nsDocument::OnPageHide(bool aPersisted,
EnumerateActivityObservers(NotifyActivityChanged, nullptr);
ClearPendingFullscreenRequests(this);
if (FullScreenStackTop()) {
if (GetUnretargetedFullScreenElement()) {
// If this document was fullscreen, we should exit fullscreen in this
// doctree branch. This ensures that if the user navigates while in
// fullscreen mode we don't leave its still visible ancestor documents
@ -10112,6 +10112,19 @@ nsDocument::GetFullscreenRoot()
return root;
}
size_t
nsDocument::CountFullscreenElements() const {
size_t count = 0;
for (const nsWeakPtr& ptr : mTopLayer) {
if (nsCOMPtr<Element> elem = do_QueryReferent(ptr)) {
if (elem->State().HasState(NS_EVENT_STATE_FULL_SCREEN)) {
count++;
}
}
}
return count;
}
void
nsDocument::SetFullscreenRoot(nsIDocument* aRoot)
{
@ -10175,7 +10188,7 @@ nsIDocument::AsyncExitFullscreen(nsIDocument* aDoc)
static bool
CountFullscreenSubDocuments(nsIDocument* aDoc, void* aData)
{
if (aDoc->FullScreenStackTop()) {
if (aDoc->GetUnretargetedFullScreenElement()) {
uint32_t* count = static_cast<uint32_t*>(aData);
(*count)++;
}
@ -10195,7 +10208,7 @@ nsDocument::IsFullscreenLeaf()
{
// A fullscreen leaf document is fullscreen, and has no fullscreen
// subdocuments.
if (!FullScreenStackTop()) {
if (!GetUnretargetedFullScreenElement()) {
return false;
}
return CountFullscreenSubDocuments(this) == 0;
@ -10204,11 +10217,11 @@ nsDocument::IsFullscreenLeaf()
static bool
ResetFullScreen(nsIDocument* aDocument, void* aData)
{
if (aDocument->FullScreenStackTop()) {
if (aDocument->GetUnretargetedFullScreenElement()) {
NS_ASSERTION(CountFullscreenSubDocuments(aDocument) <= 1,
"Should have at most 1 fullscreen subdocument.");
static_cast<nsDocument*>(aDocument)->CleanupFullscreenState();
NS_ASSERTION(!aDocument->FullScreenStackTop(),
NS_ASSERTION(!aDocument->GetUnretargetedFullScreenElement(),
"Should reset full-screen");
auto changed = reinterpret_cast<nsCOMArray<nsIDocument>*>(aData);
changed->AppendElement(aDocument);
@ -10256,7 +10269,7 @@ nsIDocument::ExitFullscreenInDocTree(nsIDocument* aMaybeNotARootDoc)
UnlockPointer();
nsCOMPtr<nsIDocument> root = aMaybeNotARootDoc->GetFullscreenRoot();
if (!root || !root->FullScreenStackTop()) {
if (!root || !root->GetUnretargetedFullScreenElement()) {
// If a document was detached before exiting from fullscreen, it is
// possible that the root had left fullscreen state. In this case,
// we would not get anything from the ResetFullScreen() call. Root's
@ -10285,7 +10298,7 @@ nsIDocument::ExitFullscreenInDocTree(nsIDocument* aMaybeNotARootDoc)
DispatchFullScreenChange(changed[changed.Length() - i - 1]);
}
NS_ASSERTION(!root->FullScreenStackTop(),
NS_ASSERTION(!root->GetUnretargetedFullScreenElement(),
"Fullscreen root should no longer be a fullscreen doc...");
// Move the top-level window out of fullscreen mode.
@ -10302,7 +10315,7 @@ GetFullscreenLeaf(nsIDocument* aDoc, void* aData)
nsIDocument** result = static_cast<nsIDocument**>(aData);
*result = aDoc;
return false;
} else if (aDoc->FullScreenStackTop()) {
} else if (aDoc->GetUnretargetedFullScreenElement()) {
aDoc->EnumerateSubDocuments(GetFullscreenLeaf, aData);
}
return true;
@ -10321,7 +10334,7 @@ GetFullscreenLeaf(nsIDocument* aDoc)
nsIDocument* root = nsContentUtils::GetRootDocument(aDoc);
// Check that the root is actually fullscreen so we don't waste time walking
// around its descendants.
if (!root->FullScreenStackTop()) {
if (!root->GetUnretargetedFullScreenElement()) {
return nullptr;
}
GetFullscreenLeaf(root, &leaf);
@ -10331,10 +10344,10 @@ GetFullscreenLeaf(nsIDocument* aDoc)
void
nsDocument::RestorePreviousFullScreenState()
{
NS_ASSERTION(!FullScreenStackTop() || !FullscreenRoots::IsEmpty(),
NS_ASSERTION(!GetUnretargetedFullScreenElement() || !FullscreenRoots::IsEmpty(),
"Should have at least 1 fullscreen root when fullscreen!");
if (!FullScreenStackTop() || !GetWindow() || FullscreenRoots::IsEmpty()) {
if (!GetUnretargetedFullScreenElement() || !GetWindow() || FullscreenRoots::IsEmpty()) {
return;
}
@ -10350,10 +10363,10 @@ nsDocument::RestorePreviousFullScreenState()
// Collect all ancestor documents which we are going to change.
for (; doc; doc = doc->GetParentDocument()) {
nsDocument* theDoc = static_cast<nsDocument*>(doc);
MOZ_ASSERT(!theDoc->mFullScreenStack.IsEmpty(),
Element* top = theDoc->GetUnretargetedFullScreenElement();
MOZ_ASSERT(!top,
"Ancestor of fullscreen document must also be in fullscreen");
if (doc != this) {
Element* top = theDoc->FullScreenStackTop();
if (top->IsHTMLElement(nsGkAtoms::iframe)) {
if (static_cast<HTMLIFrameElement*>(top)->FullscreenFlag()) {
// If this is an iframe, and it explicitly requested
@ -10363,14 +10376,14 @@ nsDocument::RestorePreviousFullScreenState()
}
}
exitDocs.AppendElement(theDoc);
if (theDoc->mFullScreenStack.Length() > 1) {
if (theDoc->CountFullscreenElements() > 1) {
break;
}
}
nsDocument* lastDoc = exitDocs.LastElement();
if (!lastDoc->GetParentDocument() &&
lastDoc->mFullScreenStack.Length() == 1) {
size_t fullscreenCount = lastDoc->CountFullscreenElements();
if (!lastDoc->GetParentDocument() && fullscreenCount == 1) {
// If we are fully exiting fullscreen, don't touch anything here,
// just wait for the window to get out from fullscreen first.
AskWindowToExitFullscreen(this);
@ -10387,8 +10400,8 @@ nsDocument::RestorePreviousFullScreenState()
// The last document will either rollback one fullscreen element, or
// completely exit from the fullscreen state as well.
nsIDocument* newFullscreenDoc;
if (lastDoc->mFullScreenStack.Length() > 1) {
lastDoc->FullScreenStackPop();
if (fullscreenCount > 1) {
lastDoc->UnsetFullscreenElement();
newFullscreenDoc = lastDoc;
} else {
lastDoc->CleanupFullscreenState();
@ -10482,93 +10495,131 @@ ClearFullscreenStateOnElement(Element* aElement)
void
nsDocument::CleanupFullscreenState()
{
// Iterate the fullscreen stack and clear the fullscreen states.
// Iterate the top layer and clear the fullscreen states.
// Since we also need to clear the fullscreen-ancestor state, and
// currently fullscreen elements can only be placed in hierarchy
// order in the stack, reversely iterating the stack could be more
// efficient. NOTE that fullscreen-ancestor state would be removed
// in bug 1199529, and the elements may not in hierarchy order
// after bug 1195213.
for (nsWeakPtr& weakPtr : Reversed(mFullScreenStack)) {
if (nsCOMPtr<Element> element = do_QueryReferent(weakPtr)) {
ClearFullscreenStateOnElement(element);
mTopLayer.RemoveElementsBy([&](const nsWeakPtr& weakPtr) {
nsCOMPtr<Element> element(do_QueryReferent(weakPtr));
if (!element || !element->IsInComposedDoc() ||
element->OwnerDoc() != this) {
return true;
}
}
mFullScreenStack.Clear();
if (element->State().HasState(NS_EVENT_STATE_FULL_SCREEN)) {
ClearFullscreenStateOnElement(element);
return true;
}
return false;
});
mFullscreenRoot = nullptr;
UpdateViewportScrollbarOverrideForFullscreen(this);
}
bool
nsDocument::FullScreenStackPush(Element* aElement)
{
NS_ASSERTION(aElement, "Must pass non-null to FullScreenStackPush()");
Element* top = FullScreenStackTop();
if (top == aElement || !aElement) {
return false;
}
EventStateManager::SetFullScreenState(aElement, true);
mFullScreenStack.AppendElement(do_GetWeakReference(aElement));
NS_ASSERTION(FullScreenStackTop() == aElement, "Should match");
void
nsDocument::UnsetFullscreenElement() {
Element* removedElement = TopLayerPop([](Element* element) -> bool {
return element->State().HasState(NS_EVENT_STATE_FULL_SCREEN);
});
MOZ_ASSERT(removedElement->State().HasState(NS_EVENT_STATE_FULL_SCREEN));
ClearFullscreenStateOnElement(removedElement);
UpdateViewportScrollbarOverrideForFullscreen(this);
}
bool
nsDocument::SetFullscreenElement(Element* aElement) {
if (TopLayerPush(aElement)) {
EventStateManager::SetFullScreenState(aElement, true);
UpdateViewportScrollbarOverrideForFullscreen(this);
return true;
}
return false;
}
bool
nsDocument::TopLayerPush(Element* aElement) {
NS_ASSERTION(aElement, "Must pass non-null to TopLayerPush()");
auto predictFunc = [&aElement](Element* element) {
return element == aElement;
};
TopLayerPop(predictFunc);
mTopLayer.AppendElement(do_GetWeakReference(aElement));
NS_ASSERTION(GetTopLayerTop() == aElement, "Should match");
return true;
}
void
nsDocument::FullScreenStackPop()
{
if (mFullScreenStack.IsEmpty()) {
return;
Element*
nsDocument::TopLayerPop(FunctionRef<bool(Element*)> aPredicateFunc) {
if (mTopLayer.IsEmpty()) {
return nullptr;
}
ClearFullscreenStateOnElement(FullScreenStackTop());
// Remove top element. Note the remaining top element in the stack
// will not have full-screen style bits set, so we will need to restore
// them on the new top element before returning.
uint32_t last = mFullScreenStack.Length() - 1;
mFullScreenStack.RemoveElementAt(last);
// Remove the topmost element that qualifies aPredicate; This
// is required is because the top layer contains not only
// fullscreen elements, but also dialog elements.
Element* removedElement;
for (auto i : Reversed(MakeRange(mTopLayer.Length()))) {
nsCOMPtr<Element> element(do_QueryReferent(mTopLayer[i]));
if (element && aPredicateFunc(element)) {
removedElement = element;
mTopLayer.RemoveElementAt(i);
break;
}
}
// Pop from the stack null elements (references to elements which have
// been GC'd since they were added to the stack) and elements which are
// no longer in this document.
while (!mFullScreenStack.IsEmpty()) {
Element* element = FullScreenStackTop();
while (!mTopLayer.IsEmpty()) {
Element* element = GetTopLayerTop();
if (!element || !element->IsInUncomposedDoc() || element->OwnerDoc() != this) {
NS_ASSERTION(!element->State().HasState(NS_EVENT_STATE_FULL_SCREEN),
"Should have already removed full-screen styles");
uint32_t last = mFullScreenStack.Length() - 1;
mFullScreenStack.RemoveElementAt(last);
mTopLayer.RemoveElementAt(mTopLayer.Length() - 1);
} else {
// The top element of the stack is now an in-doc element. Return here.
break;
}
}
UpdateViewportScrollbarOverrideForFullscreen(this);
return removedElement;
}
Element*
nsDocument::FullScreenStackTop()
{
if (mFullScreenStack.IsEmpty()) {
nsDocument::GetTopLayerTop() {
if (mTopLayer.IsEmpty()) {
return nullptr;
}
uint32_t last = mFullScreenStack.Length() - 1;
nsCOMPtr<Element> element(do_QueryReferent(mFullScreenStack[last]));
NS_ASSERTION(element, "Should have full-screen element!");
NS_ASSERTION(element->IsInComposedDoc(), "Full-screen element should be in doc");
NS_ASSERTION(element->OwnerDoc() == this, "Full-screen element should be in this doc");
uint32_t last = mTopLayer.Length() - 1;
nsCOMPtr<Element> element(do_QueryReferent(mTopLayer[last]));
NS_ASSERTION(element, "Should have a top layer element!");
NS_ASSERTION(element->IsInComposedDoc(), "Top layer element should be in doc");
NS_ASSERTION(element->OwnerDoc() == this, "Top layer element should be in this doc");
return element;
}
/* virtual */ nsTArray<Element*>
nsDocument::GetFullscreenStack() const
{
Element*
nsDocument::GetUnretargetedFullScreenElement() {
for (const nsWeakPtr& weakPtr : Reversed(mTopLayer)) {
nsCOMPtr<Element> element(do_QueryReferent(weakPtr));
// Per spec, the fullscreen element is the topmost element in the document's
// top layer of which the fullscreen flag is set, if any, and null otherwise.
if (element && element->State().HasState(NS_EVENT_STATE_FULL_SCREEN)) {
return element;
}
}
return nullptr;
}
nsTArray<Element*>
nsDocument::GetTopLayer() const {
nsTArray<Element*> elements;
for (const nsWeakPtr& ptr : mFullScreenStack) {
for (const nsWeakPtr& ptr : mTopLayer) {
if (nsCOMPtr<Element> elem = do_QueryReferent(ptr)) {
MOZ_ASSERT(elem->State().HasState(NS_EVENT_STATE_FULL_SCREEN));
elements.AppendElement(elem);
}
}
@ -10683,7 +10734,8 @@ nsDocument::FullscreenElementReadyCheck(Element* aElement,
{
NS_ASSERTION(aElement,
"Must pass non-null element to nsDocument::RequestFullScreen");
if (!aElement || aElement == FullScreenStackTop()) {
Element* fullscreenElement = GetUnretargetedFullScreenElement();
if (!aElement || aElement == fullscreenElement) {
return false;
}
if (!aElement->IsInComposedDoc()) {
@ -10712,9 +10764,9 @@ nsDocument::FullscreenElementReadyCheck(Element* aElement,
}
//XXXsmaug Note, we don't follow the latest fullscreen spec here.
// This whole check could be probably removed.
if (FullScreenStackTop() &&
if (fullscreenElement &&
!nsContentUtils::ContentIsHostIncludingDescendantOf(aElement,
FullScreenStackTop())) {
fullscreenElement)) {
// If this document is full-screen, only grant full-screen requests from
// a descendant of the current full-screen element.
DispatchFullscreenError("FullscreenDeniedNotDescendant");
@ -10871,7 +10923,7 @@ ShouldApplyFullscreenDirectly(nsIDocument* aDoc,
// If we are in the content process, we can apply the fullscreen
// state directly only if we have been in DOM fullscreen, because
// otherwise we always need to notify the chrome.
return !!nsContentUtils::GetRootDocument(aDoc)->GetFullscreenElement();
return !!nsContentUtils::GetRootDocument(aDoc)->GetUnretargetedFullScreenElement();
} else {
// If we are in the chrome process, and the window has not been in
// fullscreen, we certainly need to make that fullscreen first.
@ -10994,8 +11046,8 @@ nsDocument::ApplyFullscreen(const FullscreenRequest& aRequest)
// Set the full-screen element. This sets the full-screen style on the
// element, and the full-screen-ancestor styles on ancestors of the element
// in this document.
DebugOnly<bool> x = FullScreenStackPush(elem);
NS_ASSERTION(x, "Full-screen state of requesting doc should always change!");
DebugOnly<bool> x = SetFullscreenElement(elem);
MOZ_ASSERT(x, "Full-screen state of requesting doc should always change!");
// Set the iframe fullscreen flag.
if (elem->IsHTMLElement(nsGkAtoms::iframe)) {
static_cast<HTMLIFrameElement*>(elem)->SetFullscreenFlag(true);
@ -11017,14 +11069,14 @@ nsDocument::ApplyFullscreen(const FullscreenRequest& aRequest)
}
nsIDocument* parent = child->GetParentDocument();
Element* element = parent->FindContentForSubDocument(child)->AsElement();
if (static_cast<nsDocument*>(parent)->FullScreenStackPush(element)) {
if (static_cast<nsDocument*>(parent)->SetFullscreenElement(element)) {
changed.AppendElement(parent);
child = parent;
} else {
// We've reached either the root, or a point in the doctree where the
// new full-screen element container is the same as the previous
// full-screen element's container. No more changes need to be made
// to the full-screen stacks of documents further up the tree.
// to the top layer of documents further up the tree.
break;
}
}
@ -11294,7 +11346,7 @@ PointerLockRequest::Run()
}
// If it is neither user input initiated, nor requested in fullscreen,
// it should be rejected.
if (!error && !mUserInputOrChromeCaller && !doc->GetFullscreenElement()) {
if (!error && !mUserInputOrChromeCaller && !doc->GetUnretargetedFullScreenElement()) {
error = "PointerLockDeniedNotInputDriven";
}
if (!error && !d->SetPointerLock(e, NS_STYLE_CURSOR_NONE)) {