FullscreenManager.cpp [plain text]
#include "config.h"
#include "FullscreenManager.h"
#if ENABLE(FULLSCREEN_API)
#include "Chrome.h"
#include "ChromeClient.h"
#include "EventNames.h"
#include "Frame.h"
#include "HTMLIFrameElement.h"
#include "HTMLMediaElement.h"
#include "Page.h"
#include "QualifiedName.h"
#include "RenderFullScreen.h"
#include "RenderTreeBuilder.h"
#include "Settings.h"
namespace WebCore {
using namespace HTMLNames;
FullscreenManager::FullscreenManager(Document& document)
: m_document { document }
{
}
FullscreenManager::~FullscreenManager() = default;
void FullscreenManager::requestFullscreenForElement(Element* element, FullscreenCheckType checkType)
{
if (!element)
element = documentElement();
auto failedPreflights = [this](auto element) mutable {
m_fullscreenErrorEventTargetQueue.append(WTFMove(element));
m_fullscreenTaskQueue.enqueueTask([this] {
dispatchFullscreenChangeEvents();
});
};
if (!UserGestureIndicator::processingUserGesture()) {
failedPreflights(WTFMove(element));
return;
}
if (UserGestureIndicator::currentUserGesture()->gestureType() == UserGestureType::EscapeKey) {
document().addConsoleMessage(MessageSource::Security, MessageLevel::Error, "The Escape key may not be used as a user gesture to enter fullscreen"_s);
failedPreflights(WTFMove(element));
return;
}
if (!page() || !page()->settings().fullScreenEnabled()) {
failedPreflights(WTFMove(element));
return;
}
bool hasKeyboardAccess = true;
if (!page()->chrome().client().supportsFullScreenForElement(*element, hasKeyboardAccess)) {
hasKeyboardAccess = false;
if (!page()->chrome().client().supportsFullScreenForElement(*element, hasKeyboardAccess)) {
failedPreflights(WTFMove(element));
return;
}
}
m_pendingFullscreenElement = element;
m_fullscreenTaskQueue.enqueueTask([this, element = makeRefPtr(element), checkType, hasKeyboardAccess, failedPreflights] () mutable {
if (m_pendingFullscreenElement != element) {
failedPreflights(WTFMove(element));
return;
}
if (document().hidden()) {
failedPreflights(WTFMove(element));
return;
}
if (!element->isConnected()) {
failedPreflights(WTFMove(element));
return;
}
if (checkType == EnforceIFrameAllowFullscreenRequirement && !isFeaturePolicyAllowedByDocumentAndAllOwners(FeaturePolicy::Type::Fullscreen, document())) {
failedPreflights(WTFMove(element));
return;
}
if (!m_fullscreenElementStack.isEmpty() && !m_fullscreenElementStack.last()->contains(element.get())) {
failedPreflights(WTFMove(element));
return;
}
bool descendentHasNonEmptyStack = false;
for (Frame* descendant = frame() ? frame()->tree().traverseNext() : nullptr; descendant; descendant = descendant->tree().traverseNext()) {
if (descendant->document()->fullscreenManager().fullscreenElement()) {
descendentHasNonEmptyStack = true;
break;
}
}
if (descendentHasNonEmptyStack) {
failedPreflights(WTFMove(element));
return;
}
Document* currentDoc = &document();
Deque<Document*> docs;
do {
docs.prepend(currentDoc);
currentDoc = currentDoc->ownerElement() ? ¤tDoc->ownerElement()->document() : nullptr;
} while (currentDoc);
Deque<Document*>::iterator current = docs.begin(), following = docs.begin();
do {
++following;
Document* currentDoc = *current;
Document* followingDoc = following != docs.end() ? *following : nullptr;
if (!followingDoc) {
currentDoc->fullscreenManager().pushFullscreenElementStack(*element);
addDocumentToFullscreenChangeEventQueue(*currentDoc);
continue;
}
Element* topElement = currentDoc->fullscreenManager().fullscreenElement();
if (!topElement || topElement != followingDoc->ownerElement()) {
currentDoc->fullscreenManager().pushFullscreenElementStack(*followingDoc->ownerElement());
addDocumentToFullscreenChangeEventQueue(*currentDoc);
continue;
}
} while (++current != docs.end());
m_areKeysEnabledInFullscreen = hasKeyboardAccess;
m_fullscreenTaskQueue.enqueueTask([this, element = WTFMove(element), failedPreflights = WTFMove(failedPreflights)] () mutable {
auto page = this->page();
if (!page || document().hidden() || m_pendingFullscreenElement != element || !element->isConnected()) {
failedPreflights(element);
return;
}
page->chrome().client().enterFullScreenForElement(*element.get());
});
});
}
void FullscreenManager::cancelFullscreen()
{
Document& topDocument = document().topDocument();
if (!topDocument.fullscreenManager().fullscreenElement()) {
m_pendingFullscreenElement = nullptr;
return;
}
Vector<RefPtr<Element>> replacementFullscreenElementStack;
replacementFullscreenElementStack.append(topDocument.fullscreenManager().fullscreenElement());
topDocument.fullscreenManager().m_fullscreenElementStack.swap(replacementFullscreenElementStack);
topDocument.fullscreenManager().exitFullscreen();
}
void FullscreenManager::exitFullscreen()
{
Document* currentDoc = &document();
if (m_fullscreenElementStack.isEmpty()) {
m_pendingFullscreenElement = nullptr;
return;
}
Deque<RefPtr<Document>> descendants;
for (Frame* descendant = frame() ? frame()->tree().traverseNext() : nullptr; descendant; descendant = descendant->tree().traverseNext()) {
if (descendant->document()->fullscreenManager().fullscreenElement())
descendants.prepend(descendant->document());
}
for (auto& document : descendants) {
document->fullscreenManager().clearFullscreenElementStack();
addDocumentToFullscreenChangeEventQueue(*document);
}
Element* newTop = nullptr;
while (currentDoc) {
currentDoc->fullscreenManager().popFullscreenElementStack();
newTop = currentDoc->fullscreenManager().fullscreenElement();
if (newTop && (!newTop->isConnected() || &newTop->document() != currentDoc))
continue;
addDocumentToFullscreenChangeEventQueue(*currentDoc);
if (!newTop && currentDoc->ownerElement()) {
currentDoc = ¤tDoc->ownerElement()->document();
continue;
}
currentDoc = nullptr;
}
m_fullscreenTaskQueue.enqueueTask([this, newTop = makeRefPtr(newTop), fullscreenElement = m_fullscreenElement] {
auto* page = this->page();
if (!page)
return;
if (!fullscreenElement && m_pendingFullscreenElement) {
m_pendingFullscreenElement = nullptr;
return;
}
if (!newTop) {
page->chrome().client().exitFullScreenForElement(fullscreenElement.get());
return;
}
page->chrome().client().enterFullScreenForElement(*newTop);
});
}
bool FullscreenManager::isFullscreenEnabled() const
{
return isFeaturePolicyAllowedByDocumentAndAllOwners(FeaturePolicy::Type::Fullscreen, document());
}
static void unwrapFullscreenRenderer(RenderFullScreen* fullscreenRenderer, Element* fullscreenElement)
{
if (!fullscreenRenderer)
return;
bool requiresRenderTreeRebuild;
fullscreenRenderer->unwrapRenderer(requiresRenderTreeRebuild);
if (requiresRenderTreeRebuild && fullscreenElement && fullscreenElement->parentElement())
fullscreenElement->parentElement()->invalidateStyleAndRenderersForSubtree();
}
void FullscreenManager::willEnterFullscreen(Element& element)
{
if (!document().hasLivingRenderTree() || document().backForwardCacheState() != Document::NotInBackForwardCache)
return;
if (!page())
return;
if (m_pendingFullscreenElement != &element) {
page()->chrome().client().exitFullScreenForElement(&element);
return;
}
ASSERT(page()->settings().fullScreenEnabled());
unwrapFullscreenRenderer(m_fullscreenRenderer.get(), m_fullscreenElement.get());
element.willBecomeFullscreenElement();
ASSERT(&element == m_pendingFullscreenElement);
m_pendingFullscreenElement = nullptr;
m_fullscreenElement = &element;
auto renderer = m_fullscreenElement->renderer();
bool shouldCreatePlaceholder = is<RenderBox>(renderer);
if (shouldCreatePlaceholder) {
m_savedPlaceholderFrameRect = downcast<RenderBox>(*renderer).frameRect();
m_savedPlaceholderRenderStyle = RenderStyle::clonePtr(renderer->style());
}
if (m_fullscreenElement != documentElement() && renderer)
RenderFullScreen::wrapExistingRenderer(*renderer, document());
m_fullscreenElement->setContainsFullScreenElementOnAncestorsCrossingFrameBoundaries(true);
document().resolveStyle(Document::ResolveStyleType::Rebuild);
dispatchFullscreenChangeEvents();
}
void FullscreenManager::didEnterFullscreen()
{
if (!m_fullscreenElement)
return;
if (!hasLivingRenderTree() || backForwardCacheState() != Document::NotInBackForwardCache)
return;
m_fullscreenElement->didBecomeFullscreenElement();
}
void FullscreenManager::willExitFullscreen()
{
auto fullscreenElement = fullscreenOrPendingElement();
if (!fullscreenElement)
return;
if (!hasLivingRenderTree() || backForwardCacheState() != Document::NotInBackForwardCache)
return;
fullscreenElement->willStopBeingFullscreenElement();
}
void FullscreenManager::didExitFullscreen()
{
auto fullscreenElement = fullscreenOrPendingElement();
if (!fullscreenElement)
return;
if (!hasLivingRenderTree() || backForwardCacheState() != Document::NotInBackForwardCache)
return;
fullscreenElement->setContainsFullScreenElementOnAncestorsCrossingFrameBoundaries(false);
if (m_fullscreenElement)
m_fullscreenElement->didStopBeingFullscreenElement();
m_areKeysEnabledInFullscreen = false;
unwrapFullscreenRenderer(m_fullscreenRenderer.get(), m_fullscreenElement.get());
m_fullscreenElement = nullptr;
m_pendingFullscreenElement = nullptr;
scheduleFullStyleRebuild();
bool eventTargetQueuesEmpty = m_fullscreenChangeEventTargetQueue.isEmpty() && m_fullscreenErrorEventTargetQueue.isEmpty();
Document& exitingDocument = eventTargetQueuesEmpty ? topDocument() : document();
exitingDocument.fullscreenManager().dispatchFullscreenChangeEvents();
}
void FullscreenManager::setFullscreenRenderer(RenderTreeBuilder& builder, RenderFullScreen& renderer)
{
if (&renderer == m_fullscreenRenderer)
return;
if (m_savedPlaceholderRenderStyle)
builder.createPlaceholderForFullScreen(renderer, WTFMove(m_savedPlaceholderRenderStyle), m_savedPlaceholderFrameRect);
else if (m_fullscreenRenderer && m_fullscreenRenderer->placeholder()) {
auto* placeholder = m_fullscreenRenderer->placeholder();
builder.createPlaceholderForFullScreen(renderer, RenderStyle::clonePtr(placeholder->style()), placeholder->frameRect());
}
if (m_fullscreenRenderer)
builder.destroy(*m_fullscreenRenderer);
ASSERT(!m_fullscreenRenderer);
m_fullscreenRenderer = makeWeakPtr(renderer);
}
RenderFullScreen* FullscreenManager::fullscreenRenderer() const
{
return m_fullscreenRenderer.get();
}
void FullscreenManager::dispatchFullscreenChangeEvents()
{
Ref<Document> protectedDocument(document());
Deque<RefPtr<Node>> changeQueue;
m_fullscreenChangeEventTargetQueue.swap(changeQueue);
Deque<RefPtr<Node>> errorQueue;
m_fullscreenErrorEventTargetQueue.swap(errorQueue);
dispatchFullscreenChangeOrErrorEvent(changeQueue, eventNames().webkitfullscreenchangeEvent, true);
dispatchFullscreenChangeOrErrorEvent(errorQueue, eventNames().webkitfullscreenerrorEvent, false);
}
void FullscreenManager::dispatchFullscreenChangeOrErrorEvent(Deque<RefPtr<Node>>& queue, const AtomString& eventName, bool shouldNotifyMediaElement)
{
while (!queue.isEmpty()) {
RefPtr<Node> node = queue.takeFirst();
if (!node)
node = documentElement();
if (!node)
continue;
if (!node->isConnected())
queue.append(documentElement());
#if ENABLE(VIDEO)
if (shouldNotifyMediaElement && is<HTMLMediaElement>(*node))
downcast<HTMLMediaElement>(*node).enteredOrExitedFullscreen();
#else
UNUSED_PARAM(shouldNotifyMediaElement);
#endif
node->dispatchEvent(Event::create(eventName, Event::CanBubble::Yes, Event::IsCancelable::No));
}
}
void FullscreenManager::adjustFullscreenElementOnNodeRemoval(Node& node, Document::NodeRemoval nodeRemoval)
{
auto fullscreenElement = fullscreenOrPendingElement();
if (!fullscreenElement)
return;
bool elementInSubtree = false;
if (nodeRemoval == Document::NodeRemoval::ChildrenOfNode)
elementInSubtree = fullscreenElement->isDescendantOf(node);
else
elementInSubtree = (fullscreenElement == &node) || fullscreenElement->isDescendantOf(node);
if (elementInSubtree) {
fullscreenElement->setContainsFullScreenElementOnAncestorsCrossingFrameBoundaries(false);
cancelFullscreen();
}
}
bool FullscreenManager::isAnimatingFullscreen() const
{
return m_isAnimatingFullscreen;
}
void FullscreenManager::setAnimatingFullscreen(bool flag)
{
if (m_isAnimatingFullscreen == flag)
return;
m_isAnimatingFullscreen = flag;
if (m_fullscreenElement && m_fullscreenElement->isDescendantOf(document())) {
m_fullscreenElement->invalidateStyleForSubtree();
scheduleFullStyleRebuild();
}
}
bool FullscreenManager::areFullscreenControlsHidden() const
{
return m_areFullscreenControlsHidden;
}
void FullscreenManager::setFullscreenControlsHidden(bool flag)
{
if (m_areFullscreenControlsHidden == flag)
return;
m_areFullscreenControlsHidden = flag;
if (m_fullscreenElement && m_fullscreenElement->isDescendantOf(document())) {
m_fullscreenElement->invalidateStyleForSubtree();
scheduleFullStyleRebuild();
}
}
void FullscreenManager::clear()
{
m_fullscreenElement = nullptr;
m_pendingFullscreenElement = nullptr;
m_fullscreenElementStack.clear();
}
void FullscreenManager::emptyEventQueue()
{
m_fullscreenChangeEventTargetQueue.clear();
m_fullscreenErrorEventTargetQueue.clear();
}
void FullscreenManager::clearFullscreenElementStack()
{
m_fullscreenElementStack.clear();
}
void FullscreenManager::popFullscreenElementStack()
{
if (m_fullscreenElementStack.isEmpty())
return;
m_fullscreenElementStack.removeLast();
}
void FullscreenManager::pushFullscreenElementStack(Element& element)
{
m_fullscreenElementStack.append(&element);
}
void FullscreenManager::addDocumentToFullscreenChangeEventQueue(Document& document)
{
Node* target = document.fullscreenManager().fullscreenElement();
if (!target)
target = document.fullscreenManager().currentFullscreenElement();
if (!target)
target = &document;
m_fullscreenChangeEventTargetQueue.append(target);
}
}
#endif