mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-04-21 12:05:15 +00:00
Merge 0cc3c78181
into 8e37cd2f71
This commit is contained in:
commit
6d5850ef68
32 changed files with 955 additions and 5 deletions
|
@ -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 {
|
||||
|
|
|
@ -44,6 +44,9 @@
|
|||
"focus-within": {
|
||||
"argument": ""
|
||||
},
|
||||
"fullscreen": {
|
||||
"argument": ""
|
||||
},
|
||||
"has": {
|
||||
"argument": "<relative-selector-list>"
|
||||
},
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 document’s list of pending fullscreen events.
|
||||
// 2. Empty document’s 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 element’s 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 element’s 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 document’s 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 document’s top layer, except for document’s 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 doc’s 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 doc’s node navigable’s top-level traversable’s 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 doc’s fullscreen element is not connected:
|
||||
// Append (fullscreenchange, doc’s fullscreen element) to doc’s list of pending fullscreen events.
|
||||
// Unfullscreen doc’s 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 doc’s 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 doc’s 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 doc’s 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, exitDoc’s fullscreen element) to exitDoc’s 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 exitDoc’s 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, descendantDoc’s fullscreen element) to descendantDoc’s 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 docs’s last document.
|
||||
auto last_doc = docs.last();
|
||||
// 2. Assert: lastDoc’s 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 lastDoc’s node navigable’s container.
|
||||
auto container = last_doc->navigable()->container();
|
||||
// 5. If container is null, then break.
|
||||
if (!container)
|
||||
break;
|
||||
// 6. If container’s 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 container’s 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 element’s 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)
|
||||
{
|
||||
|
|
|
@ -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<>
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
// This’s 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
|
||||
|
||||
// This’s 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;
|
||||
|
||||
// element’s 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 element’s 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 this’s 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 pendingDoc’s 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 pendingDoc’s node navigable’s top-level traversable’s
|
||||
// active document’s viewport’s 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:
|
||||
// This’s 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 pendingDoc’s 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 last’s node navigable’s 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 element’s node document.
|
||||
Document& doc = el->document();
|
||||
// 2. If element is doc’s 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 element’s 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 doc’s 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 removedNode’s node document.
|
||||
auto& doc = document();
|
||||
// 2. Let nodes be removedNode’s 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 document’s fullscreen element, exit fullscreen document.
|
||||
doc.exit_fullscreen();
|
||||
} else {
|
||||
// 2. Otherwise, unfullscreen node.
|
||||
doc.unfullscreen_element(*element);
|
||||
}
|
||||
// 3. If document’s 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)
|
||||
{
|
||||
|
|
|
@ -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 };
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -59,6 +59,8 @@ enum class ShouldComputeRole {
|
|||
X(EditingInsertion) \
|
||||
X(ElementAttributeChange) \
|
||||
X(ElementSetShadowRoot) \
|
||||
X(FocusedElementChange) \
|
||||
X(Fullscreen) \
|
||||
X(HTMLHyperlinkElementHrefChange) \
|
||||
X(HTMLIFrameElementGeometryChange) \
|
||||
X(HTMLInputElementSetChecked) \
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -67,6 +67,8 @@ public:
|
|||
|
||||
virtual void finalize() override;
|
||||
|
||||
GC::Ptr<Element> fullscreen_element() const;
|
||||
|
||||
protected:
|
||||
virtual void visit_edges(Cell::Visitor&) override;
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -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) \
|
||||
|
|
|
@ -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&);
|
||||
|
|
|
@ -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&) { }
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) =|
|
||||
|
|
|
@ -127,4 +127,5 @@ endpoint WebContentServer
|
|||
set_user_style(u64 page_id, String source) =|
|
||||
|
||||
system_time_zone_changed() =|
|
||||
exit_fullscreen(u64 page_id) =|
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 };
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
};
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue