This commit is contained in:
Simon Farre 2025-04-19 07:18:49 -04:00 committed by GitHub
commit 6d5850ef68
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 955 additions and 5 deletions

View file

@ -959,6 +959,31 @@ button, meter, progress, select {
animation-fill-mode: inherit;
animation-delay: inherit;
}
/* Fullscreen API defaults https://fullscreen.spec.whatwg.org/#user-agent-level-style-sheet-defaults */
*|*:not(:root):fullscreen {
position:fixed !important;
inset:0 !important;
margin:0 !important;
box-sizing:border-box !important;
min-width:0 !important;
max-width:none !important;
min-height:0 !important;
max-height:none !important;
width:100% !important;
height:100% !important;
transform:none !important;
/* intentionally not !important */
object-fit:contain;
}
iframe:fullscreen {
border:none !important;
padding:0 !important;
}
*|*:not(:root):fullscreen::backdrop {
background:black;
}
/* Default cross-fade transition */
@keyframes -ua-view-transition-fade-out {

View file

@ -44,6 +44,9 @@
"focus-within": {
"argument": ""
},
"fullscreen": {
"argument": ""
},
"has": {
"argument": "<relative-selector-list>"
},

View file

@ -8,6 +8,7 @@
#include <LibWeb/CSS/ComputedProperties.h>
#include <LibWeb/CSS/Keyword.h>
#include <LibWeb/CSS/Parser/Parser.h>
#include <LibWeb/CSS/PseudoClass.h>
#include <LibWeb/CSS/SelectorEngine.h>
#include <LibWeb/DOM/Attr.h>
#include <LibWeb/DOM/Document.h>
@ -461,6 +462,9 @@ static inline bool matches_pseudo_class(CSS::Selector::SimpleSelector::PseudoCla
auto* focused_element = element.document().focused_element();
return focused_element && element.is_inclusive_ancestor_of(*focused_element);
}
case CSS::PseudoClass::Fullscreen: {
return element.is_fullscreen_element();
}
case CSS::PseudoClass::FirstChild:
if (context.collect_per_element_selector_involvement_metadata) {
const_cast<DOM::Element&>(element).set_affected_by_first_or_last_child_pseudo_class(true);

View file

@ -128,6 +128,7 @@
#include <LibWeb/HTML/Scripting/Agent.h>
#include <LibWeb/HTML/Scripting/ClassicScript.h>
#include <LibWeb/HTML/Scripting/ExceptionReporter.h>
#include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h>
#include <LibWeb/HTML/Scripting/WindowEnvironmentSettingsObject.h>
#include <LibWeb/HTML/SharedResourceRequest.h>
#include <LibWeb/HTML/Storage.h>
@ -582,6 +583,10 @@ void Document::visit_edges(Cell::Visitor& visitor)
visitor.visit(event.target);
}
for (auto& event : m_pending_fullscreen_events) {
visitor.visit(event.element);
}
visitor.visit(m_adopted_style_sheets);
for (auto& shadow_root : m_shadow_roots)
@ -4034,6 +4039,7 @@ void Document::run_unloading_cleanup_steps()
}
FileAPI::run_unloading_cleanup_steps(*this);
exit_fullscreen_fully();
}
// https://html.spec.whatwg.org/multipage/document-lifecycle.html#destroy-a-document
@ -4388,6 +4394,9 @@ bool Document::is_allowed_to_use_feature(PolicyControlledFeature feature) const
case PolicyControlledFeature::FocusWithoutUserActivation:
// FIXME: Implement allowlist for this.
return true;
case PolicyControlledFeature::Fullscreen:
// FIXME: Implement the permissions policy specification
return true;
}
// 4. Return false.
@ -6214,6 +6223,304 @@ void Document::remove_render_blocking_element(GC::Ref<Element> element)
m_render_blocking_elements.remove(element);
}
// https://fullscreen.spec.whatwg.org/#run-the-fullscreen-steps
void Document::run_fullscreen_steps()
{
// 1. Let pendingEvents be documents list of pending fullscreen events.
// 2. Empty documents list of pending fullscreen events.
Vector<PendingFullscreenEvent> pending_events;
std::swap(m_pending_fullscreen_events, pending_events);
// 3. For each (type, element) in pendingEvents:
for (auto const& [type, element] : pending_events) {
// 1. Let target be element if element is connected and its node document is document, and otherwise let target be document.
EventTarget* target = nullptr;
if (element->is_connected() && &element->document() == this) {
target = element;
} else {
target = this;
}
// 2. Fire an event named type, with its bubbles and composed attributes set to true, at target.
switch (type) {
case PendingFullscreenEvent::Type::Change:
target->dispatch_event(Event::create(realm(), HTML::EventNames::fullscreenchange, EventInit { .bubbles = true, .composed = true }));
break;
case PendingFullscreenEvent::Type::Error:
target->dispatch_event(Event::create(realm(), HTML::EventNames::fullscreenerror, EventInit { .bubbles = true, .composed = true }));
break;
}
}
}
void Document::append_pending_fullscreen_change(PendingFullscreenEvent::Type type, GC::Ref<Element> element)
{
m_pending_fullscreen_events.append(PendingFullscreenEvent { type, element });
}
WebIDL::CallbackType* Document::onfullscreenchange()
{
return event_handler_attribute(HTML::EventNames::fullscreenchange);
}
void Document::set_onfullscreenchange(WebIDL::CallbackType* value)
{
set_event_handler_attribute(HTML::EventNames::fullscreenchange, value);
}
WebIDL::CallbackType* Document::onfullscreenerror()
{
return event_handler_attribute(HTML::EventNames::fullscreenerror);
}
void Document::set_onfullscreenerror(WebIDL::CallbackType* value)
{
set_event_handler_attribute(HTML::EventNames::fullscreenerror, value);
}
// https://fullscreen.spec.whatwg.org/#fullscreen-an-element
void Document::fullscreen_element_within_doc(GC::Ref<Element> element)
{
using HTML::HTMLElement;
auto const get_hide_until = [&](auto const& popover_list) {
return 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.
// 2. If hideUntil is null, then set hideUntil to elements node document.
auto hide_until = get_hide_until(showing_hint_popover_list());
if (hide_until == nullptr) {
hide_until = get_hide_until(showing_auto_popover_list());
}
// 3. Run hide all popovers until given hideUntil, false, and true.
if (hide_until) {
HTMLElement::hide_all_popovers_until(hide_until, HTML::FocusPreviousElement::No, HTML::FireEvents::Yes);
} else {
HTMLElement::hide_all_popovers_until(element->owner_document(), 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);
}
GC::Ptr<Element> Document::fullscreen_element() const
{
GC::Ptr<Element> fullscreen_elem { nullptr };
for (auto const& el : top_layer_elements().in_reverse()) {
if (el->is_fullscreen_element()) {
fullscreen_elem = el;
break;
}
}
if (!fullscreen_elem) {
return nullptr;
}
// 1. If this is a shadow root and its host is not connected, then return null.
// - We're not a shadow root
// 2. Let candidate be the result of retargeting fullscreen element against this.
auto* candidate = retarget(fullscreen_elem.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;
}
bool Document::fullscreen() const
{
return fullscreen_element();
}
bool Document::fullscreen_enabled() const
{
// FIXME: Implement check policy check and "is supported" check.
return true;
}
void Document::exit_fullscreen_fully()
{
// 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.
Vector<GC::Ref<Element>, 8> fullscreen_elements;
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);
}
HTML::TemporaryExecutionContext context(relevant_settings_object().realm(), HTML::TemporaryExecutionContext::CallbacksEnabled::Yes);
// 3. Exit fullscreen document.
(void)exit_fullscreen();
}
GC::Ref<WebIDL::Promise> Document::exit_fullscreen()
{
auto* realm = vm().current_realm();
auto* doc = this;
// 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.
Vector<GC::Ptr<Document>> docs = doc->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.
if (top_level_doc->is_simple_fullscreen_document() && docs.contains_slow(top_level_doc)) {
doc = top_level_doc;
resize = true;
}
// 7. If docs fullscreen element is not connected:
// Append (fullscreenchange, docs fullscreen element) to docs list of pending fullscreen events.
// Unfullscreen docs fullscreen element.
if (auto fullscreen_element = doc->fullscreen_element(); !fullscreen_element->is_connected()) {
doc->append_pending_fullscreen_change(PendingFullscreenEvent::Type::Change, *fullscreen_element);
doc->unfullscreen_element(*fullscreen_element);
}
// 8. Return promise, and run the remaining steps in parallel.
// FIXME: 9. Run the fully unlock the screen orientation steps with doc.
// 10. If resize is true, resize docs viewport to its "normal" dimensions.
// FIXME: When async IPC lands, this request should be issued with async-with-callback/core promise
// and when the callback executes, *then* we queue the global task. For now, we just fire and forget the request and move on.
// this will also be useful when site isolation lands. Because it will need some form of coordination from a master process
// as this will make the "in parallel" steps a synchronized queue that will prevent multiple in-parallel fullscreen steps from racing
// see https://html.spec.whatwg.org/multipage/infrastructure.html#parallel-queue.
// N.B: Fullscreen API is affected by site-isolation and will required additional work once site-isolation is implemented.
if (resize)
doc->page().client().page_did_request_exit_fullscreen();
HTML::queue_global_task(HTML::Task::Source::UserInteraction, realm->global_object(), GC::create_function(heap(), [doc = GC::Ref { *this }, promise, realm, resize]() {
HTML::TemporaryExecutionContext context(*realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes);
// 11. If docs fullscreen element is null, then resolve promise with undefined and terminate these steps.
if (!doc->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 = doc->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.
Vector<GC::Ptr<Document>> descendant_docs;
for (auto& descendant : doc->descendant_navigables()) {
if (descendant->active_document()->fullscreen_element())
descendant_docs.append(descendant->active_document());
}
// 14. For each exitDoc in exitDocs:
for (auto const& exit_doc : exit_docs) {
// 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) {
// If resize is true, unfullscreen exitDoc.
for (auto el : exit_doc->top_layer_elements()) {
if (el->is_fullscreen_element())
exit_doc->unfullscreen_element(el);
}
} else {
// 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) {
auto el = descendant_doc->fullscreen_element();
// Append (fullscreenchange, descendantDocs fullscreen element) to descendantDocs list of pending fullscreen events.
descendant_doc->append_pending_fullscreen_change(PendingFullscreenEvent::Type::Change, *el);
// Unfullscreen descendantDoc.
Vector<GC::Ref<Element>, 8> fullscreen_elements;
for (auto const& element : descendant_doc->top_layer_elements()) {
if (element->is_fullscreen_element())
fullscreen_elements.append(element);
}
for (auto& el : fullscreen_elements) {
descendant_doc->unfullscreen_element(el);
}
}
// 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;
}
bool Document::is_simple_fullscreen_document() const
{
u32 total = 0;
for (auto const& element : top_layer_elements()) {
total += element->is_fullscreen_element();
if (total > 1)
return false;
}
return total == 1;
}
Vector<GC::Ptr<Document>> Document::collect_documents_to_unfullscreen() const
{
// 1. Let docs be an ordered set consisting of doc.
Vector<GC::Ptr<Document>> docs;
docs.append(GC::Ptr { const_cast<Document*>(this) });
// 2. While true:
for (;;) {
// 1. Let lastDoc be docss last document.
auto last_doc = docs.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.
auto container = last_doc->navigable()->container();
// 5. If container is null, then break.
if (!container)
break;
// 6. If containers iframe fullscreen flag is set, break.
if (HTML::HTMLIFrameElement* iframe_element = as<HTML::HTMLIFrameElement>(container.ptr()); iframe_element) {
if (iframe_element->iframe_fullscreen_flag())
break;
}
// 7. Append containers node document to docs.
docs.append(container->document());
}
// 3. Return docs.
return docs;
}
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);
HTML::HTMLIFrameElement* iframe_element = as_if<HTML::HTMLIFrameElement>(element.ptr());
if (iframe_element)
iframe_element->set_iframe_fullscreen_flag(false);
remove_an_element_from_the_top_layer_immediately(element);
invalidate_layout_tree(InvalidateLayoutTreeReason::DocumentAddAnElementToTheTopLayer);
}
// https://dom.spec.whatwg.org/#document-allow-declarative-shadow-roots
void Document::set_allow_declarative_shadow_roots(bool allow)
{

View file

@ -154,6 +154,15 @@ struct ElementCreationOptions {
enum class PolicyControlledFeature : u8 {
Autoplay,
FocusWithoutUserActivation,
Fullscreen,
};
struct PendingFullscreenEvent {
enum class Type {
Change,
Error,
} type;
GC::Ref<Element> element;
};
class Document
@ -896,6 +905,31 @@ public:
}
ElementByIdMap& element_by_id() const;
// https://fullscreen.spec.whatwg.org/#run-the-fullscreen-steps
void run_fullscreen_steps();
void append_pending_fullscreen_change(PendingFullscreenEvent::Type type, GC::Ref<Element> element);
// https://fullscreen.spec.whatwg.org/#api
[[nodiscard]] WebIDL::CallbackType* onfullscreenchange();
void set_onfullscreenchange(WebIDL::CallbackType*);
[[nodiscard]] WebIDL::CallbackType* onfullscreenerror();
void set_onfullscreenerror(WebIDL::CallbackType*);
// https://fullscreen.spec.whatwg.org/#fullscreen-an-element
void fullscreen_element_within_doc(GC::Ref<Element> element);
// https://fullscreen.spec.whatwg.org/#fullscreen-element
GC::Ptr<Element> fullscreen_element() const;
// https://fullscreen.spec.whatwg.org/#dom-document-fullscreen
bool fullscreen() const;
// https://fullscreen.spec.whatwg.org/#dom-document-fullscreenenabled
bool fullscreen_enabled() const;
// https://fullscreen.spec.whatwg.org/#fully-exit-fullscreen
void exit_fullscreen_fully();
// https://fullscreen.spec.whatwg.org/#exit-fullscreen
GC::Ref<WebIDL::Promise> exit_fullscreen();
void unfullscreen_element(GC::Ref<Element> element);
protected:
virtual void initialize(JS::Realm&) override;
@ -917,6 +951,10 @@ private:
void evaluate_media_rules();
// https://fullscreen.spec.whatwg.org/#simple-fullscreen-document
bool is_simple_fullscreen_document() const;
Vector<GC::Ptr<Document>> collect_documents_to_unfullscreen() const;
enum class AddLineFeed {
Yes,
No,
@ -1249,6 +1287,8 @@ private:
HashTable<GC::Ref<Element>> m_render_blocking_elements;
HashTable<WeakPtr<Node>> m_pending_nodes_for_style_invalidation_due_to_presence_of_has;
// https://fullscreen.spec.whatwg.org/#list-of-pending-fullscreen-events
Vector<PendingFullscreenEvent> m_pending_fullscreen_events;
};
template<>

View file

@ -156,6 +156,15 @@ interface Document : Node {
// special event handler IDL attributes that only apply to Document objects
[LegacyLenientThis] attribute EventHandler onreadystatechange;
attribute EventHandler onvisibilitychange;
// https://fullscreen.spec.whatwg.org/#api
attribute EventHandler onfullscreenchange;
attribute EventHandler onfullscreenerror;
// FIXME: [LegacyLenientSetter]
readonly attribute boolean fullscreenEnabled;
// FIXME: [LegacyLenientSetter, Unscopable]
readonly attribute boolean fullscreen; // historical
Promise<undefined> exitFullscreen();
};
dictionary ElementCreationOptions {

View file

@ -8,4 +8,8 @@ interface mixin DocumentOrShadowRoot {
// https://www.w3.org/TR/web-animations-1/#extensions-to-the-documentorshadowroot-interface-mixin
sequence<Animation> getAnimations();
// https://fullscreen.spec.whatwg.org/#fullscreen-an-element
// FIXME: [LegacyLenientSetter] not implemented
readonly attribute Element? fullscreenElement;
};

View file

@ -1,11 +1,13 @@
/*
* Copyright (c) 2018-2024, Andreas Kling <andreas@ladybird.org>
* Copyright (c) 2022-2023, San Atkins <atkinssj@serenityos.org>
* Copyright (c) 2025, Simon Farre <simon.farre.cx@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/AnyOf.h>
#include <AK/Assertions.h>
#include <AK/Debug.h>
#include <AK/StringBuilder.h>
#include <LibUnicode/CharacterTypes.h>
@ -45,9 +47,11 @@
#include <LibWeb/HTML/HTMLAreaElement.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>
@ -74,6 +78,7 @@
#include <LibWeb/Layout/ListItemBox.h>
#include <LibWeb/Layout/TreeBuilder.h>
#include <LibWeb/Layout/Viewport.h>
#include <LibWeb/MathML/MathMLElement.h>
#include <LibWeb/Namespace.h>
#include <LibWeb/Page/Page.h>
#include <LibWeb/Painting/PaintableBox.h>
@ -1179,6 +1184,7 @@ void Element::removed_from(Node* old_parent, Node& old_root)
if (m_name.has_value())
document().element_with_name_was_removed({}, *this);
}
removing_steps_fullscreen();
}
void Element::children_changed(ChildrenChangedMetadata const* metadata)
@ -1905,6 +1911,231 @@ WebIDL::ExceptionOr<void> Element::insert_adjacent_html(String const& position,
return {};
}
enum class RequestFullscreenError {
False,
ElementReadyCheckFailed,
UnsupportedElement,
NoTransientUserActivation,
ElementNodeDocIsNotPendingDoc
};
static constexpr AK::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:
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]
if (!(element.namespace_uri() == Namespace::HTML || element.is_svg_element() || is<MathML::MathMLElement>(element)))
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.
HTML::Window* window = as_if<HTML::Window>(&HTML::relevant_global_object(element));
// is_connected, in element_ready_check has returned true, there should be a window.
ASSERT(window);
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
{
// 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 html_element = as_if<const 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()
{
// 1. Let pendingDoc be thiss node document.
auto pending_doc = m_document;
auto* realm = vm().current_realm();
// 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
RequestFullscreenError 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.
HTML::queue_global_task(HTML::Task::Source::UserInteraction, realm->global_object(), GC::create_function(heap(), [realm, err = error, pending_doc, element = GC::Ref<Element> { *this }, promise]() {
HTML::TemporaryExecutionContext context(*realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes);
RequestFullscreenError error = err;
// 8. If error is false, then resize pendingDocs node navigables top-level traversables
// active documents viewports dimensions, optionally taking into account options["navigationUI"]:
// FIXME: When async IPC lands, instead of queue_global_task here, we would send a "go fullscreen" message
// and upon it's return then execute the in-parallell part (i.e. post this global task). Until then
// there's really no point, so just fire-and-forget. By also having this IPC message async,
// one can (either implicitly or explicitly) encode a "parallel queue" for these steps, so that at any one time
// only one execution of the "parallel steps" is happening at any one time - simply by sending an IPC message
// and resume the remaining steps only when it returns.
// See https://html.spec.whatwg.org/multipage/infrastructure.html#parallel-queue. This parallel queue could be called
// "Fullscreen Service" or "Fullscreen Parallel Queue".
// N.B: Fullscreen API is affected by site-isolation and will required additional work once site-isolation is implemented.
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 != element->owner_document())
error = RequestFullscreenError::ElementNodeDocIsNotPendingDoc;
if (!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, 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.
Vector<GC::Ref<Element>> fullscreen_elements;
fullscreen_elements.append(element);
// 12. While true:
// 1. Let last be the last item of fullscreenElements.
// 2. Let container be lasts node navigables container.
// 3. If container is null, then break.
// 4. Append container to fullscreenElements.
for (auto last = fullscreen_elements.last();; last = fullscreen_elements.last()) {
auto container = last->navigable()->container();
if (!container) {
break;
}
fullscreen_elements.append(*container);
}
// 13. For each element in fullscreenElements:
for (GC::Ref<Element>& el : fullscreen_elements) {
// 1. Let doc be elements node document.
Document& doc = el->document();
// 2. If element is docs fullscreen element, continue.
if (doc.fullscreen_element() == el) {
// Spec note: No need to notify observers when nothing has changed.
continue;
}
// 4. If element is this and this is an iframe element, then set elements iframe fullscreen flag.
if (el == element && element->is_html_iframe_element()) {
auto& iframe_element = static_cast<HTML::HTMLIFrameElement&>(*el);
iframe_element.set_iframe_fullscreen_flag(true);
}
// 5. Fullscreen element within doc.
doc.fullscreen_element_within_doc(el);
// 6. Append (fullscreenchange, element) to docs list of pending fullscreen events.
doc.append_pending_fullscreen_change(PendingFullscreenEvent::Type::Change, el);
}
// 14. Resolve promise with undefined
WebIDL::resolve_promise(*realm, promise, JS::js_undefined());
}));
return promise;
}
void Element::removing_steps_fullscreen()
{
// 1. Let document be removedNodes node document.
auto& doc = document();
// 2. Let nodes be removedNodes shadow-including inclusive descendants that have their fullscreen flag set, in shadow-including tree order.
// 3. For each node in nodes:
for_each_shadow_including_inclusive_descendant([&](Node& node) {
if (!is<Element>(node)) {
return TraversalDecision::Continue;
}
Element* element = static_cast<Element*>(&node);
if (element->is_fullscreen_element()) {
if (doc.fullscreen_element() == element) {
// 1. If node is documents fullscreen element, exit fullscreen document.
doc.exit_fullscreen();
} else {
// 2. Otherwise, unfullscreen node.
doc.unfullscreen_element(*element);
}
// 3. If documents top layer contains node, remove from the top layer immediately given node (handled above)
}
return TraversalDecision::Continue;
});
}
GC::Ptr<WebIDL::CallbackType> Element::onfullscreenchange()
{
return event_handler_attribute(HTML::EventNames::fullscreenchange);
}
void Element::set_onfullscreenchange(GC::Ptr<WebIDL::CallbackType> event_handler)
{
set_event_handler_attribute(HTML::EventNames::fullscreenchange, event_handler);
}
GC::Ptr<WebIDL::CallbackType> Element::onfullscreenerror()
{
return event_handler_attribute(HTML::EventNames::fullscreenerror);
}
void Element::set_onfullscreenerror(GC::Ptr<WebIDL::CallbackType> event_handler)
{
set_event_handler_attribute(HTML::EventNames::fullscreenerror, event_handler);
}
// https://dom.spec.whatwg.org/#insert-adjacent
WebIDL::ExceptionOr<GC::Ptr<Node>> Element::insert_adjacent(StringView where, GC::Ref<Node> node)
{

View file

@ -232,6 +232,19 @@ public:
WebIDL::ExceptionOr<void> insert_adjacent_html(String const& position, String 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>);
GC::Ptr<WebIDL::CallbackType> onfullscreenerror();
void set_onfullscreenerror(GC::Ptr<WebIDL::CallbackType>);
WebIDL::ExceptionOr<String> outer_html() const;
WebIDL::ExceptionOr<void> set_outer_html(String const&);
@ -568,6 +581,7 @@ private:
bool m_affected_by_first_or_last_child_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

@ -33,6 +33,17 @@ dictionary CheckVisibilityOptions {
boolean visibilityProperty = false;
};
// https://fullscreen.spec.whatwg.org/#api
enum FullscreenNavigationUI {
"auto",
"show",
"hide"
};
dictionary FullscreenOptions {
FullscreenNavigationUI navigationUI = "auto";
};
// https://dom.spec.whatwg.org/#element
[Exposed=Window]
interface Element : Node {
@ -124,6 +135,11 @@ interface Element : Node {
undefined setPointerCapture(long pointerId);
undefined releasePointerCapture(long pointerId);
boolean hasPointerCapture(long pointerId);
// partial interface for https://fullscreen.spec.whatwg.org/#api
Promise<undefined> requestFullscreen();
attribute EventHandler onfullscreenchange;
attribute EventHandler onfullscreenerror;
};
dictionary GetHTMLOptions {

View file

@ -59,6 +59,8 @@ enum class ShouldComputeRole {
X(EditingInsertion) \
X(ElementAttributeChange) \
X(ElementSetShadowRoot) \
X(FocusedElementChange) \
X(Fullscreen) \
X(HTMLHyperlinkElementHrefChange) \
X(HTMLIFrameElementGeometryChange) \
X(HTMLInputElementSetChecked) \

View file

@ -31,6 +31,13 @@ void ShadowRoot::finalize()
document().unregister_shadow_root({}, *this);
}
// https://fullscreen.spec.whatwg.org/#dom-document-fullscreenelement
GC::Ptr<Element> ShadowRoot::fullscreen_element() const
{
// FIXME: Should return a fullscreen element.
return nullptr;
}
void ShadowRoot::initialize(JS::Realm& realm)
{
Base::initialize(realm);

View file

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

View file

@ -379,7 +379,10 @@ void EventLoop::update_the_rendering()
document->update_animations_and_send_events(HighResolutionTime::relative_high_resolution_time(frame_timestamp, relevant_global_object(*document)));
};
// FIXME: 12. For each doc of docs, run the fullscreen steps for doc. [FULLSCREEN]
// 12. For each doc of docs, run the fullscreen steps for doc. [FULLSCREEN]
for (auto& document : docs) {
document->run_fullscreen_steps();
}
// FIXME: 13. For each doc of docs, if the user agent detects that the backing storage associated with a CanvasRenderingContext2D or an OffscreenCanvasRenderingContext2D, context, has been lost, then it must run the context lost steps for each such context:

View file

@ -62,6 +62,8 @@ namespace Web::HTML::EventNames {
__ENUMERATE_HTML_EVENT(focusin) \
__ENUMERATE_HTML_EVENT(focusout) \
__ENUMERATE_HTML_EVENT(formdata) \
__ENUMERATE_HTML_EVENT(fullscreenchange) \
__ENUMERATE_HTML_EVENT(fullscreenerror) \
__ENUMERATE_HTML_EVENT(hashchange) \
__ENUMERATE_HTML_EVENT(input) \
__ENUMERATE_HTML_EVENT(invalid) \

View file

@ -35,6 +35,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);
@ -64,6 +67,7 @@ private:
Optional<HighResolutionTime::DOMHighResTimeStamp> m_pending_resource_start_time = {};
GC::Ptr<DOM::DOMTokenList> m_sandbox;
bool m_iframe_fullscreen_flag : 1 { false };
};
void run_iframe_load_event_steps(HTML::HTMLIFrameElement&);

View file

@ -336,6 +336,7 @@ public:
virtual void page_did_request_maximize_window() { }
virtual void page_did_request_minimize_window() { }
virtual void page_did_request_fullscreen_window() { }
virtual void page_did_request_exit_fullscreen() { }
virtual void page_did_start_loading(URL::URL const&, bool is_redirect) { (void)is_redirect; }
virtual void page_did_create_new_document(Web::DOM::Document&) { }
virtual void page_did_change_active_document_in_top_level_browsing_context(Web::DOM::Document&) { }

View file

@ -425,6 +425,11 @@ void ViewImplementation::js_console_request_messages(i32 start_index)
client().async_js_console_request_messages(page_id(), start_index);
}
void ViewImplementation::exit_fullscreen()
{
client().async_exit_fullscreen(page_id());
}
void ViewImplementation::alert_closed()
{
client().async_alert_closed(page_id());

View file

@ -122,6 +122,7 @@ public:
void run_javascript(String const&);
void js_console_input(String const&);
void js_console_request_messages(i32 start_index);
void exit_fullscreen();
void alert_closed();
void confirm_closed(bool accepted);
@ -215,6 +216,7 @@ public:
Function<void()> on_maximize_window;
Function<void()> on_minimize_window;
Function<void()> on_fullscreen_window;
Function<void()> on_exit_fullscreen_window;
Function<void(Color current_color)> on_request_color_picker;
Function<void(Web::HTML::FileFilter const& accepted_file_types, Web::HTML::AllowMultipleFiles)> on_request_file_picker;
Function<void(Gfx::IntPoint content_position, i32 minimum_width, Vector<Web::HTML::SelectItem> items)> on_request_select_dropdown;

View file

@ -579,12 +579,21 @@ 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)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_fullscreen_window)
view->on_exit_fullscreen_window();
}
}
void WebContentClient::did_request_file(u64 page_id, ByteString path, i32 request_id)

View file

@ -114,7 +114,8 @@ 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;
virtual void did_request_file_picker(u64 page_id, Web::HTML::FileFilter accepted_file_types, Web::HTML::AllowMultipleFiles) override;

View file

@ -1299,4 +1299,10 @@ void ConnectionFromClient::system_time_zone_changed()
Unicode::clear_system_time_zone_cache();
}
void ConnectionFromClient::exit_fullscreen(u64 page_id)
{
if (auto page = this->page(page_id); page.has_value()) {
page.value().page().top_level_browsing_context().active_document()->exit_fullscreen_fully();
}
}
}

View file

@ -156,6 +156,7 @@ private:
virtual void paste(u64 page_id, String text) override;
virtual void system_time_zone_changed() override;
virtual void exit_fullscreen(u64 page_id) override;
NonnullOwnPtr<PageHost> m_page_host;

View file

@ -329,6 +329,11 @@ void PageClient::page_did_request_fullscreen_window()
client().async_did_request_fullscreen_window(m_id);
}
void PageClient::page_did_request_exit_fullscreen()
{
client().async_did_request_exit_fullscreen(m_id);
}
void PageClient::page_did_request_tooltip_override(Web::CSSPixelPoint position, ByteString const& title)
{
auto device_position = page().css_to_device_point(position);

View file

@ -127,6 +127,7 @@ private:
virtual void page_did_request_maximize_window() override;
virtual void page_did_request_minimize_window() override;
virtual void page_did_request_fullscreen_window() override;
virtual void page_did_request_exit_fullscreen() override;
virtual void page_did_request_tooltip_override(Web::CSSPixelPoint, ByteString const&) override;
virtual void page_did_stop_tooltip_override() override;
virtual void page_did_enter_tooltip_area(ByteString const&) override;

View file

@ -83,7 +83,8 @@ 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) =|
did_request_file_picker(u64 page_id, Web::HTML::FileFilter accepted_file_types, Web::HTML::AllowMultipleFiles allow_multiple_files) =|

View file

@ -127,4 +127,5 @@ endpoint WebContentServer
set_user_style(u64 page_id, String source) =|
system_time_zone_changed() =|
exit_fullscreen(u64 page_id) =|
}

View file

@ -29,6 +29,7 @@ HeadlessWebView::HeadlessWebView(Core::AnonymousBuffer theme, Web::DevicePixelSi
};
on_reposition_window = [this](auto position) {
m_cached_dims.set_location(position.template to_type<Web::DevicePixels>());
client().async_set_window_position(m_client_state.page_index, position.template to_type<Web::DevicePixels>());
client().async_did_update_window_rect(m_client_state.page_index);
@ -53,6 +54,7 @@ HeadlessWebView::HeadlessWebView(Core::AnonymousBuffer theme, Web::DevicePixelSi
on_maximize_window = [this]() {
m_viewport_size = screen_rect.size();
m_cached_dims = screen_rect;
client().async_set_window_position(m_client_state.page_index, screen_rect.location());
client().async_set_window_size(m_client_state.page_index, screen_rect.size());
@ -62,6 +64,7 @@ HeadlessWebView::HeadlessWebView(Core::AnonymousBuffer theme, Web::DevicePixelSi
};
on_fullscreen_window = [this]() {
m_cached_dims.set_size(m_viewport_size);
m_viewport_size = screen_rect.size();
client().async_set_window_position(m_client_state.page_index, screen_rect.location());
@ -71,6 +74,15 @@ HeadlessWebView::HeadlessWebView(Core::AnonymousBuffer theme, Web::DevicePixelSi
client().async_did_update_window_rect(m_client_state.page_index);
};
on_exit_fullscreen_window = [this]() {
m_viewport_size = m_cached_dims.size();
client().async_set_window_position(m_client_state.page_index, m_cached_dims.location());
client().async_set_window_size(m_client_state.page_index, m_cached_dims.size());
client().async_set_viewport_size(m_client_state.page_index, m_cached_dims.size());
client().async_did_update_window_rect(m_client_state.page_index);
};
on_request_alert = [this](auto const&) {
m_pending_dialog = Web::Page::PendingDialog::Alert;
};

View file

@ -51,6 +51,7 @@ private:
Web::Page::PendingDialog m_pending_dialog { Web::Page::PendingDialog::None };
Optional<String> m_pending_prompt_text;
Web::DevicePixelRect m_cached_dims;
};
}

View file

@ -4,10 +4,12 @@
* Copyright (c) 2022, Filiph Sandström <filiph.sandstrom@filfatstudios.com>
* Copyright (c) 2023, Linus Groh <linusg@serenityos.org>
* Copyright (c) 2024, Sam Atkins <sam@ladybird.org>
* Copyright (c) 2025, Simon Farre <simon.farre.cx@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/RefPtr.h>
#include <AK/TypeCasts.h>
#include <LibWeb/CSS/PreferredColorScheme.h>
#include <LibWeb/CSS/PreferredContrast.h>
@ -32,12 +34,133 @@
#include <QInputDialog>
#include <QMessageBox>
#include <QPlainTextEdit>
#include <QPropertyAnimation>
#include <QPushButton>
#include <QShortcut>
#include <QStatusBar>
#include <QTabBar>
#include <QTimer>
#include <QWidget>
#include <QWindow>
#include <qnamespace.h>
namespace Ladybird {
FullscreenMode::FullscreenMode(BrowserWindow* window, ExitFullscreenButton* exit_button)
: QObject(window)
, m_window(window)
, m_exit_button(exit_button)
{
connect(m_exit_button, &QPushButton::clicked, this, [this]() {
exit();
});
}
void FullscreenMode::exit()
{
// If there's a document tree in fullscreen, exit fully on root document.
if (is_api_fullscreen()) {
qApp->removeEventFilter(this);
if (m_window->tab_index(m_fullscreen_tab) != -1) {
m_fullscreen_tab->view().exit_fullscreen();
}
emit on_exit_fullscreen();
}
m_fullscreen_tab = nullptr;
}
void FullscreenMode::enter(Tab* tab)
{
qApp->installEventFilter(this);
m_fullscreen_tab = tab;
m_window->enter_fullscreen();
}
void FullscreenMode::entered_fullscreen()
{
m_debounce = true;
m_exit_button->animate_show();
// Let button float in place 3 * time it takes to animate it in place
QTimer::singleShot(button_animation_time() * 3, [this]() { m_debounce = false; });
}
bool FullscreenMode::is_api_fullscreen() const
{
return m_fullscreen_tab;
}
bool FullscreenMode::debounce() const
{
return m_debounce;
}
void FullscreenMode::maybe_animate_show_exit_button(QPointF pos)
{
u64 const mouse_y = static_cast<u64>(pos.y());
u64 const threshold = static_cast<u64>(m_window->height() * 0.01);
if (debounce()) {
return;
}
// Display the button if the mouse is 1% from the top
if (mouse_y <= threshold) {
if (!m_exit_button->isVisible()) {
m_debounce = true;
m_exit_button->animate_show();
QTimer::singleShot(button_animation_time() * 3, [this]() { m_debounce = false; });
}
} else if (mouse_y > (threshold * 10) && m_exit_button->isVisible()) {
// if the button has floated in, we want to hide it when leaving the top 10%
m_exit_button->hide();
}
}
bool FullscreenMode::eventFilter(QObject* obj, QEvent* event)
{
ASSERT(is_api_fullscreen());
if (event->type() == QEvent::MouseMove) {
QMouseEvent* mouse_event = static_cast<QMouseEvent*>(event);
maybe_animate_show_exit_button(mouse_event->pos());
}
if (event->type() == QEvent::KeyPress) {
QKeyEvent* key = static_cast<QKeyEvent*>(event);
if (key->key() == Qt::Key_Escape)
exit();
}
return QObject::eventFilter(obj, event);
}
ExitFullscreenButton::ExitFullscreenButton(QWidget* parent)
: QPushButton("Exit fullscreen", parent)
{
setStyleSheet("background-color:rgb(55, 99, 129); color: white; padding: 10px; border-radius: 5px;");
adjustSize();
hide();
m_widget_animation = new QPropertyAnimation(this, "pos");
}
void ExitFullscreenButton::animate_show()
{
if (isVisible())
return;
show();
QScreen* current_screen = screen();
QRect screen_geometry = current_screen->geometry();
int const destination_x = (screen_geometry.width() - width()) / 2;
int const destination_y = static_cast<int>(static_cast<float>(screen_geometry.height()) * 0.05);
m_widget_animation->setDuration(FullscreenMode::button_animation_time());
m_widget_animation->setStartValue(QPoint(destination_x, -height()));
m_widget_animation->setEndValue(QPoint(destination_x, destination_y));
m_widget_animation->setEasingCurve(QEasingCurve::OutBounce);
m_widget_animation->start();
}
static QIcon const& app_icon()
{
static QIcon icon;
@ -618,12 +741,19 @@ BrowserWindow::BrowserWindow(Vector<URL::URL> const& initial_urls, IsPopupWindow
(void)static_cast<Ladybird::Application*>(QApplication::instance())->new_window({});
});
QObject::connect(open_file_action, &QAction::triggered, this, &BrowserWindow::open_file);
m_exit_button = new ExitFullscreenButton { this };
m_fullscreen_mode = new FullscreenMode { this, m_exit_button };
connect(m_fullscreen_mode, &FullscreenMode::on_exit_fullscreen, this, &BrowserWindow::exit_fullscreen);
connect(m_fullscreen_mode, &FullscreenMode::on_exit_fullscreen, m_exit_button, &ExitFullscreenButton::hide);
QObject::connect(m_tabs_container, &QTabWidget::currentChanged, [this](int index) {
auto* tab = as<Tab>(m_tabs_container->widget(index));
if (tab)
setWindowTitle(QString("%1 - Ladybird").arg(tab->title()));
set_current_tab(tab);
fullscreen_mode().exit();
});
QObject::connect(m_tabs_container, &QTabWidget::tabCloseRequested, this, &BrowserWindow::close_tab);
QObject::connect(close_current_tab_action, &QAction::triggered, this, &BrowserWindow::close_current_tab);
@ -762,6 +892,11 @@ Tab& BrowserWindow::create_new_tab(Web::HTML::ActivateTab activate_tab, Tab& par
return *tab;
}
FullscreenMode& BrowserWindow::fullscreen_mode()
{
return *m_fullscreen_mode;
}
Tab& BrowserWindow::create_new_tab(Web::HTML::ActivateTab activate_tab)
{
auto* tab = new Tab(this);
@ -1157,6 +1292,24 @@ void BrowserWindow::copy_selected_text()
clipboard->setText(qstring_from_ak_string(text));
}
void BrowserWindow::enter_fullscreen()
{
m_tabs_container->tabBar()->hide();
m_tabs_container->cornerWidget()->hide();
m_restore_to_maximized = isMaximized();
showFullScreen();
}
void BrowserWindow::exit_fullscreen()
{
m_tabs_container->tabBar()->show();
m_tabs_container->cornerWidget()->show();
if (m_restore_to_maximized)
showMaximized();
else
showNormal();
}
bool BrowserWindow::event(QEvent* event)
{
#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0)
@ -1181,6 +1334,17 @@ void BrowserWindow::resizeEvent(QResizeEvent* event)
});
}
void BrowserWindow::changeEvent(QEvent* event)
{
if (event->type() == QEvent::WindowStateChange) {
QWindowStateChangeEvent* stateChangeEvent = static_cast<QWindowStateChangeEvent*>(event);
if (windowState() & Qt::WindowFullScreen && !(stateChangeEvent->oldState() & Qt::WindowFullScreen)) {
m_fullscreen_mode->entered_fullscreen();
}
}
QWidget::changeEvent(event);
}
void BrowserWindow::moveEvent(QMoveEvent* event)
{
QWidget::moveEvent(event);

View file

@ -18,13 +18,62 @@
#include <QLineEdit>
#include <QMainWindow>
#include <QMenuBar>
#include <QPushButton>
#include <QTabBar>
#include <QTabWidget>
#include <QToolBar>
class QPropertyAnimation;
namespace Ladybird {
class WebContentView;
class BrowserWindow;
class ExitFullscreenButton : public QPushButton {
Q_OBJECT
public:
ExitFullscreenButton(QWidget* parent = nullptr);
~ExitFullscreenButton() override = default;
void animate_show();
private:
QPropertyAnimation* m_widget_animation;
};
// Handles Qt UI state related to when Ladybird has entered fullscreen,
// like displaying an exit button, listening for escape key presses and so on
class FullscreenMode : public QObject {
Q_OBJECT
public:
static constexpr int button_animation_time() { return 750; }
explicit FullscreenMode(BrowserWindow* window, ExitFullscreenButton* exit_button);
void exit();
void enter(Tab* tab);
// Called after a window change event that has identifed the current window state to be fullscreen.
void entered_fullscreen();
bool is_api_fullscreen() const;
signals:
void on_exit_fullscreen();
protected:
// FullscreenMode's eventFilter is responsible for things that need to happen when a document
// is in "fullscreen API fullscreen", like exiting when pushing escape, showing the exit button
virtual bool eventFilter(QObject* obj, QEvent* event) override;
private:
bool debounce() const;
// Called when in fullscreen. Displays exit fullscreen button if mouse comes close to the top of the screen.
void maybe_animate_show_exit_button(QPointF pos);
BrowserWindow* m_window;
ExitFullscreenButton* m_exit_button;
// Never access this directly. First check m_window->tab_index(m_fullscreen_tab) != -1, to verify it's liveness.
Tab* m_fullscreen_tab { nullptr };
bool m_debounce { false };
};
class BrowserWindow : public QMainWindow {
Q_OBJECT
@ -42,6 +91,7 @@ public:
int tab_count() { return m_tabs_container->count(); }
int tab_index(Tab*);
Tab& create_new_tab(Web::HTML::ActivateTab activate_tab);
FullscreenMode& fullscreen_mode();
QMenu& hamburger_menu()
{
@ -132,6 +182,8 @@ public slots:
void show_find_in_page();
void paste();
void copy_selected_text();
void enter_fullscreen();
void exit_fullscreen();
protected:
bool eventFilter(QObject* obj, QEvent* event) override;
@ -139,6 +191,7 @@ protected:
private:
virtual bool event(QEvent*) override;
virtual void resizeEvent(QResizeEvent*) override;
virtual void changeEvent(QEvent* event) override;
virtual void moveEvent(QMoveEvent*) override;
virtual void wheelEvent(QWheelEvent*) override;
virtual void closeEvent(QCloseEvent*) override;
@ -210,6 +263,11 @@ private:
ByteString m_navigator_compatibility_mode {};
IsPopupWindow m_is_popup_window { IsPopupWindow::No };
ExitFullscreenButton* m_exit_button { nullptr };
FullscreenMode* m_fullscreen_mode { nullptr };
// Determine if window should restore to maximized or normal, when exiting fullscreen.
bool m_restore_to_maximized { false };
};
}

View file

@ -365,7 +365,16 @@ Tab::Tab(BrowserWindow* window, RefPtr<WebView::WebContentClient> parent_client,
};
view().on_fullscreen_window = [this]() {
m_window->showFullScreen();
BrowserWindow* window = static_cast<BrowserWindow*>(m_window);
m_toolbar->hide();
window->fullscreen_mode().enter(this);
view().did_update_window_rect();
};
view().on_exit_fullscreen_window = [this]() {
BrowserWindow* window = static_cast<BrowserWindow*>(m_window);
window->fullscreen_mode().exit();
m_toolbar->show();
view().did_update_window_rect();
};