LibWeb: Implement requestFullscreen algorithm

The required functionality to exit fullscreen will be in a followup
commit.
This commit is contained in:
Simon Farre 2025-03-31 19:38:15 +02:00 committed by Luke Wilde
parent 44e0735d9b
commit bc17805b2b
16 changed files with 345 additions and 9 deletions

View file

@ -559,8 +559,7 @@ static inline bool matches_pseudo_class(CSS::Selector::SimpleSelector::PseudoCla
return focused_area && element.is_inclusive_ancestor_of(*focused_area);
}
case CSS::PseudoClass::Fullscreen: {
// FIXME: Add fullscreen support
return false;
return element.is_fullscreen_element();
}
case CSS::PseudoClass::FirstChild:
if (context.collect_per_element_selector_involvement_metadata) {

View file

@ -5,6 +5,7 @@
* Copyright (c) 2021-2026, Sam Atkins <sam@ladybird.org>
* Copyright (c) 2024, Matthew Olsson <mattco@serenityos.org>
* Copyright (c) 2025, Jelle Raaijmakers <jelle@ladybird.org>
* Copyright (c) 2025, Simon Farre <simon.farre.cx@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@ -6797,6 +6798,95 @@ void Document::append_pending_fullscreen_change(PendingFullscreenEvent::Type typ
m_pending_fullscreen_events.append(PendingFullscreenEvent { type, element });
}
// https://fullscreen.spec.whatwg.org/#fullscreen-an-element
void Document::fullscreen_element_within_doc(GC::Ref<Element> element)
{
auto const get_hide_until = [&](auto const& popover_list) {
return HTML::HTMLElement::topmost_popover_ancestor(element, popover_list, nullptr, HTML::IsPopover::No);
};
// 1. Let hideUntil be the result of running topmost popover ancestor given
// element, null, and false.
auto hide_until = get_hide_until(showing_hint_popover_list());
// Finding topmost popover ancestor algorithm takes different parameters than those
// described by the fullscreen spec. Since the new algorithm takes 4 parameters, with the new "popover list"
// we must also account for the auto popover list.
// More can be read about this "spec bug" in https://github.com/whatwg/fullscreen/issues/245
if (hide_until == nullptr)
hide_until = get_hide_until(showing_auto_popover_list());
// Our hide_all_popovers_until takes a variant. topmost_popover_ancestor produces a Ptr<HTMLElement>
Variant<GC::Ptr<HTML::HTMLElement>, GC::Ptr<Document>> hide_until_argument { hide_until };
// 2. If hideUntil is null, then set hideUntil to elements node document.
if (hide_until == nullptr)
hide_until_argument = element->owner_document();
// 3. Run hide all popovers until given hideUntil, false, and true.
HTML::HTMLElement::hide_all_popovers_until(hide_until_argument, HTML::FocusPreviousElement::No, HTML::FireEvents::Yes);
// 4. Set elements fullscreen flag.
element->set_fullscreen_flag(true);
// 5. Remove from the top layer immediately given element.
remove_an_element_from_the_top_layer_immediately(element);
// 6. Add to the top layer given element.
add_an_element_to_the_top_layer(element);
element->invalidate_style(StyleInvalidationReason::Fullscreen);
}
// https://fullscreen.spec.whatwg.org/#fullscreen-element
GC::Ptr<Element> Document::fullscreen_element() const
{
// All documents have an associated fullscreen element. The fullscreen element is the topmost element in the
// documents top layer whose fullscreen flag is set, if any, and null otherwise.
for (auto const& el : top_layer_elements().in_reverse()) {
if (el->is_fullscreen_element())
return el;
}
return nullptr;
}
// https://fullscreen.spec.whatwg.org/#dom-document-fullscreenelement
GC::Ptr<Element> Document::fullscreen_element_for_bindings() const
{
GC::Ptr<Element> fullscreen_element = this->fullscreen_element();
if (!fullscreen_element) {
return nullptr;
}
// 1. If this is a shadow root and its host is not connected, then return null.
// Note: We're not a shadow root. See ShadowRoot::fullscreen_element_for_bindings() instead.
// 2. Let candidate be the result of retargeting fullscreen element against this.
auto* candidate = retarget(fullscreen_element.ptr(), const_cast<Document*>(this));
if (!candidate) {
return nullptr;
}
// 3. If candidate and this are in the same tree, then return candidate.
if (auto* retargeted_element = as<Element>(candidate); retargeted_element && &retargeted_element->root() == &root()) {
return retargeted_element;
}
// 4. Return null.
return nullptr;
}
// https://fullscreen.spec.whatwg.org/#dom-document-fullscreen
bool Document::fullscreen() const
{
// The fullscreen getter steps are to return false if this's fullscreen element is null, and true otherwise.
return fullscreen_element() != nullptr;
}
// https://fullscreen.spec.whatwg.org/#dom-document-fullscreenenabled
bool Document::fullscreen_enabled() const
{
// FIXME: Implement check policy check and "is supported" check.
return is_allowed_to_use_feature(PolicyControlledFeature::Fullscreen);
}
// https://dom.spec.whatwg.org/#document-allow-declarative-shadow-roots
void Document::set_allow_declarative_shadow_roots(bool allow)
{

View file

@ -977,6 +977,13 @@ public:
void run_fullscreen_steps();
void append_pending_fullscreen_change(PendingFullscreenEvent::Type type, GC::Ref<Element> element);
void fullscreen_element_within_doc(GC::Ref<Element> element);
GC::Ptr<Element> fullscreen_element() const;
GC::Ptr<Element> fullscreen_element_for_bindings() const;
bool fullscreen() const;
bool fullscreen_enabled() const;
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; }

View file

@ -1,4 +1,5 @@
#import <CSS/StyleSheetList.idl>
#import <Fullscreen/DocumentOrShadowRootExtensions.idl>
// https://dom.spec.whatwg.org/#documentorshadowroot
interface mixin DocumentOrShadowRoot {

View file

@ -1,6 +1,7 @@
/*
* Copyright (c) 2018-2024, Andreas Kling <andreas@ladybird.org>
* Copyright (c) 2022-2023, Sam Atkins <atkinssj@serenityos.org>
* Copyright (c) 2025, Simon Farre <simon.farre.cx@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@ -61,9 +62,11 @@
#include <LibWeb/HTML/HTMLBaseElement.h>
#include <LibWeb/HTML/HTMLBodyElement.h>
#include <LibWeb/HTML/HTMLButtonElement.h>
#include <LibWeb/HTML/HTMLDialogElement.h>
#include <LibWeb/HTML/HTMLFieldSetElement.h>
#include <LibWeb/HTML/HTMLFrameSetElement.h>
#include <LibWeb/HTML/HTMLHtmlElement.h>
#include <LibWeb/HTML/HTMLIFrameElement.h>
#include <LibWeb/HTML/HTMLInputElement.h>
#include <LibWeb/HTML/HTMLLIElement.h>
#include <LibWeb/HTML/HTMLMenuElement.h>
@ -95,12 +98,15 @@
#include <LibWeb/Layout/ListItemBox.h>
#include <LibWeb/Layout/TreeBuilder.h>
#include <LibWeb/Layout/Viewport.h>
#include <LibWeb/MathML/MathMLElement.h>
#include <LibWeb/MathML/TagNames.h>
#include <LibWeb/Namespace.h>
#include <LibWeb/Page/Page.h>
#include <LibWeb/Painting/AccumulatedVisualContext.h>
#include <LibWeb/Painting/PaintableBox.h>
#include <LibWeb/Painting/StackingContext.h>
#include <LibWeb/Painting/ViewportPaintable.h>
#include <LibWeb/Platform/EventLoopPlugin.h>
#include <LibWeb/SVG/SVGAElement.h>
#include <LibWeb/Selection/Selection.h>
#include <LibWeb/TrustedTypes/RequireTrustedTypesForDirective.h>
@ -2419,6 +2425,186 @@ WebIDL::ExceptionOr<void> Element::insert_adjacent_html(String const& position,
return {};
}
// Used to signal what message should be shown when the promise for the algorithm rejects..
enum class RequestFullscreenError : u8 {
False,
ElementReadyCheckFailed,
UnsupportedElement,
NoTransientUserActivation,
ElementNodeDocIsNotPendingDoc
};
static constexpr String to_string(RequestFullscreenError error)
{
switch (error) {
// This should never be called with this value
case RequestFullscreenError::False:
return "false"_string;
case RequestFullscreenError::ElementReadyCheckFailed:
return "Element ready check failed"_string;
case RequestFullscreenError::UnsupportedElement:
return "Not supported element"_string;
case RequestFullscreenError::NoTransientUserActivation:
return "No transient user activation available to consume"_string;
case RequestFullscreenError::ElementNodeDocIsNotPendingDoc:
return "Element's node document is not pending doc"_string;
}
VERIFY_NOT_REACHED();
}
// step 5 of requestFullscreen:
// 5. If any of conditions are false, set error to true
static RequestFullscreenError fullscreen_has_error_check(Element const& element)
{
// Thiss namespace is the HTML namespace or this is an SVG svg or MathML math element. [SVG] [MATHML]
// FIXME: This likely wants to use is<MathML::MathMLMathElement> instead.
if (!(element.namespace_uri() == Namespace::HTML || element.is_svg_svg_element() || (is<MathML::MathMLElement>(element) && element.tag_name() == MathML::TagNames::math)))
return RequestFullscreenError::UnsupportedElement;
// This is not a dialog element
if (is<HTML::HTMLDialogElement>(element))
return RequestFullscreenError::UnsupportedElement;
// The fullscreen element ready check for this returns true.
if (!element.element_ready_check())
return RequestFullscreenError::ElementReadyCheckFailed;
// FIXME: Implement 'Fullscreen is supported.' check
// Thiss relevant global object has transient activation or
// FIXME: the algorithm is triggered by a user generated orientation change.
auto* window = as<HTML::Window>(&HTML::relevant_global_object(element));
if (!window->has_transient_activation())
return RequestFullscreenError::NoTransientUserActivation;
return RequestFullscreenError::False;
}
// https://fullscreen.spec.whatwg.org/#fullscreen-element-ready-check
bool Element::element_ready_check() const
{
// A fullscreen element ready check for an element element returns true if all of the following are true, and false otherwise:
// element is connected.
if (!is_connected())
return false;
// elements node document is allowed to use the "fullscreen" feature.
if (!m_document->is_allowed_to_use_feature(PolicyControlledFeature::Fullscreen))
return false;
// element namespace is not the HTML namespace or elements popover visibility state is hidden.
if (namespace_uri() != Namespace::HTML)
return true;
auto const* html_element = as_if<HTML::HTMLElement>(this);
return html_element ? (html_element->popover_visibility_state() == HTML::HTMLElement::PopoverVisibilityState::Hidden) : false;
}
// https://fullscreen.spec.whatwg.org/#dom-element-requestfullscreen
GC::Ref<WebIDL::Promise> Element::request_fullscreen()
{
auto& realm = this->realm();
// 1. Let pendingDoc be thiss node document.
auto pending_doc = m_document;
// 2. Let promise be a new promise.
auto promise = WebIDL::create_promise(realm);
// 3. If pendingDoc is not fully active, then reject promise with a TypeError exception and return promise.
if (!pending_doc->is_fully_active()) {
WebIDL::reject_promise(realm, promise, JS::TypeError::create(realm, "Document not fully active."_string));
return promise;
}
// 4. Let error be false.
// 5. If any of conditions are false, set error to true
auto error = fullscreen_has_error_check(*this);
// 6. If error is false, then consume user activation given pendingDocs relevant global object.
if (error == RequestFullscreenError::False) {
auto& relevant_global = as<HTML::Window>(relevant_global_object(*pending_doc));
relevant_global.consume_user_activation();
}
// 7. Return promise, and run the remaining steps in parallel.
Platform::EventLoopPlugin::the().deferred_invoke(GC::create_function(heap(), [&realm, error, pending_doc, requesting_element = GC::Ref { *this }, promise]() mutable {
HTML::TemporaryExecutionContext context(realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes);
// N.B: Fullscreen API is affected by site-isolation and will require additional work once site-isolation is implemented.
// 8. If error is false, then resize pendingDocs node navigables top-level traversables
// active documents viewports dimensions FIXME: optionally taking into account options["navigationUI"]:
if (error == RequestFullscreenError::False)
pending_doc->page().client().page_did_request_fullscreen_window();
// 9. If any of the following conditions are false, then set error to true:
// Thiss node document is pendingDoc.
// The fullscreen element ready check for this returns true.
if (pending_doc != requesting_element->owner_document())
error = RequestFullscreenError::ElementNodeDocIsNotPendingDoc;
if (!requesting_element->element_ready_check())
error = RequestFullscreenError::ElementReadyCheckFailed;
// 10. If error is true:
// Append (fullscreenerror, this) to pendingDocs list of pending fullscreen events.
// Reject promise with a TypeError exception and terminate these steps.
if (error != RequestFullscreenError::False) {
pending_doc->append_pending_fullscreen_change(PendingFullscreenEvent::Type::Error, requesting_element);
WebIDL::reject_promise(realm, promise, JS::TypeError::create(realm, to_string(error)));
return;
}
// 11. Let fullscreenElements be an ordered set initially consisting of this.
auto fullscreen_elements = realm.heap().allocate<GC::HeapVector<GC::Ref<Element>>>();
fullscreen_elements->elements().append(requesting_element);
// 12. While true:
while (true) {
// 1. Let last be the last item of fullscreenElements.
auto last = fullscreen_elements->elements().last();
// 2. Let container be lasts node navigables container.
auto container = last->navigable()->container();
// 3. If container is null, then break.
if (!container)
break;
// 4. Append container to fullscreenElements.
fullscreen_elements->elements().append(*container);
}
// 13. For each element in fullscreenElements:
for (auto& element : fullscreen_elements->elements()) {
// 1. Let doc be elements node document.
auto& doc = element->document();
// 2. If element is docs fullscreen element, continue.
if (doc.fullscreen_element() == element) {
// Spec note: No need to notify observers when nothing has changed.
continue;
}
// 3. If element is this and this is an iframe element, then set elements iframe fullscreen flag.
if (element == requesting_element && requesting_element->is_html_iframe_element()) {
auto& iframe_element = static_cast<HTML::HTMLIFrameElement&>(*element);
iframe_element.set_iframe_fullscreen_flag(true);
}
// 4. Fullscreen element within doc.
doc.fullscreen_element_within_doc(element);
// 5. Append (fullscreenchange, element) to docs list of pending fullscreen events.
doc.append_pending_fullscreen_change(PendingFullscreenEvent::Type::Change, element);
}
// 14. Resolve promise with undefined
WebIDL::resolve_promise(realm, promise, JS::js_undefined());
}));
return promise;
}
GC::Ptr<WebIDL::CallbackType> Element::onfullscreenchange()
{
return event_handler_attribute(HTML::EventNames::fullscreenchange);

View file

@ -262,6 +262,13 @@ public:
WebIDL::ExceptionOr<void> insert_adjacent_html(String const& position, TrustedTypes::TrustedHTMLOrString const&);
bool element_ready_check() const;
GC::Ref<WebIDL::Promise> request_fullscreen();
void removing_steps_fullscreen();
void set_fullscreen_flag(bool is_fullscreen) { m_fullscreen_flag = is_fullscreen; }
bool is_fullscreen_element() const { return m_fullscreen_flag; }
GC::Ptr<WebIDL::CallbackType> onfullscreenchange();
void set_onfullscreenchange(GC::Ptr<WebIDL::CallbackType>);
@ -659,6 +666,7 @@ private:
bool m_affected_by_sibling_position_or_count_pseudo_class : 1 { false };
bool m_affected_by_nth_child_pseudo_class : 1 { false };
bool m_affected_by_has_pseudo_class_with_relative_selector_that_has_sibling_combinator : 1 { false };
bool m_fullscreen_flag : 1 { false };
size_t m_sibling_invalidation_distance { 0 };

View file

@ -13,6 +13,7 @@
#include <LibWeb/DOM/Event.h>
#include <LibWeb/DOM/ShadowRoot.h>
#include <LibWeb/DOM/SlotRegistry.h>
#include <LibWeb/DOM/Utils.h>
#include <LibWeb/HTML/HTMLSlotElement.h>
#include <LibWeb/HTML/HTMLTemplateElement.h>
#include <LibWeb/HTML/Parser/HTMLParser.h>
@ -40,6 +41,30 @@ void ShadowRoot::finalize()
document().unregister_shadow_root({}, *this);
}
// https://fullscreen.spec.whatwg.org/#dom-document-fullscreenelement
GC::Ptr<Element> ShadowRoot::fullscreen_element_for_bindings() const
{
// 1. If this is a shadow root and its host is not connected, then return null.
if (!host() || !host()->is_connected())
return nullptr;
// 2. Let candidate be the result of retargeting fullscreen element against this.
// Note: ShadowRoot does not have it's own top layer. But the algorithm says to get the fullscreen
// element from the top layer, so it's grabbed from this' document.
auto* candidate = retarget(const_cast<ShadowRoot*>(this)->document().fullscreen_element().ptr(), const_cast<ShadowRoot*>(this));
if (!candidate)
return nullptr;
// 3. If candidate and this are in the same tree, then return candidate.
if (auto* retargeted_element = as<Element>(candidate); &retargeted_element->root() == &root())
return retargeted_element;
// 4. Return null.
return nullptr;
}
void ShadowRoot::initialize(JS::Realm& realm)
{
WEB_SET_PROTOTYPE_FOR_INTERFACE(ShadowRoot);

View file

@ -96,6 +96,8 @@ public:
virtual void finalize() override;
GC::Ptr<Element> fullscreen_element_for_bindings() const;
protected:
virtual void visit_edges(Cell::Visitor&) override;

View file

@ -2,6 +2,9 @@
// https://fullscreen.spec.whatwg.org/#ref-for-document
partial interface Document {
[LegacyLenientSetter] readonly attribute boolean fullscreenEnabled;
[LegacyLenientSetter, Unscopable] readonly attribute boolean fullscreen; // historical
attribute EventHandler onfullscreenchange;
attribute EventHandler onfullscreenerror;
};

View file

@ -0,0 +1,6 @@
#import <DOM/Element.idl>
// https://fullscreen.spec.whatwg.org/#ref-for-documentorshadowroot
partial interface mixin DocumentOrShadowRoot {
[LegacyLenientSetter, ImplementedAs=fullscreen_element_for_bindings] readonly attribute Element? fullscreenElement;
};

View file

@ -2,6 +2,8 @@
// https://fullscreen.spec.whatwg.org/#ref-for-element
partial interface Element {
Promise<undefined> requestFullscreen();
attribute EventHandler onfullscreenchange;
attribute EventHandler onfullscreenerror;
};

View file

@ -43,6 +43,9 @@ public:
virtual void visit_edges(Cell::Visitor&) override;
void set_iframe_fullscreen_flag(bool iframe_fullscreen_flag) { m_iframe_fullscreen_flag = iframe_fullscreen_flag; }
bool iframe_fullscreen_flag() const { return m_iframe_fullscreen_flag; }
private:
HTMLIFrameElement(DOM::Document&, DOM::QualifiedName);
@ -68,6 +71,9 @@ private:
// https://html.spec.whatwg.org/multipage/iframe-embed-object.html#current-navigation-was-lazy-loaded
bool m_current_navigation_was_lazy_loaded { false };
// https://fullscreen.spec.whatwg.org/#iframe-fullscreen-flag
bool m_iframe_fullscreen_flag { false };
// https://html.spec.whatwg.org/multipage/iframe-embed-object.html#iframe-pending-resource-timing-start-time
Optional<HighResolutionTime::DOMHighResTimeStamp> m_pending_resource_start_time = {};

View file

@ -687,12 +687,13 @@ void WebContentClient::did_request_minimize_window(u64 page_id)
}
}
void WebContentClient::did_request_fullscreen_window(u64 page_id)
Messages::WebContentClient::DidRequestFullscreenWindowResponse WebContentClient::did_request_fullscreen_window(u64 page_id)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_fullscreen_window)
view->on_fullscreen_window();
}
return true;
}
void WebContentClient::did_request_exit_fullscreen(u64 page_id)

View file

@ -130,7 +130,7 @@ private:
virtual void did_request_resize_window(u64 page_id, Gfx::IntSize) override;
virtual void did_request_maximize_window(u64 page_id) override;
virtual void did_request_minimize_window(u64 page_id) override;
virtual void did_request_fullscreen_window(u64 page_id) override;
virtual Messages::WebContentClient::DidRequestFullscreenWindowResponse did_request_fullscreen_window(u64 page_id) override;
virtual void did_request_exit_fullscreen(u64 page_id) override;
virtual void did_request_file(u64 page_id, ByteString path, i32) override;
virtual void did_request_color_picker(u64 page_id, Color current_color) override;

View file

@ -100,7 +100,7 @@ endpoint WebContentClient
did_request_resize_window(u64 page_id, Gfx::IntSize size) =|
did_request_maximize_window(u64 page_id) =|
did_request_minimize_window(u64 page_id) =|
did_request_fullscreen_window(u64 page_id) =|
did_request_fullscreen_window(u64 page_id) => (bool success)
did_request_exit_fullscreen(u64 page_id) =|
did_request_file(u64 page_id, ByteString path, i32 request_id) =|
did_request_color_picker(u64 page_id, Color current_color) =|

View file

@ -2,8 +2,8 @@ Harness status: OK
Found 17 tests
14 Pass
3 Fail
16 Pass
1 Fail
Pass String attribute setter should throw if this value is invalid
Pass String attribute setter should treat no arguments as undefined
Pass String attribute setter called with undefined should behave in the same way as no arguments
@ -17,7 +17,7 @@ Pass [Replaceable] setter called with other value should just work
Fail [LegacyLenientThis] setter should not throw even if this value is invalid, regardless of the arguments count
Pass [LegacyLenientThis] setter should treat no arguments as undefined
Pass [LegacyLenientThis] setter called with undefined should behave in the same way as no arguments
Fail [LegacyLenientSetter] setter should treat no arguments as undefined
Fail [LegacyLenientSetter] setter called with undefined should behave in the same way as no arguments
Pass [LegacyLenientSetter] setter should treat no arguments as undefined
Pass [LegacyLenientSetter] setter called with undefined should behave in the same way as no arguments
Pass [PutForward] setter should treat no arguments as undefined
Pass [PutForward] setter called with undefined should behave in the same way as no arguments