Files
ladybird/Libraries/LibWeb/Page/Page.cpp
Zaggy1024 2e54c18fb3 LibWeb: Use a queue to process fullscreen request completions
Instead of immediately firing fullscreenchange, defer that until
WebContent's client has confirmed that it is in fullscreen for the
content. The fullscreenchange is fired by the viewport change, so in
cases where the fullscreen transition is instantaneous (i.e. the
fullscreen state is entered at the exact moment the viewport expands),
the resize event should precede the fullscreenchange event, as the spec
requires.

This fixes the WPT element-request-fullscreen-timing.html test, which
was previously succeeding by accident because we were immediately
fullscreenchange upon requestFullscreen() being called, instead of
following spec and doing the viewport (window) resize in parallel. The
WPT test was actually initially intended to assert that the
fullscreenchange event follows the resize event, but the WPT runner
didn't actually have a different resolution for normal vs fullscreen
viewports, so the resize event doesn't actually fire in their setup. In
our headless mode, the default viewport is 800x600, and the fullscreen
viewport is 1920x1080, so we do fire a resize event when entering
fullscreen. Therefore, that imported test is reverted to assert that
the resize precedes the fullscreenchange.
2026-03-17 18:58:37 -05:00

1098 lines
40 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* Copyright (c) 2020, Andreas Kling <andreas@ladybird.org>
* Copyright (c) 2022, Sam Atkins <atkinssj@serenityos.org>
* Copyright (c) 2024-2025, Tim Ledbetter <tim.ledbetter@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/SourceLocation.h>
#include <LibIPC/Decoder.h>
#include <LibIPC/Encoder.h>
#include <LibWeb/Bindings/ExceptionOrUtils.h>
#include <LibWeb/CSS/StyleComputer.h>
#include <LibWeb/Clipboard/SystemClipboard.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/DOM/Element.h>
#include <LibWeb/DOM/Range.h>
#include <LibWeb/HTML/BrowsingContext.h>
#include <LibWeb/HTML/EventLoop/EventLoop.h>
#include <LibWeb/HTML/HTMLIFrameElement.h>
#include <LibWeb/HTML/HTMLInputElement.h>
#include <LibWeb/HTML/HTMLMediaElement.h>
#include <LibWeb/HTML/HTMLSelectElement.h>
#include <LibWeb/HTML/Scripting/Environments.h>
#include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h>
#include <LibWeb/HTML/SelectedFile.h>
#include <LibWeb/HTML/TraversableNavigable.h>
#include <LibWeb/HTML/Window.h>
#include <LibWeb/Page/Page.h>
#include <LibWeb/Platform/EventLoopPlugin.h>
#include <LibWeb/Selection/Selection.h>
namespace Web {
GC_DEFINE_ALLOCATOR(Page);
GC::Ref<Page> Page::create(JS::VM& vm, GC::Ref<PageClient> page_client)
{
return vm.heap().allocate<Page>(page_client);
}
Page::Page(GC::Ref<PageClient> client)
: m_client(client)
{
}
Page::~Page() = default;
void Page::visit_edges(JS::Cell::Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_top_level_traversable);
visitor.visit(m_client);
visitor.visit(m_window_rect_observer);
visitor.visit(m_on_pending_dialog_closed);
visitor.visit(m_pending_clipboard_requests);
m_pending_fullscreen_operations.for_each([&](auto const& operation) {
operation.visit([&](PendingFullscreenEnter const& enter_operation) {
visitor.visit(enter_operation.element);
visitor.visit(enter_operation.pending_doc);
visitor.visit(enter_operation.promise); },
[&](PendingFullscreenExit const& exit_operation) {
visitor.visit(exit_operation.doc);
visitor.visit(exit_operation.promise);
});
});
}
HTML::Navigable& Page::focused_navigable()
{
if (m_focused_navigable)
return *m_focused_navigable;
return top_level_traversable();
}
void Page::set_focused_navigable(Badge<EventHandler>, HTML::Navigable& navigable)
{
m_focused_navigable = navigable;
}
void Page::navigable_document_destroyed(Badge<DOM::Document>, HTML::Navigable& navigable)
{
if (&navigable == m_focused_navigable.ptr())
m_focused_navigable = nullptr;
}
void Page::load(URL::URL const& url)
{
(void)top_level_traversable()->navigate({ .url = url, .source_document = *top_level_traversable()->active_document(), .user_involvement = HTML::UserNavigationInvolvement::BrowserUI });
}
void Page::load_html(StringView html)
{
// FIXME: #23909 Figure out why GC threshold does not stay low when repeatedly loading html from the WebView
heap().collect_garbage();
(void)top_level_traversable()->navigate({ .url = URL::about_srcdoc(),
.source_document = *top_level_traversable()->active_document(),
.document_resource = String::from_utf8(html).release_value_but_fixme_should_propagate_errors(),
.user_involvement = HTML::UserNavigationInvolvement::BrowserUI });
}
void Page::reload()
{
top_level_traversable()->reload();
}
void Page::traverse_the_history_by_delta(int delta)
{
top_level_traversable()->traverse_the_history_by_delta(delta);
}
Gfx::Palette Page::palette() const
{
return m_client->palette();
}
// https://drafts.csswg.org/cssom-view-1/#web-exposed-screen-area
CSSPixelRect Page::web_exposed_screen_area() const
{
// FIXME: 1. Let target be thiss relevant global objects browsing context.
// FIXME: 2. Let emulated screen area be the WebDriver BiDi emulated total screen area of target.
// FIXME: 3. If emulated screen area is not null, return emulated screen area.
// 4. Otherwise, return one of the following:
// - The area of the output device, in CSS pixels.
// - The area of the viewport, in CSS pixels.
// NB: This is the area of the output device, but in device pixels.
// See: https://github.com/LadybirdBrowser/ladybird/pull/4084
auto device_pixel_rect = m_client->screen_rect();
return {
device_pixel_rect.x().value(),
device_pixel_rect.y().value(),
device_pixel_rect.width().value(),
device_pixel_rect.height().value()
};
}
// https://drafts.csswg.org/cssom-view-1/#web-exposed-available-screen-area
CSSPixelRect Page::web_exposed_available_screen_area() const
{
// FIXME: 1. Let target be thiss relevant global objects browsing context.
// FIXME: 2. Let emulated screen area be the WebDriver BiDi emulated total screen area of target.
// FIXME: 3. If emulated screen area is not null, return emulated screen area.
// 4. Otherwise, return one of the following:
// - The available area of the rendering surface of the output device, in CSS pixels.
// - The area of the output device, in CSS pixels.
// - The area of the viewport, in CSS pixels.
// NB: This is the area of the output device, but in device pixels. See note in web_exposed_screen_area()
auto device_pixel_rect = m_client->screen_rect();
return {
device_pixel_rect.x().value(),
device_pixel_rect.y().value(),
device_pixel_rect.width().value(),
device_pixel_rect.height().value()
};
}
CSS::PreferredColorScheme Page::preferred_color_scheme() const
{
auto preferred_color_scheme = m_client->preferred_color_scheme();
if (preferred_color_scheme == CSS::PreferredColorScheme::Auto)
preferred_color_scheme = palette().is_dark() ? CSS::PreferredColorScheme::Dark : CSS::PreferredColorScheme::Light;
return preferred_color_scheme;
}
CSS::PreferredContrast Page::preferred_contrast() const
{
return m_client->preferred_contrast();
}
CSS::PreferredMotion Page::preferred_motion() const
{
return m_client->preferred_motion();
}
CSSPixelPoint Page::device_to_css_point(DevicePixelPoint point) const
{
return {
point.x().value() / client().device_pixels_per_css_pixel(),
point.y().value() / client().device_pixels_per_css_pixel(),
};
}
DevicePixelPoint Page::css_to_device_point(CSSPixelPoint point) const
{
return {
point.x() * client().device_pixels_per_css_pixel(),
point.y() * client().device_pixels_per_css_pixel(),
};
}
DevicePixelRect Page::css_to_device_rect(CSSPixelRect rect) const
{
return {
rect.location().to_type<double>() * client().device_pixels_per_css_pixel(),
rect.size().to_type<double>() * client().device_pixels_per_css_pixel(),
};
}
CSSPixelRect Page::device_to_css_rect(DevicePixelRect rect) const
{
auto scale = client().device_pixels_per_css_pixel();
return {
CSSPixels::nearest_value_for(rect.x().value() / scale),
CSSPixels::nearest_value_for(rect.y().value() / scale),
CSSPixels::floored_value_for(rect.width().value() / scale),
CSSPixels::floored_value_for(rect.height().value() / scale),
};
}
CSSPixelSize Page::device_to_css_size(DevicePixelSize size) const
{
auto scale = client().device_pixels_per_css_pixel();
return {
CSSPixels::floored_value_for(size.width().value() / scale),
CSSPixels::floored_value_for(size.height().value() / scale),
};
}
DevicePixelRect Page::enclosing_device_rect(CSSPixelRect rect) const
{
auto scale = client().device_pixels_per_css_pixel();
return DevicePixelRect(
floor(rect.x().to_double() * scale),
floor(rect.y().to_double() * scale),
ceil(rect.width().to_double() * scale),
ceil(rect.height().to_double() * scale));
}
DevicePixelRect Page::rounded_device_rect(CSSPixelRect rect) const
{
auto scale = client().device_pixels_per_css_pixel();
return {
roundf(rect.x().to_double() * scale),
roundf(rect.y().to_double() * scale),
roundf(rect.width().to_double() * scale),
roundf(rect.height().to_double() * scale)
};
}
ChromeMetrics Page::chrome_metrics() const
{
return ChromeMetrics { m_client->zoom_level() };
}
EventResult Page::handle_mouseup(DevicePixelPoint position, DevicePixelPoint screen_position, unsigned button, unsigned buttons, unsigned modifiers)
{
return top_level_traversable()->event_handler().handle_mouseup(device_to_css_point(position), device_to_css_point(screen_position), button, buttons, modifiers);
}
EventResult Page::handle_mousedown(DevicePixelPoint position, DevicePixelPoint screen_position, unsigned button, unsigned buttons, unsigned modifiers, int click_count)
{
return top_level_traversable()->event_handler().handle_mousedown(device_to_css_point(position), device_to_css_point(screen_position), button, buttons, modifiers, click_count);
}
EventResult Page::handle_mousemove(DevicePixelPoint position, DevicePixelPoint screen_position, unsigned buttons, unsigned modifiers)
{
return top_level_traversable()->event_handler().handle_mousemove(device_to_css_point(position), device_to_css_point(screen_position), buttons, modifiers);
}
EventResult Page::handle_mouseleave()
{
return top_level_traversable()->event_handler().handle_mouseleave();
}
EventResult Page::handle_mousewheel(DevicePixelPoint position, DevicePixelPoint screen_position, unsigned button, unsigned buttons, unsigned modifiers, DevicePixels wheel_delta_x, DevicePixels wheel_delta_y)
{
return top_level_traversable()->event_handler().handle_mousewheel(device_to_css_point(position), device_to_css_point(screen_position), button, buttons, modifiers, wheel_delta_x.value(), wheel_delta_y.value());
}
EventResult Page::handle_drag_and_drop_event(DragEvent::Type type, DevicePixelPoint position, DevicePixelPoint screen_position, unsigned button, unsigned buttons, unsigned modifiers, Vector<HTML::SelectedFile> files)
{
return top_level_traversable()->event_handler().handle_drag_and_drop_event(type, device_to_css_point(position), device_to_css_point(screen_position), button, buttons, modifiers, move(files));
}
EventResult Page::handle_pinch_event(DevicePixelPoint position, double scale)
{
return top_level_traversable()->event_handler().handle_pinch_event(device_to_css_point(position), scale);
}
EventResult Page::handle_keydown(UIEvents::KeyCode key, unsigned modifiers, u32 code_point, bool repeat)
{
return focused_navigable().event_handler().handle_keydown(key, modifiers, code_point, repeat);
}
EventResult Page::handle_keyup(UIEvents::KeyCode key, unsigned modifiers, u32 code_point, bool repeat)
{
return focused_navigable().event_handler().handle_keyup(key, modifiers, code_point, repeat);
}
void Page::handle_sdl_input_events()
{
top_level_traversable()->event_handler().handle_sdl_input_events();
}
void Page::set_top_level_traversable(GC::Ref<HTML::TraversableNavigable> navigable)
{
VERIFY(!m_top_level_traversable); // Replacement is not allowed!
VERIFY(&navigable->page() == this);
m_top_level_traversable = navigable;
}
bool Page::top_level_traversable_is_initialized() const
{
return m_top_level_traversable;
}
HTML::BrowsingContext& Page::top_level_browsing_context()
{
return *m_top_level_traversable->active_browsing_context();
}
HTML::BrowsingContext const& Page::top_level_browsing_context() const
{
return *m_top_level_traversable->active_browsing_context();
}
GC::Ref<HTML::TraversableNavigable> Page::top_level_traversable() const
{
return *m_top_level_traversable;
}
void Page::did_update_window_rect()
{
if (m_window_rect_observer)
m_window_rect_observer->function()({ window_position(), window_size() });
}
template<typename ResponseType>
static ResponseType spin_event_loop_until_dialog_closed(PageClient& client, Optional<ResponseType>& response, SourceLocation location = SourceLocation::current())
{
auto& event_loop = Web::HTML::current_principal_settings_object().responsible_event_loop();
auto pause_handle = event_loop.pause();
Web::Platform::EventLoopPlugin::the().spin_until(GC::create_function(event_loop.heap(), [&]() {
return response.has_value() || !client.is_connection_open();
}));
if (!client.is_connection_open()) {
dbgln("WebContent client disconnected during {}. Exiting peacefully.", location.function_name());
exit(0);
}
return response.release_value();
}
void Page::did_request_alert(String const& message)
{
m_pending_dialog = PendingDialog::Alert;
m_client->page_did_request_alert(message);
if (!message.is_empty())
m_pending_dialog_text = message;
spin_event_loop_until_dialog_closed(*m_client, m_pending_alert_response);
}
void Page::alert_closed()
{
if (m_pending_dialog == PendingDialog::Alert) {
m_pending_alert_response = Empty {};
on_pending_dialog_closed();
}
}
bool Page::did_request_confirm(String const& message)
{
m_pending_dialog = PendingDialog::Confirm;
m_client->page_did_request_confirm(message);
if (!message.is_empty())
m_pending_dialog_text = message;
return spin_event_loop_until_dialog_closed(*m_client, m_pending_confirm_response);
}
void Page::confirm_closed(bool accepted)
{
if (m_pending_dialog == PendingDialog::Confirm) {
m_pending_confirm_response = accepted;
on_pending_dialog_closed();
}
}
Optional<String> Page::did_request_prompt(String const& message, String const& default_)
{
m_pending_dialog = PendingDialog::Prompt;
m_client->page_did_request_prompt(message, default_);
if (!message.is_empty())
m_pending_dialog_text = message;
return spin_event_loop_until_dialog_closed(*m_client, m_pending_prompt_response);
}
void Page::prompt_closed(Optional<String> response)
{
if (m_pending_dialog == PendingDialog::Prompt) {
m_pending_prompt_response = move(response);
on_pending_dialog_closed();
}
}
void Page::dismiss_dialog(GC::Ref<GC::Function<void()>> on_dialog_closed)
{
m_on_pending_dialog_closed = on_dialog_closed;
switch (m_pending_dialog) {
case PendingDialog::None:
break;
case PendingDialog::Alert:
m_client->page_did_request_accept_dialog();
break;
case PendingDialog::Confirm:
case PendingDialog::Prompt:
m_client->page_did_request_dismiss_dialog();
break;
}
}
void Page::accept_dialog(GC::Ref<GC::Function<void()>> on_dialog_closed)
{
m_on_pending_dialog_closed = on_dialog_closed;
switch (m_pending_dialog) {
case PendingDialog::None:
break;
case PendingDialog::Alert:
case PendingDialog::Confirm:
case PendingDialog::Prompt:
m_client->page_did_request_accept_dialog();
break;
}
}
void Page::on_pending_dialog_closed()
{
m_pending_dialog = PendingDialog::None;
m_pending_dialog_text.clear();
if (m_on_pending_dialog_closed) {
m_on_pending_dialog_closed->function()();
m_on_pending_dialog_closed = nullptr;
}
}
void Page::did_request_color_picker(GC::Weak<HTML::HTMLInputElement> target, Color current_color)
{
if (m_pending_non_blocking_dialog == PendingNonBlockingDialog::None) {
m_pending_non_blocking_dialog = PendingNonBlockingDialog::ColorPicker;
m_pending_non_blocking_dialog_target = move(target);
m_client->page_did_request_color_picker(current_color);
}
}
void Page::color_picker_update(Optional<Color> picked_color, HTML::ColorPickerUpdateState state)
{
if (m_pending_non_blocking_dialog == PendingNonBlockingDialog::ColorPicker) {
if (state == HTML::ColorPickerUpdateState::Closed)
m_pending_non_blocking_dialog = PendingNonBlockingDialog::None;
if (m_pending_non_blocking_dialog_target) {
auto& input_element = as<HTML::HTMLInputElement>(*m_pending_non_blocking_dialog_target);
input_element.did_pick_color(move(picked_color), state);
if (state == HTML::ColorPickerUpdateState::Closed)
m_pending_non_blocking_dialog_target = nullptr;
}
}
}
void Page::did_request_file_picker(GC::Weak<HTML::HTMLInputElement> target, HTML::FileFilter const& accepted_file_types, HTML::AllowMultipleFiles allow_multiple_files)
{
if (m_pending_non_blocking_dialog == PendingNonBlockingDialog::None) {
m_pending_non_blocking_dialog = PendingNonBlockingDialog::FilePicker;
m_pending_non_blocking_dialog_target = move(target);
m_client->page_did_request_file_picker(accepted_file_types, allow_multiple_files);
}
}
void Page::file_picker_closed(Span<HTML::SelectedFile> selected_files)
{
if (m_pending_non_blocking_dialog == PendingNonBlockingDialog::FilePicker) {
m_pending_non_blocking_dialog = PendingNonBlockingDialog::None;
if (m_pending_non_blocking_dialog_target) {
auto& input_element = as<HTML::HTMLInputElement>(*m_pending_non_blocking_dialog_target);
input_element.did_select_files(selected_files);
m_pending_non_blocking_dialog_target = nullptr;
}
}
}
void Page::did_request_select_dropdown(GC::Weak<HTML::HTMLSelectElement> target, Web::CSSPixelPoint content_position, Web::CSSPixels minimum_width, Vector<Web::HTML::SelectItem> items)
{
if (m_pending_non_blocking_dialog == PendingNonBlockingDialog::None) {
m_pending_non_blocking_dialog = PendingNonBlockingDialog::Select;
m_pending_non_blocking_dialog_target = move(target);
m_client->page_did_request_select_dropdown(content_position, minimum_width, move(items));
}
}
void Page::select_dropdown_closed(Optional<u32> const& selected_item_id)
{
if (m_pending_non_blocking_dialog == PendingNonBlockingDialog::Select) {
m_pending_non_blocking_dialog = PendingNonBlockingDialog::None;
if (m_pending_non_blocking_dialog_target) {
auto& select_element = as<HTML::HTMLSelectElement>(*m_pending_non_blocking_dialog_target);
select_element.did_select_item(selected_item_id);
m_pending_non_blocking_dialog_target = nullptr;
}
}
}
void Page::request_clipboard_entries(ClipboardRequest request)
{
auto request_id = m_next_clipboard_request_id++;
m_pending_clipboard_requests.set(request_id, request);
client().page_did_request_clipboard_entries(request_id);
}
void Page::retrieved_clipboard_entries(u64 request_id, Vector<Clipboard::SystemClipboardItem> items)
{
if (auto request = m_pending_clipboard_requests.take(request_id); request.has_value())
(*request)->function()(move(items));
}
void Page::register_media_element(Badge<HTML::HTMLMediaElement>, UniqueNodeID media_id)
{
m_media_elements.append(media_id);
}
void Page::unregister_media_element(Badge<HTML::HTMLMediaElement>, UniqueNodeID media_id)
{
m_media_elements.remove_all_matching([&](auto candidate_id) {
return candidate_id == media_id;
});
}
template<typename Callback>
void Page::for_each_media_element(Callback&& callback)
{
for (auto media_id : m_media_elements) {
if (auto* node = DOM::Node::from_unique_id(media_id))
callback(as<HTML::HTMLMediaElement>(*node));
}
}
void Page::update_all_media_element_video_sinks()
{
for_each_media_element([](auto& media_element) {
media_element.update_video_frame_and_timeline();
});
}
void Page::register_canvas_element(Badge<HTML::HTMLCanvasElement>, UniqueNodeID canvas_id)
{
m_canvas_elements.append(canvas_id);
}
void Page::unregister_canvas_element(Badge<HTML::HTMLCanvasElement>, UniqueNodeID canvas_id)
{
m_canvas_elements.remove_all_matching([&](auto candidate_id) {
return candidate_id == canvas_id;
});
}
template<typename Callback>
void Page::for_each_canvas_element(Callback&& callback)
{
for (auto canvas_id : m_canvas_elements) {
if (auto* node = DOM::Node::from_unique_id(canvas_id))
callback(as<HTML::HTMLCanvasElement>(*node));
}
}
void Page::present_all_canvas_element_surfaces()
{
for_each_canvas_element([](auto& canvas_element) {
canvas_element.present();
});
}
void Page::did_request_media_context_menu(UniqueNodeID media_id, CSSPixelPoint position, ByteString const& target, unsigned modifiers, MediaContextMenu const& menu)
{
m_media_context_menu_element_id = media_id;
client().page_did_request_media_context_menu(position, target, modifiers, menu);
}
void Page::toggle_media_play_state()
{
auto media_element = media_context_menu_element();
if (!media_element)
return;
// AD-HOC: An execution context is required for Promise creation hooks.
HTML::TemporaryExecutionContext execution_context { media_element->realm() };
if (media_element->potentially_playing())
media_element->pause();
else
media_element->play();
}
void Page::toggle_media_mute_state()
{
auto media_element = media_context_menu_element();
if (!media_element)
return;
// AD-HOC: An execution context is required for Promise creation hooks.
HTML::TemporaryExecutionContext execution_context { media_element->realm() };
media_element->set_muted(!media_element->muted());
}
void Page::toggle_media_loop_state()
{
auto media_element = media_context_menu_element();
if (!media_element)
return;
// AD-HOC: An execution context is required for Promise creation hooks.
HTML::TemporaryExecutionContext execution_context { media_element->realm() };
if (media_element->has_attribute(HTML::AttributeNames::loop))
media_element->remove_attribute(HTML::AttributeNames::loop);
else
media_element->set_attribute_value(HTML::AttributeNames::loop, String {});
}
void Page::toggle_media_fullscreen_state()
{
auto media_element = media_context_menu_element();
if (!media_element)
return;
HTML::TemporaryExecutionContext execution_context { media_element->realm() };
media_element->toggle_fullscreen();
}
void Page::toggle_media_controls_state()
{
auto media_element = media_context_menu_element();
if (!media_element)
return;
HTML::TemporaryExecutionContext execution_context { media_element->realm() };
if (media_element->has_attribute(HTML::AttributeNames::controls))
media_element->remove_attribute(HTML::AttributeNames::controls);
else
media_element->set_attribute_value(HTML::AttributeNames::controls, String {});
}
void Page::toggle_page_mute_state()
{
m_mute_state = HTML::invert_mute_state(m_mute_state);
for_each_media_element([&](auto& media_element) {
media_element.page_mute_state_changed({});
});
}
GC::Ptr<HTML::HTMLMediaElement> Page::media_context_menu_element()
{
if (!m_media_context_menu_element_id.has_value())
return nullptr;
auto* dom_node = DOM::Node::from_unique_id(*m_media_context_menu_element_id);
if (dom_node == nullptr)
return nullptr;
if (!is<HTML::HTMLMediaElement>(dom_node))
return nullptr;
return static_cast<HTML::HTMLMediaElement*>(dom_node);
}
void Page::set_user_style(String source)
{
m_user_style_sheet_source = source;
if (top_level_traversable_is_initialized() && top_level_traversable()->active_document()) {
auto& document = *top_level_traversable()->active_document();
document.style_scope().invalidate_rule_cache();
document.for_each_shadow_root([](auto& shadow_root) {
shadow_root.style_scope().invalidate_rule_cache();
});
}
}
Vector<GC::Root<DOM::Document>> Page::documents_in_active_window() const
{
if (!top_level_traversable_is_initialized())
return {};
auto documents = HTML::main_thread_event_loop().documents_in_this_event_loop_matching([&](auto& document) {
return document.window() == top_level_traversable()->active_window();
});
return documents;
}
void Page::clear_selection()
{
for (auto const& document : documents_in_active_window()) {
auto selection = document->get_selection();
if (!selection)
continue;
selection->remove_all_ranges();
}
}
Page::FindInPageResult Page::perform_find_in_page_query(FindInPageQuery const& query, Optional<SearchDirection> direction)
{
VERIFY(top_level_traversable_is_initialized());
Vector<GC::Root<DOM::Range>> all_matches;
auto active_range = [](auto& document) -> GC::Ptr<DOM::Range> {
auto selection = document.get_selection();
if (!selection || selection->is_collapsed())
return {};
return selection->range();
};
auto find_current_match_index = [this](DOM::Range& range, auto const& matches) -> Optional<size_t> {
// Always return the first match if there is no active query.
if (!m_last_find_in_page_query.has_value())
return 0;
for (size_t i = 0; i < matches.size(); ++i) {
auto boundary_comparison_or_error = matches[i]->compare_boundary_points(DOM::Range::HowToCompareBoundaryPoints::START_TO_START, range);
if (!boundary_comparison_or_error.is_error() && boundary_comparison_or_error.value() >= 0)
return i;
}
return {};
};
auto should_update_match_index = false;
for (auto const& document : documents_in_active_window()) {
auto matches = document->find_matching_text(query.string, query.case_sensitivity);
if (document == top_level_traversable()->active_document()) {
if (auto range = active_range(*document)) {
auto new_match_index = find_current_match_index(*range, matches);
should_update_match_index = true;
m_find_in_page_match_index = new_match_index.value_or(0) + all_matches.size();
} else {
m_find_in_page_match_index = all_matches.size();
}
}
all_matches.extend(move(matches));
}
if (auto active_document = top_level_traversable()->active_document()) {
if (m_last_find_in_page_url.serialize(URL::ExcludeFragment::Yes) != active_document->url().serialize(URL::ExcludeFragment::Yes)) {
m_last_find_in_page_url = top_level_traversable()->active_document()->url();
m_find_in_page_match_index = 0;
}
}
if (direction.has_value() && should_update_match_index) {
if (direction == SearchDirection::Forward) {
if (m_find_in_page_match_index >= all_matches.size() - 1) {
if (query.wrap_around == WrapAround::No)
return {};
m_find_in_page_match_index = 0;
} else {
m_find_in_page_match_index++;
}
} else {
if (m_find_in_page_match_index == 0) {
if (query.wrap_around == WrapAround::No)
return {};
m_find_in_page_match_index = all_matches.size() - 1;
} else {
m_find_in_page_match_index--;
}
}
}
update_find_in_page_selection(all_matches);
return Page::FindInPageResult {
.current_match_index = m_find_in_page_match_index,
.total_match_count = all_matches.size(),
};
}
Page::FindInPageResult Page::find_in_page(FindInPageQuery const& query)
{
if (!top_level_traversable_is_initialized())
return {};
if (query.string.is_empty()) {
m_last_find_in_page_query = {};
clear_selection();
return {};
}
auto result = perform_find_in_page_query(query);
m_last_find_in_page_query = query;
m_last_find_in_page_url = top_level_traversable()->active_document()->url();
return result;
}
Page::FindInPageResult Page::find_in_page_next_match()
{
if (!(m_last_find_in_page_query.has_value() && top_level_traversable_is_initialized()))
return {};
auto result = perform_find_in_page_query(*m_last_find_in_page_query, SearchDirection::Forward);
return result;
}
Page::FindInPageResult Page::find_in_page_previous_match()
{
if (!(m_last_find_in_page_query.has_value() && top_level_traversable_is_initialized()))
return {};
auto result = perform_find_in_page_query(*m_last_find_in_page_query, SearchDirection::Backward);
return result;
}
void Page::update_find_in_page_selection(Vector<GC::Root<DOM::Range>> matches)
{
if (matches.is_empty())
return;
clear_selection();
auto current_range = matches[m_find_in_page_match_index];
auto common_ancestor_container = current_range->common_ancestor_container();
auto& document = common_ancestor_container->document();
if (!document.window())
return;
auto selection = document.get_selection();
if (!selection)
return;
selection->add_range(*current_range);
if (auto element = common_ancestor_container->parent_element()) {
DOM::ScrollIntoViewOptions scroll_options;
scroll_options.block = Bindings::ScrollLogicalPosition::Nearest;
scroll_options.inline_ = Bindings::ScrollLogicalPosition::Nearest;
scroll_options.behavior = Bindings::ScrollBehavior::Instant;
(void)element->scroll_into_view(scroll_options);
}
}
void Page::enqueue_fullscreen_enter(GC::Ref<DOM::Element> element, GC::Ref<DOM::Document> pending_doc, DOM::RequestFullscreenError error, GC::Ref<WebIDL::Promise> promise)
{
m_pending_fullscreen_operations.enqueue(PendingFullscreenEnter { element, pending_doc, error, promise });
// NOTE: Processing is deferred because the spec says "run the remaining steps in parallel",
// meaning the caller's synchronous JS should complete before we process the operation.
Platform::EventLoopPlugin::the().deferred_invoke(GC::create_function(heap(), [this]() {
process_pending_fullscreen_operations();
}));
}
void Page::enqueue_fullscreen_exit(GC::Ref<DOM::Document> doc, bool resize, GC::Ref<WebIDL::Promise> promise)
{
m_pending_fullscreen_operations.enqueue(PendingFullscreenExit { doc, resize, promise });
// NOTE: Processing is deferred because the spec says "run the remaining steps in parallel",
// meaning the caller's synchronous JS should complete before we process the operation.
Platform::EventLoopPlugin::the().deferred_invoke(GC::create_function(heap(), [this]() {
process_pending_fullscreen_operations();
}));
}
void Page::process_pending_fullscreen_operations()
{
// FIXME: The Fullscreen API interacts with the top-level traversable's viewport. With site-isolation,
// an iframe's content process won't have direct access to this Page, so fullscreen operations
// will need to be routed through IPC to the top-level process.
// NOTE: Resolving/rejecting promises during processing may trigger JS microtasks that re-enter
// this function (e.g., JS calls exitFullscreen() after a requestFullscreen() promise resolves).
// The outer call's while loop will pick up newly enqueued items.
if (m_processing_fullscreen_operations)
return;
m_processing_fullscreen_operations = true;
ScopeGuard guard = [this] { m_processing_fullscreen_operations = false; };
while (!m_pending_fullscreen_operations.is_empty()) {
auto& front = m_pending_fullscreen_operations.head();
auto processed = front.visit(
[&](PendingFullscreenEnter& enter) -> bool {
// https://fullscreen.spec.whatwg.org/#dom-element-requestfullscreen
// 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"]:
if (enter.error == DOM::RequestFullscreenError::False) {
if (m_viewport_is_fullscreen == ViewportIsFullscreen::No) {
if (!m_fullscreen_ipc_sent_to_ui) {
m_client->page_did_request_fullscreen_window();
m_fullscreen_ipc_sent_to_ui = true;
}
// NB: Stop processing here and wait for a change in the fullscreen state if we aren't
// in the desired state yet.
return false;
}
// 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 (enter.element->owner_document() != enter.pending_doc.ptr())
enter.error = DOM::RequestFullscreenError::ElementNodeDocIsNotPendingDoc;
else if (!enter.element->is_element_ready_for_fullscreen())
enter.error = DOM::RequestFullscreenError::ElementReadyCheckFailed;
}
auto& realm = enter.element->realm();
HTML::TemporaryExecutionContext context(realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes);
// 10. If error is true:
if (enter.error != DOM::RequestFullscreenError::False) {
// 1. Append (fullscreenerror, this) to pendingDoc's list of pending fullscreen events.
enter.pending_doc->append_pending_fullscreen_change(DOM::PendingFullscreenEvent::Type::Error, enter.element);
// 2. Reject promise with a TypeError exception and terminate these steps.
WebIDL::reject_promise(realm, enter.promise, JS::TypeError::create(realm, DOM::request_fullscreen_error_to_string(enter.error)));
return true;
}
// 11. Let fullscreenElements be an ordered set initially consisting of this.
auto fullscreen_elements = realm.heap().allocate<GC::HeapVector<GC::Ref<DOM::Element>>>();
fullscreen_elements->elements().append(enter.element);
// 12. While true:
while (true) {
// 1. Let last be the last item of fullscreenElements.
auto last = fullscreen_elements->elements().last();
// 2. Let container be last's node navigable's container.
auto container = last->navigable()->container();
// 3. If container is null, then break.
if (!container)
break;
// 4. Append container to fullscreenElements.
fullscreen_elements->elements().append(*container);
}
// 13. For each element in fullscreenElements:
for (auto& element : fullscreen_elements->elements()) {
// 1. Let doc be element's node document.
auto& doc = element->document();
// 2. If element is doc's fullscreen element, continue.
if (doc.fullscreen_element() == element)
continue;
// 3. If element is this and this is an iframe element, then set element's iframe fullscreen flag.
if (element == enter.element && is<HTML::HTMLIFrameElement>(*enter.element))
as<HTML::HTMLIFrameElement>(*element).set_iframe_fullscreen_flag(true);
// 4. Fullscreen element within doc.
doc.fullscreen_element_within_doc(element);
// 5. Append (fullscreenchange, element) to doc's list of pending fullscreen events.
doc.append_pending_fullscreen_change(DOM::PendingFullscreenEvent::Type::Change, element);
}
// 14. Resolve promise with undefined
WebIDL::resolve_promise(realm, enter.promise, JS::js_undefined());
return true;
},
[&](PendingFullscreenExit& exit) -> bool {
auto& realm = exit.doc->realm();
HTML::TemporaryExecutionContext context(realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes);
// https://fullscreen.spec.whatwg.org/#exit-fullscreen
// 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.
if (exit.resize && m_viewport_is_fullscreen == ViewportIsFullscreen::Yes) {
if (!m_fullscreen_ipc_sent_to_ui) {
m_client->page_did_request_exit_fullscreen();
m_fullscreen_ipc_sent_to_ui = true;
}
// NB: Stop processing here and wait for a change in the fullscreen state if we aren't
// in the desired state yet.
return false;
}
// 11. If doc's fullscreen element is null, then resolve promise with undefined and terminate these
// steps.
if (!exit.doc->fullscreen_element()) {
WebIDL::resolve_promise(realm, exit.promise, JS::js_undefined());
return true;
}
// 12. Let exitDocs be the result of collecting documents to unfullscreen given doc.
auto exit_docs = exit.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.
auto descendant_docs = realm.heap().allocate<GC::HeapVector<GC::Ref<DOM::Document>>>();
for (auto& descendant : exit.doc->descendant_navigables()) {
if (descendant->active_document()->fullscreen_element())
descendant_docs->elements().append(*descendant->active_document());
}
// 14. For each exitDoc in exitDocs:
for (auto& exit_doc : exit_docs->elements()) {
// 1. Append (fullscreenchange, exitDoc's fullscreen element) to exitDoc's list of pending
// fullscreen events.
exit_doc->append_pending_fullscreen_change(DOM::PendingFullscreenEvent::Type::Change, *exit_doc->fullscreen_element());
// 2. If resize is true, unfullscreen exitDoc.
if (exit.resize)
exit_doc->unfullscreen();
// 3. Otherwise, unfullscreen exitDoc's fullscreen element.
else
exit_doc->unfullscreen_element(*exit_doc->fullscreen_element());
}
// 15. For each descendantDoc in descendantDocs:
for (auto& descendant_doc : descendant_docs->elements()) {
// 1. Append (fullscreenchange, descendantDoc's fullscreen element) to descendantDoc's list of
// pending fullscreen events.
descendant_doc->append_pending_fullscreen_change(DOM::PendingFullscreenEvent::Type::Change, *descendant_doc->fullscreen_element());
// 2. Unfullscreen descendantDoc.
descendant_doc->unfullscreen();
}
// 16. Resolve promise with undefined.
WebIDL::resolve_promise(realm, exit.promise, JS::js_undefined());
return true;
});
if (!processed)
break;
m_pending_fullscreen_operations.dequeue();
}
}
void Page::set_viewport_is_fullscreen(ViewportIsFullscreen is_fullscreen)
{
if (m_viewport_is_fullscreen == is_fullscreen)
return;
m_viewport_is_fullscreen = is_fullscreen;
m_fullscreen_ipc_sent_to_ui = false;
process_pending_fullscreen_operations();
}
}
template<>
ErrorOr<void> IPC::encode(Encoder& encoder, Web::Page::MediaContextMenu const& menu)
{
TRY(encoder.encode(menu.media_url));
TRY(encoder.encode(menu.is_video));
TRY(encoder.encode(menu.is_playing));
TRY(encoder.encode(menu.is_muted));
TRY(encoder.encode(menu.has_user_agent_controls));
TRY(encoder.encode(menu.is_looping));
TRY(encoder.encode(menu.is_fullscreen));
return {};
}
template<>
ErrorOr<Web::Page::MediaContextMenu> IPC::decode(Decoder& decoder)
{
return Web::Page::MediaContextMenu {
.media_url = TRY(decoder.decode<URL::URL>()),
.is_video = TRY(decoder.decode<bool>()),
.is_playing = TRY(decoder.decode<bool>()),
.is_muted = TRY(decoder.decode<bool>()),
.has_user_agent_controls = TRY(decoder.decode<bool>()),
.is_looping = TRY(decoder.decode<bool>()),
.is_fullscreen = TRY(decoder.decode<bool>()),
};
}