LibWeb: Implement exitFullscreen algorithm

Exiting fullscreen from the UI will be added in future commits.
This commit is contained in:
Simon Farre 2025-04-01 14:23:04 +02:00 committed by Luke Wilde
parent bc17805b2b
commit 04d1e2bf3d
8 changed files with 235 additions and 0 deletions

View file

@ -186,6 +186,7 @@
#include <LibWeb/Painting/StackingContext.h>
#include <LibWeb/Painting/ViewportPaintable.h>
#include <LibWeb/PermissionsPolicy/AutoplayAllowlist.h>
#include <LibWeb/Platform/EventLoopPlugin.h>
#include <LibWeb/ResizeObserver/ResizeObserver.h>
#include <LibWeb/ResizeObserver/ResizeObserverEntry.h>
#include <LibWeb/SVG/SVGDecodedImageData.h>
@ -6887,6 +6888,209 @@ bool Document::fullscreen_enabled() const
return is_allowed_to_use_feature(PolicyControlledFeature::Fullscreen);
}
// https://fullscreen.spec.whatwg.org/#fully-exit-fullscreen
void Document::fully_exit_fullscreen()
{
// 1. If documents fullscreen element is null, terminate these steps.
GC::Ptr<Element> fullscreened_element = fullscreen_element();
if (!fullscreened_element)
return;
// 2. Unfullscreen elements whose fullscreen flag is set, within documents top layer, except for documents fullscreen element.
GC::RootVector<GC::Ref<Element>, 8> fullscreen_elements { heap() };
for (auto const& element : top_layer_elements()) {
if (element->is_fullscreen_element() && element != fullscreened_element)
fullscreen_elements.append(element);
}
for (auto const& element : fullscreen_elements) {
unfullscreen_element(element);
}
// 3. Exit fullscreen document.
// Note/FIXME: Because Document::destroy() does not "Assert: this is running as part of a task queued on document's relevant agent's event loop." and doesn't seem
// to have any temporary execution context while it's running (because it's making exit_fullscreen crash), we check if there's an execution context
// if not, it means this is running via the "run_unloading_cleanup_steps" and thus we must first add a context.
HTML::TemporaryExecutionContext context(realm(), HTML::TemporaryExecutionContext::CallbacksEnabled::Yes);
(void)exit_fullscreen();
}
// https://fullscreen.spec.whatwg.org/#exit-fullscreen
GC::Ref<WebIDL::Promise> Document::exit_fullscreen()
{
auto& realm = this->realm();
// 1. Let promise be a new promise.
auto promise = WebIDL::create_promise(realm);
// 2. If doc is not fully active or docs fullscreen element is null, then reject promise with a TypeError exception and return promise.
if (!is_fully_active() || !fullscreen_element()) {
WebIDL::reject_promise(realm, promise, JS::TypeError::create(realm, "Document not fully active or no fullscreen element."sv));
return promise;
}
// 3. Let resize be false.
bool resize = false;
// 4. Let docs be the result of collecting documents to unfullscreen given doc.
auto docs = collect_documents_to_unfullscreen();
// 5. Let topLevelDoc be docs node navigables top-level traversables active document.
auto top_level_doc = navigable()->top_level_traversable()->active_document();
// 6. If topLevelDoc is in docs, and it is a simple fullscreen document, then set doc to topLevelDoc and resize to true.
GC::Ref<Document> document_to_unfullscreen { *this };
if (top_level_doc && top_level_doc->is_simple_fullscreen_document() && docs->elements().contains_slow(GC::Ref { *top_level_doc })) {
document_to_unfullscreen = *top_level_doc;
resize = true;
}
// 7. If docs fullscreen element is not connected:
if (auto fullscreen_element = document_to_unfullscreen->fullscreen_element(); !fullscreen_element->is_connected()) {
// 1. Append (fullscreenchange, docs fullscreen element) to docs list of pending fullscreen events.
document_to_unfullscreen->append_pending_fullscreen_change(PendingFullscreenEvent::Type::Change, *fullscreen_element);
// 2. Unfullscreen docs fullscreen element.
document_to_unfullscreen->unfullscreen_element(*fullscreen_element);
}
// 8. Return promise, and run the remaining steps in parallel.
Platform::EventLoopPlugin::the().deferred_invoke(GC::create_function(heap(), [&realm, document_to_unfullscreen, promise, resize] {
HTML::TemporaryExecutionContext context(realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes);
// FIXME: 9. Run the fully unlock the screen orientation steps with doc.
// 10. If resize is true, resize docs viewport to its "normal" dimensions.
// NB: Fullscreen API is affected by site-isolation and will require additional work once site-isolation is implemented.
if (resize)
document_to_unfullscreen->page().client().page_did_request_exit_fullscreen();
// 11. If docs fullscreen element is null, then resolve promise with undefined and terminate these steps.
if (!document_to_unfullscreen->fullscreen_element()) {
WebIDL::resolve_promise(realm, promise, JS::js_undefined());
return;
}
// 12. Let exitDocs be the result of collecting documents to unfullscreen given doc.
auto exit_docs = document_to_unfullscreen->collect_documents_to_unfullscreen();
// 13. Let descendantDocs be an ordered set consisting of docs descendant navigables' active documents whose fullscreen element is non-null, if any, in tree order.
auto descendant_docs = realm.heap().allocate<GC::HeapVector<GC::Ref<Document>>>();
for (auto& descendant : document_to_unfullscreen->descendant_navigables()) {
if (descendant->active_document()->fullscreen_element())
descendant_docs->elements().append(*descendant->active_document());
}
// 14. For each exitDoc in exitDocs:
for (auto& exit_doc : exit_docs->elements()) {
// 1. Append (fullscreenchange, exitDocs fullscreen element) to exitDocs list of pending fullscreen events.
exit_doc->append_pending_fullscreen_change(PendingFullscreenEvent::Type::Change, *exit_doc->fullscreen_element());
if (resize) {
// 2. If resize is true, unfullscreen exitDoc.
exit_doc->unfullscreen();
} else {
// 3. Otherwise, unfullscreen exitDocs fullscreen element.
exit_doc->unfullscreen_element(*exit_doc->fullscreen_element());
}
}
// 15. For each descendantDoc in descendantDocs:
for (auto& descendant_doc : descendant_docs->elements()) {
// 1. Append (fullscreenchange, descendantDocs fullscreen element) to descendantDocs list of pending fullscreen events.
descendant_doc->append_pending_fullscreen_change(PendingFullscreenEvent::Type::Change, *descendant_doc->fullscreen_element());
// 2. Unfullscreen descendantDoc.
descendant_doc->unfullscreen();
}
// NOTE: The order in which documents are unfullscreened is not observable, because run the fullscreen steps is invoked in tree order.
// 16. Resolve promise with undefined.
WebIDL::resolve_promise(realm, promise, JS::js_undefined());
}));
return promise;
}
// https://fullscreen.spec.whatwg.org/#unfullscreen-a-document
void Document::unfullscreen()
{
// To unfullscreen a document, unfullscreen all elements, within documents top layer, whose fullscreen flag is set.
// NB: This has to be a copy of the list of those elements, since unfullscreen an element immediately removes it
// from the top layer, invalidating iterators.
auto fullscreen_elements = heap().allocate<GC::HeapVector<GC::Ref<Element>>>();
for (auto el : top_layer_elements()) {
if (el->is_fullscreen_element())
fullscreen_elements->elements().append(el);
}
for (auto el : fullscreen_elements->elements())
unfullscreen_element(el);
}
// https://fullscreen.spec.whatwg.org/#simple-fullscreen-document
bool Document::is_simple_fullscreen_document() const
{
// A document is said to be a simple fullscreen document if there is exactly one element in its top layer that has its fullscreen flag set.
u32 total = 0;
for (auto const& element : top_layer_elements()) {
if (element->is_fullscreen_element())
++total;
if (total > 1)
return false;
}
return total == 1;
}
// https://fullscreen.spec.whatwg.org/#collect-documents-to-unfullscreen
GC::Ref<GC::HeapVector<GC::Ref<Document>>> Document::collect_documents_to_unfullscreen()
{
// 1. Let docs be an ordered set consisting of doc.
auto docs = heap().allocate<GC::HeapVector<GC::Ref<Document>>>();
docs->elements().append(*this);
// 2. While true:
while (true) {
// 1. Let lastDoc be docss last document.
auto last_doc = docs->elements().last();
// 2. Assert: lastDocs fullscreen element is not null.
VERIFY(last_doc->fullscreen_element());
// 3. If lastDoc is not a simple fullscreen document, break.
if (!last_doc->is_simple_fullscreen_document())
break;
// 4. Let container be lastDocs node navigables container.
// Note on spec: It doesn't first check if `node navigable` is null.
auto container = last_doc->navigable() ? last_doc->navigable()->container() : nullptr;
// 5. If container is null, then break.
if (!container)
break;
// 6. If containers iframe fullscreen flag is set, break.
if (auto* iframe_element = as_if<HTML::HTMLIFrameElement>(container.ptr()); iframe_element && iframe_element->iframe_fullscreen_flag())
break;
// 7. Append containers node document to docs.
docs->elements().append(container->document());
}
// 3. Return docs.
return docs;
}
// https://fullscreen.spec.whatwg.org/#unfullscreen-an-element
void Document::unfullscreen_element(GC::Ref<Element> element)
{
// To unfullscreen an element, unset elements fullscreen flag and iframe fullscreen flag (if any), and remove from the top layer immediately given element.
element->set_fullscreen_flag(false);
if (auto* iframe_element = as_if<HTML::HTMLIFrameElement>(element.ptr()))
iframe_element->set_iframe_fullscreen_flag(false);
remove_an_element_from_the_top_layer_immediately(element);
}
// https://dom.spec.whatwg.org/#document-allow-declarative-shadow-roots
void Document::set_allow_declarative_shadow_roots(bool allow)
{

View file

@ -984,6 +984,11 @@ public:
bool fullscreen() const;
bool fullscreen_enabled() const;
void fully_exit_fullscreen();
GC::Ref<WebIDL::Promise> exit_fullscreen();
void unfullscreen_element(GC::Ref<Element> element);
auto& script_blocking_style_sheet_set() { return m_script_blocking_style_sheet_set; }
auto const& script_blocking_style_sheet_set() const { return m_script_blocking_style_sheet_set; }
@ -1031,6 +1036,9 @@ private:
void evaluate_media_rules();
bool is_simple_fullscreen_document() const;
GC::Ref<GC::HeapVector<GC::Ref<Document>>> collect_documents_to_unfullscreen();
enum class AddLineFeed {
Yes,
No,
@ -1067,6 +1075,8 @@ private:
void ensure_cookie_version_index(URL::URL const& new_url, URL::URL const& old_url = {});
void unfullscreen();
GC::Ref<Page> m_page;
GC::Ptr<CSS::StyleComputer> m_style_computer;
GC::Ptr<CSS::FontComputer> m_font_computer;

View file

@ -5,6 +5,8 @@ partial interface Document {
[LegacyLenientSetter] readonly attribute boolean fullscreenEnabled;
[LegacyLenientSetter, Unscopable] readonly attribute boolean fullscreen; // historical
Promise<undefined> exitFullscreen();
attribute EventHandler onfullscreenchange;
attribute EventHandler onfullscreenerror;
};

View file

@ -475,6 +475,11 @@ void ViewImplementation::js_console_input(String const& js_source)
client().async_js_console_input(page_id(), js_source);
}
void ViewImplementation::exit_fullscreen()
{
client().async_exit_fullscreen(page_id());
}
void ViewImplementation::alert_closed()
{
client().async_alert_closed(page_id());

View file

@ -133,6 +133,7 @@ public:
void run_javascript(String const&);
void js_console_input(String const&);
void exit_fullscreen();
void alert_closed();
void confirm_closed(bool accepted);

View file

@ -39,6 +39,7 @@
#include <LibWeb/Fetch/Fetching/Fetching.h>
#include <LibWeb/HTML/BrowsingContext.h>
#include <LibWeb/HTML/HTMLInputElement.h>
#include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h>
#include <LibWeb/HTML/SelectedFile.h>
#include <LibWeb/HTML/Storage.h>
#include <LibWeb/HTML/TraversableNavigable.h>
@ -1388,4 +1389,12 @@ void ConnectionFromClient::request_close(u64 page_id)
page->page().top_level_traversable()->close_top_level_traversable();
}
void ConnectionFromClient::exit_fullscreen(u64 page_id)
{
if (auto page = this->page(page_id); page.has_value()) {
Web::HTML::TemporaryExecutionContext context(page->page().top_level_browsing_context().active_document()->realm(), Web::HTML::TemporaryExecutionContext::CallbacksEnabled::Yes);
page->page().top_level_browsing_context().active_document()->fully_exit_fullscreen();
}
}
}

View file

@ -168,6 +168,8 @@ private:
virtual void request_close(u64 page_id) override;
virtual void exit_fullscreen(u64 page_id) override;
NonnullOwnPtr<PageHost> m_page_host;
HashMap<int, Web::FileRequest> m_requested_files {};

View file

@ -140,4 +140,6 @@ endpoint WebContentServer
cookies_changed(u64 page_id, Vector<HTTP::Cookie::Cookie> cookies) =|
request_close(u64 page_id) =|
exit_fullscreen(u64 page_id) =|
}