ladybird/Libraries/LibWebView/ViewImplementation.cpp

2847 lines
125 KiB
C++
Raw Normal View History

/*
* Copyright (c) 2023, Linus Groh <linusg@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/Debug.h>
#include <AK/Error.h>
#include <AK/NeverDestroyed.h>
#include <AK/NumericLimits.h>
#include <AK/ScopeGuard.h>
#include <AK/String.h>
#include <AK/Time.h>
#include <LibCore/EventLoop.h>
#include <LibCore/StandardPaths.h>
#include <LibCore/Timer.h>
#include <LibGfx/ImageFormats/PNGWriter.h>
#include <LibGfx/SharedImageBuffer.h>
#include <LibURL/Parser.h>
#include <LibWeb/CSS/SystemColor.h>
#include <LibWeb/Crypto/Crypto.h>
#include <LibWeb/Infra/Strings.h>
#include <LibWeb/WebDriver/Error.h>
#include <LibWebView/Application.h>
#include <LibWebView/BookmarkStore.h>
#include <LibWebView/ErrorHTML.h>
#include <LibWebView/HelperProcess.h>
#include <LibWebView/HistoryDebug.h>
#include <LibWebView/HistoryStore.h>
#include <LibWebView/Menu.h>
#include <LibWebView/SiteIsolation.h>
#include <LibWebView/URL.h>
#include <LibWebView/UserAgent.h>
#include <LibWebView/ViewImplementation.h>
namespace WebView {
static HashMap<u64, ViewImplementation*>& all_views()
{
static NeverDestroyed<HashMap<u64, ViewImplementation*>> views;
return *views;
}
static u64 s_view_count = 1; // This has to start at 1 for Firefox DevTools.
void ViewImplementation::for_each_view(Function<IterationDecision(ViewImplementation&)> callback)
{
for (auto& view : all_views()) {
if (callback(*view.value) == IterationDecision::Break)
break;
}
}
Optional<ViewImplementation&> ViewImplementation::find_view_by_id(u64 id)
{
if (auto view = all_views().get(id); view.has_value())
return *view.value();
return {};
}
ViewImplementation::ViewImplementation()
: m_document_cookie_version_buffer(Core::create_shared_version_buffer())
, m_view_id(s_view_count++)
{
all_views().set(m_view_id, this);
initialize_context_menus();
m_repeated_crash_timer = Core::Timer::create_single_shot(1000, [this] {
// Reset the "crashing a lot" counter after 1 second in case we just
// happen to be visiting crashy websites a lot.
this->m_crash_count = 0;
});
on_request_file = [this](auto const& path, auto request_id) {
auto file = Core::File::open(path, Core::File::OpenMode::Read);
if (file.is_error())
client().async_handle_file_return(page_id(), file.error().code(), {}, request_id);
else
client().async_handle_file_return(page_id(), 0, IPC::File::adopt_file(file.release_value()), request_id);
};
}
ViewImplementation::~ViewImplementation()
{
all_views().remove(m_view_id);
if (m_client_state.client)
m_client_state.client->unregister_view(m_client_state.page_index);
}
WebContentClient& ViewImplementation::client()
{
VERIFY(m_client_state.client);
return *m_client_state.client;
}
WebContentClient const& ViewImplementation::client() const
{
VERIFY(m_client_state.client);
return *m_client_state.client;
}
u64 ViewImplementation::page_id() const
{
VERIFY(m_client_state.client);
return m_client_state.page_index;
}
void ViewImplementation::set_url(URL::URL url)
{
if (m_url == url)
return;
auto previous_host = current_host();
m_url = move(url);
update_bookmark_action();
if (current_host() != previous_host)
apply_zoom_for_current_host();
}
void ViewImplementation::set_favicon(Badge<WebContentClient>, Gfx::Bitmap const& favicon)
{
m_favicon_base64_png.clear();
if (auto favicon_png = Gfx::PNGWriter::encode(favicon); !favicon_png.is_error()) {
if (auto favicon_base64_png = encode_base64(favicon_png.value().bytes()); !favicon_base64_png.is_error())
m_favicon_base64_png = favicon_base64_png.release_value();
}
if (m_favicon_base64_png.has_value()) {
Application::bookmark_store().update_favicon(m_url, *m_favicon_base64_png);
if (!m_should_suppress_history_for_current_load)
Application::history_store().update_favicon(m_url, *m_favicon_base64_png);
}
if (on_favicon_change)
on_favicon_change(favicon);
}
void ViewImplementation::create_new_process_for_cross_site_navigation(URL::URL const& url, Variant<Empty, String, Web::HTML::POSTResource> document_resource, Web::Bindings::NavigationHistoryBehavior history_handling)
{
dump_session_history("before-process-swap"sv);
m_webdriver_pending_navigation_url = url;
if (m_client_state.has_usable_bitmap) {
// Keep showing the old page until the new WebContent process paints its first frame.
m_backup_shared_image_buffer = move(m_client_state.front_bitmap.shared_image_buffer);
m_backup_bitmap_size = m_client_state.front_bitmap.last_painted_size;
}
if (m_client_state.client) {
m_client_state.client->unregister_view(m_client_state.page_index);
}
initialize_client();
VERIFY(m_client_state.client);
if (on_web_content_process_change_for_cross_site_navigation)
on_web_content_process_change_for_cross_site_navigation();
handle_resize();
auto ui_session_history_already_points_to_url = false;
if (should_manage_session_history_in_ui_process()) {
if (auto const* current_entry = m_session_history.current_entry(); current_entry && current_entry->url == url)
ui_session_history_already_points_to_url = true;
if (m_pending_session_history_traversal.has_value() && m_pending_session_history_traversal->will_replace_web_content_process)
m_pending_session_history_traversal->stage = PendingSessionHistoryTraversal::Stage::ReplacingWebContentProcess;
if (m_pending_session_history_navigation.has_value())
m_pending_session_history_navigation->web_content_restore_mode = PendingSessionHistoryNavigation::WebContentRestoreMode::RestoreFromUIProcess;
m_current_web_content_session_history_matches_mirror = false;
m_session_history.forget_web_content_state();
m_pending_web_content_session_history_seed.waiting_for_ack = false;
m_pending_web_content_session_history_seed.should_send_entries = true;
m_pending_web_content_session_history_seed.ignore_updates_until_seed = true;
}
auto seed_replacement_process_before_load_if_possible = [&] {
if (!should_manage_session_history_in_ui_process())
return false;
if (!m_pending_web_content_session_history_seed.should_send_entries)
return false;
if (m_session_history_entry_url_loading_from_ui_process.has_value())
return false;
if (m_session_history.current_step_to_restore_after_loading_top_level_entry().has_value())
return false;
seed_web_content_session_history_from_ui_process();
return true;
};
if (ui_session_history_already_points_to_url) {
m_should_suppress_history_for_current_load = false;
m_should_suppress_history_for_next_load = false;
if (!m_session_history_entry_url_loading_from_ui_process.has_value())
m_pending_web_content_session_history_seed.step_after_loading_top_level_entry = m_session_history.current_step_to_restore_after_loading_top_level_entry();
set_url(url);
auto seeded_replacement_process_before_load = seed_replacement_process_before_load_if_possible();
auto web_content_history_handling = seeded_replacement_process_before_load ? Web::Bindings::NavigationHistoryBehavior::Replace : history_handling;
dump_session_history("process-swap-load-existing-ui-entry"sv);
client().async_load_url_with_document_resource(page_id(), url, document_resource, web_content_history_handling);
} else {
m_should_suppress_history_for_current_load = false;
m_should_suppress_history_for_next_load = false;
if (should_manage_session_history_in_ui_process() && !m_session_history_entry_url_loading_from_ui_process.has_value()) {
if (m_session_history.current_entry()) {
m_pending_session_history_navigation = PendingSessionHistoryNavigation {
url,
m_session_history,
PendingSessionHistoryNavigation::WebContentRestoreMode::RestoreFromUIProcess,
};
} else {
m_pending_session_history_navigation.clear();
}
if (history_handling == Web::Bindings::NavigationHistoryBehavior::Replace)
m_session_history.replace_current_entry(url, document_resource);
else
m_session_history.navigate(url, document_resource);
m_current_web_content_session_history_matches_mirror = false;
update_navigation_action_state();
}
if (!m_session_history_entry_url_loading_from_ui_process.has_value())
m_pending_web_content_session_history_seed.step_after_loading_top_level_entry = m_session_history.current_step_to_restore_after_loading_top_level_entry();
set_url(url);
auto seeded_replacement_process_before_load = seed_replacement_process_before_load_if_possible();
auto web_content_history_handling = seeded_replacement_process_before_load ? Web::Bindings::NavigationHistoryBehavior::Replace : history_handling;
dump_session_history("load"sv);
client().async_load_url_with_document_resource(page_id(), url, document_resource, web_content_history_handling);
}
dump_session_history("after-process-swap-load"sv);
}
void ViewImplementation::server_did_paint(Badge<WebContentClient>, i32 bitmap_id, Gfx::IntSize size)
{
bool did_swap_bitmap = false;
if (m_client_state.back_bitmap.id == bitmap_id) {
m_client_state.has_usable_bitmap = true;
m_client_state.back_bitmap.last_painted_size = size.to_type<Web::DevicePixels>();
swap(m_client_state.back_bitmap, m_client_state.front_bitmap);
m_backup_shared_image_buffer = nullptr;
did_swap_bitmap = true;
}
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] UI received presented bitmap {} for page {} size={}x{} did_swap={} front={} back={}",
bitmap_id, page_id(), size.width(), size.height(), did_swap_bitmap, m_client_state.front_bitmap.id, m_client_state.back_bitmap.id);
client().notify_presented_bitmap_ready_to_paint(page_id(), bitmap_id);
if (did_swap_bitmap && on_ready_to_paint)
on_ready_to_paint();
}
void ViewImplementation::set_window_position(Gfx::IntPoint position)
{
client().async_set_window_position(m_client_state.page_index, position.to_type<Web::DevicePixels>());
}
void ViewImplementation::set_window_size(Gfx::IntSize size)
{
client().async_set_window_size(m_client_state.page_index, size.to_type<Web::DevicePixels>());
}
void ViewImplementation::did_update_window_rect()
{
client().async_did_update_window_rect(m_client_state.page_index);
}
void ViewImplementation::set_system_visibility_state(Web::HTML::VisibilityState visibility_state)
{
if (m_system_visibility_state == visibility_state)
return;
m_system_visibility_state = visibility_state;
client().async_set_system_visibility_state(m_client_state.page_index, m_system_visibility_state);
}
void ViewImplementation::load(URL::URL const& url, Web::Bindings::NavigationHistoryBehavior history_handling)
{
m_is_showing_crash_page = false;
m_should_suppress_history_for_current_load = false;
m_should_suppress_history_for_next_load = false;
auto should_defer_ui_process_history_update = false;
if (!m_session_history_entry_url_loading_from_ui_process.has_value())
abandon_pending_web_content_session_history_seed();
if (should_manage_session_history_in_ui_process() && !m_session_history_entry_url_loading_from_ui_process.has_value()) {
m_pending_session_history_traversal.clear();
auto const* current_entry = m_session_history.current_entry();
auto is_javascript_navigation = url.scheme() == "javascript"sv;
should_defer_ui_process_history_update = is_javascript_navigation;
if (current_entry && !is_javascript_navigation)
m_pending_session_history_navigation = PendingSessionHistoryNavigation { url, m_session_history };
else
m_pending_session_history_navigation.clear();
if (!is_javascript_navigation) {
auto ui_process_history_handling = history_handling;
if (ui_process_history_handling == Web::Bindings::NavigationHistoryBehavior::Auto) {
// https://html.spec.whatwg.org/multipage/browsing-the-web.html#navigate
// If url equals navigable's active document's URL, and
// initiatorOriginSnapshot is same origin with targetNavigable's
// active document's origin, then set historyHandling to "replace".
if (current_entry && current_entry->url == url)
ui_process_history_handling = Web::Bindings::NavigationHistoryBehavior::Replace;
else
ui_process_history_handling = Web::Bindings::NavigationHistoryBehavior::Push;
}
if (ui_process_history_handling == Web::Bindings::NavigationHistoryBehavior::Replace)
m_session_history.replace_current_entry(url, Empty {});
else
m_session_history.navigate(url);
m_current_web_content_session_history_matches_mirror = false;
update_navigation_action_state();
}
}
if (!should_defer_ui_process_history_update)
set_url(url);
dump_session_history("load"sv);
client().async_load_url(page_id(), url, history_handling);
}
void ViewImplementation::load_html(StringView html)
{
m_is_showing_crash_page = false;
m_should_suppress_history_for_current_load = false;
m_should_suppress_history_for_next_load = false;
abandon_pending_web_content_session_history_seed();
if (should_manage_session_history_in_ui_process()) {
m_current_web_content_session_history_matches_mirror = false;
m_session_history.forget_web_content_state();
}
client().async_load_html(page_id(), html);
}
void ViewImplementation::load_crash_page_html(StringView html, URL::URL const& crashed_url)
{
m_is_showing_crash_page = true;
m_should_suppress_history_for_current_load = true;
m_should_suppress_history_for_next_load = true;
abandon_pending_web_content_session_history_seed();
if (should_manage_session_history_in_ui_process()) {
m_current_web_content_session_history_matches_mirror = false;
m_session_history.forget_web_content_state();
}
set_url(crashed_url);
client().async_load_html_with_url(page_id(), html, crashed_url);
}
void ViewImplementation::load_navigation_error_page(StringView text)
{
auto message = MUST(String::formatted("Failed to load \"{}\"", text));
StringBuilder builder;
builder.appendff(ERROR_HTML_HEADER, ""sv, ERROR_SVG, message);
builder.append("<p>If you were trying to enter a search query, please enable search in <a href=\"about:settings#search\">settings</a>.</p>"sv);
builder.append(ERROR_HTML_FOOTER);
load_html(builder.string_view());
}
void ViewImplementation::reload()
{
if (should_manage_session_history_in_ui_process() && m_is_showing_crash_page) {
m_is_showing_crash_page = false;
m_should_suppress_history_for_current_load = false;
m_should_suppress_history_for_next_load = false;
prepare_to_seed_web_content_session_history_from_ui_process();
restore_current_session_history_entry_from_ui_process();
return;
}
m_is_showing_crash_page = false;
m_should_suppress_history_for_current_load = false;
m_should_suppress_history_for_next_load = false;
abandon_pending_web_content_session_history_seed();
if (should_manage_session_history_in_ui_process()) {
m_session_history.mark_current_entry_reload_pending();
m_current_web_content_session_history_matches_mirror = false;
update_navigation_action_state();
dump_session_history("reload-mark-current-entry-reload-pending"sv);
}
client().async_reload(page_id());
}
ViewImplementation::HistoryTraversalOutcome ViewImplementation::traverse_the_history_by_delta(
int delta,
CheckForCancelation check_for_cancelation,
Function<void(HistoryTraversalOutcome)> on_cancelation_check_complete)
{
if (!should_manage_session_history_in_ui_process()) {
m_should_suppress_history_for_current_load = false;
m_should_suppress_history_for_next_load = false;
m_webdriver_pending_navigation_completes_with_session_history_update = false;
client().async_traverse_the_history_by_delta(page_id(), delta);
return {
.status = HistoryTraversalStatus::Started,
.will_change_top_level_entry = true,
};
}
// https://html.spec.whatwg.org/multipage/browsing-the-web.html#traverse-the-history-by-a-delta
// Let allSteps be the result of getting all used history steps for traversable.
// Let currentStepIndex be the index of traversable's current session history step within allSteps.
// Let targetStepIndex be currentStepIndex plus delta.
//
// AD-HOC: Browser UI calls do not pass sourceDocument. Since the UI process owns the top-level session history
// mirror, it resolves targetStepIndex before asking WebContent to apply or precheck the resulting
// traverse history step.
auto target = m_session_history.traversal_target_for_delta(delta);
if (!target.has_value()) {
dump_session_history("traverse-no-entry"sv);
return { .status = HistoryTraversalStatus::NoEntry };
}
m_should_suppress_history_for_current_load = false;
m_should_suppress_history_for_next_load = false;
auto will_replace_web_content_process = !is_url_suitable_for_same_process_navigation(m_url, target->target_top_level_entry->url);
auto pending_traversal = PendingSessionHistoryTraversal {
.target_step = target->target_step,
.target_step_index = target->target_step_index,
.will_change_top_level_entry = target->changes_top_level_entry,
.will_replace_web_content_process = will_replace_web_content_process,
.on_cancelation_check_complete = nullptr,
};
auto web_content_can_apply_traversal = !m_pending_web_content_session_history_seed.should_send_entries
&& !m_pending_web_content_session_history_seed.ignore_updates_until_seed
&& !m_pending_web_content_session_history_seed.waiting_for_ack
&& !m_session_history_entry_url_loading_from_ui_process.has_value()
&& !m_pending_web_content_session_history_seed.step_after_loading_top_level_entry.has_value()
&& m_session_history.web_content_can_traverse_to(*target);
// If WebContent already has enough state to apply the traverse history step,
// let it run the spec algorithm directly.
if (web_content_can_apply_traversal && !will_replace_web_content_process) {
m_pending_session_history_traversal = move(pending_traversal);
m_webdriver_pending_navigation_url = target->target_top_level_entry->url;
if (auto const* current_entry = m_session_history.current_entry()) {
m_webdriver_pending_navigation_completes_with_session_history_update = current_entry->document_state.id != 0
&& current_entry->document_state.id == target->target_top_level_entry->document_state.id;
} else {
m_webdriver_pending_navigation_completes_with_session_history_update = false;
}
dump_session_history("traverse-delegate-to-webcontent"sv);
client().async_traverse_the_history_to_step(page_id(), target->target_step);
return {
.status = HistoryTraversalStatus::Started,
.will_replace_web_content_process = will_replace_web_content_process,
.will_change_top_level_entry = target->changes_top_level_entry,
};
}
// https://html.spec.whatwg.org/multipage/browsing-the-web.html#apply-the-history-step
// If checkForCancelation is true, and the result of checking if unloading is canceled given
// navigablesCrossingDocuments, traversable, targetStep, and userInvolvement is not "continue", then return that
// result.
//
// AD-HOC: If WebContent cannot apply the step itself, the UI still asks it to run that precheck before moving
// the UI-owned history mirror and loading or reseeding WebContent from that state. A renderer-initiated
// traversal can report that this already happened, but only trust that precheck if WebContent can
// traverse to the same target step in the UI-owned history mirror.
auto needs_cancelation_check = check_for_cancelation == CheckForCancelation::Yes
|| (check_for_cancelation == CheckForCancelation::IfWebContentCannotTraverseTarget && !web_content_can_apply_traversal);
if (needs_cancelation_check) {
pending_traversal.stage = PendingSessionHistoryTraversal::Stage::CheckingCancelation;
pending_traversal.cancelation_check_request_id = m_next_traverse_history_step_cancelation_check_request_id++;
pending_traversal.on_cancelation_check_complete = move(on_cancelation_check_complete);
auto request_id = pending_traversal.cancelation_check_request_id;
m_pending_session_history_traversal = move(pending_traversal);
client().async_check_if_traverse_history_step_is_canceled(page_id(), request_id, target->target_step);
dump_session_history("traverse-fallback-check-cancelation"sv);
return {
.status = HistoryTraversalStatus::Started,
.will_replace_web_content_process = will_replace_web_content_process,
.will_change_top_level_entry = target->changes_top_level_entry,
.waiting_for_cancelation_check = true,
};
}
pending_traversal.stage = PendingSessionHistoryTraversal::Stage::LoadingEntryFromUIProcess;
m_pending_session_history_traversal = move(pending_traversal);
load_session_history_traversal_target_from_ui_process(*target, "traverse-fallback-load"sv);
return {
.status = HistoryTraversalStatus::Started,
.will_replace_web_content_process = will_replace_web_content_process,
.will_change_top_level_entry = target->changes_top_level_entry,
};
}
Vector<ViewImplementation::SessionHistoryTraversalMenuItem> ViewImplementation::session_history_traversal_menu_items(int direction) const
{
VERIFY(direction == -1 || direction == 1);
auto current_used_step_index = m_session_history.current_used_step_index();
if (!current_used_step_index.has_value())
return {};
Vector<SessionHistoryTraversalMenuItem> items;
auto append_item = [&](size_t target_step_index, TraversableSessionHistory::Entry const& target_entry) {
auto history_entry = Application::history_store().entry_for_url(target_entry.url);
auto url = target_entry.url.serialize();
auto title = history_entry.has_value() && history_entry->title.has_value() && !history_entry->title->is_empty()
? move(*history_entry->title)
: url;
items.append({
static_cast<int>(target_step_index) - static_cast<int>(*current_used_step_index),
move(title),
move(url),
history_entry.has_value() ? move(history_entry->favicon_base64_png) : Optional<String> {},
});
};
if (direction < 0) {
for (size_t target_step_index = *current_used_step_index; target_step_index > 0; --target_step_index) {
auto target_step = m_session_history.step_at(target_step_index - 1);
if (!target_step.has_value())
continue;
auto const* target_entry = m_session_history.top_level_entry_for_step(*target_step);
if (!target_entry)
continue;
append_item(target_step_index - 1, *target_entry);
}
} else {
for (size_t target_step_index = *current_used_step_index + 1; target_step_index < m_session_history.used_step_count(); ++target_step_index) {
auto target_step = m_session_history.step_at(target_step_index);
if (!target_step.has_value())
continue;
auto const* target_entry = m_session_history.top_level_entry_for_step(*target_step);
if (!target_entry)
continue;
append_item(target_step_index, *target_entry);
}
}
return items;
}
void ViewImplementation::zoom_in()
{
if (m_zoom_level >= ZOOM_MAX_LEVEL)
return;
m_zoom_level = round_to<int>((m_zoom_level + ZOOM_STEP) * 100) / 100.0;
update_zoom();
Application::settings().set_zoom_for_host(current_host(), m_zoom_level);
}
void ViewImplementation::zoom_out()
{
if (m_zoom_level <= ZOOM_MIN_LEVEL)
return;
m_zoom_level = round_to<int>((m_zoom_level - ZOOM_STEP) * 100) / 100.0;
update_zoom();
Application::settings().set_zoom_for_host(current_host(), m_zoom_level);
}
void ViewImplementation::set_zoom(double zoom_level)
{
m_zoom_level = max(ZOOM_MIN_LEVEL, min(zoom_level, ZOOM_MAX_LEVEL));
update_zoom();
}
void ViewImplementation::reset_zoom()
{
m_zoom_level = 1.0;
update_zoom();
client().async_reset_zoom(m_client_state.page_index);
Application::settings().set_zoom_for_host(current_host(), m_zoom_level);
}
void ViewImplementation::enqueue_input_event(Web::InputEvent event)
{
auto* mouse_event = event.get_pointer<Web::MouseEvent>();
auto* pinch_event = event.get_pointer<Web::PinchEvent>();
if (mouse_event && mouse_event->type == Web::MouseEvent::Type::MouseWheel) {
mouse_event->wheel_delta_x /= zoom_level();
mouse_event->wheel_delta_y /= zoom_level();
}
if (Application::web_content_options().enable_async_scrolling == EnableAsyncScrolling::Yes
&& m_client_state.has_usable_bitmap
&& mouse_event) {
if (mouse_event->type == Web::MouseEvent::Type::MouseWheel) {
auto wheel_delta_x = mouse_event->wheel_delta_x;
auto wheel_delta_y = mouse_event->wheel_delta_y;
if (mouse_event->modifiers & Web::UIEvents::KeyModifier::Mod_Shift)
swap(wheel_delta_x, wheel_delta_y);
auto device_pixels_per_css_pixel = static_cast<float>(device_pixel_ratio() * zoom_level());
auto position = Gfx::FloatPoint {
static_cast<float>(mouse_event->position.x().value()),
static_cast<float>(mouse_event->position.y().value()),
};
auto delta_in_device_pixels = Gfx::FloatPoint { wheel_delta_x, wheel_delta_y }.scaled(device_pixels_per_css_pixel);
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] UI attempting compositor wheel bypass for page {} at {},{} device delta {},{}",
m_client_state.page_index, position.x(), position.y(), delta_in_device_pixels.x(), delta_in_device_pixels.y());
if (client().send_async_scroll_to_compositor(m_client_state.page_index, position, delta_in_device_pixels))
mouse_event->async_scroll_performed_default_action = true;
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] UI compositor wheel bypass result for page {}: {}",
m_client_state.page_index, mouse_event->async_scroll_performed_default_action ? "accepted"sv : "rejected"sv);
LibWebView+WebContent: Add feature-gated Compositor process backend We are moving toward an architecture where the browser owns a single process that holds the GPU context, so Skia resources can be shared across every renderer and WebContent processes can be sandboxed away from direct GPU access. A dedicated Compositor helper process is the first step. The earlier commits laid the foundation; this one turns the helper on as a selectable backend, gated behind --enable-compositor-process so the existing in-process path stays the shipping default. Default topology -- one compositor per WebContent, in-process: +---------+ | Browser | +---------+ / | \ v v v +---+ +---+ +---+ each WebContent runs its own |WC | |WC | |WC | CompositorThread on a dedicated |+C+| |+C+| |+C+| thread, with its own GPU context. +---+ +---+ +---+ Opt-in topology -- one single-threaded Compositor, shared by all WCs: +---------+ control +-------------------+ | Browser |<----------->| Compositor | +---------+ | (single thread, | / | \ | single GPU ctx, | v v v data | shared by all | [WC] [WC] [WC] ---------->| connected WCs) | +-------------------+ Three IPC channels carry the work in the opt-in topology. The existing Browser<->WebContent channel gains context allocation. A new Browser<->Compositor channel carries context lifetime, viewport, UI input, and presentation acks plus backing-store and frame upcalls. A new WebContent<->Compositor channel carries display lists, scroll state, video, compositor surfaces, async scrolling, presentation, and screenshots, with upcalls for delegated input and compositor loss.
2026-05-22 11:17:27 -03:00
} else if (client().handle_mouse_event_in_compositor(m_client_state.page_index, *mouse_event)) {
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] UI compositor handled mouse event for page {} at {},{}",
m_client_state.page_index, mouse_event->position.x().value(), mouse_event->position.y().value());
return;
}
}
if (Application::web_content_options().enable_async_scrolling == EnableAsyncScrolling::Yes
&& m_client_state.has_usable_bitmap
&& pinch_event) {
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] UI attempting compositor pinch bypass for page {} at {},{} scale delta {}",
m_client_state.page_index, pinch_event->position.x().value(), pinch_event->position.y().value(), pinch_event->scale_delta);
auto handled = client().handle_pinch_event_in_compositor(m_client_state.page_index, *pinch_event);
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] UI compositor pinch bypass result for page {}: {}",
m_client_state.page_index, handled ? "accepted"sv : "rejected"sv);
}
// Send the next event over to the WebContent to be handled by JS. We'll later get a message to say whether JS
// prevented the default event behavior, at which point we either discard or handle that event, and then try to
// process the next one.
m_pending_input_events.enqueue(move(event));
m_pending_input_events.tail().visit(
[this](Web::KeyEvent const& event) {
client().async_key_event(m_client_state.page_index, event.clone_without_browser_data());
},
[this](Web::MouseEvent const& event) {
LibWebView+WebContent: Add feature-gated Compositor process backend We are moving toward an architecture where the browser owns a single process that holds the GPU context, so Skia resources can be shared across every renderer and WebContent processes can be sandboxed away from direct GPU access. A dedicated Compositor helper process is the first step. The earlier commits laid the foundation; this one turns the helper on as a selectable backend, gated behind --enable-compositor-process so the existing in-process path stays the shipping default. Default topology -- one compositor per WebContent, in-process: +---------+ | Browser | +---------+ / | \ v v v +---+ +---+ +---+ each WebContent runs its own |WC | |WC | |WC | CompositorThread on a dedicated |+C+| |+C+| |+C+| thread, with its own GPU context. +---+ +---+ +---+ Opt-in topology -- one single-threaded Compositor, shared by all WCs: +---------+ control +-------------------+ | Browser |<----------->| Compositor | +---------+ | (single thread, | / | \ | single GPU ctx, | v v v data | shared by all | [WC] [WC] [WC] ---------->| connected WCs) | +-------------------+ Three IPC channels carry the work in the opt-in topology. The existing Browser<->WebContent channel gains context allocation. A new Browser<->Compositor channel carries context lifetime, viewport, UI input, and presentation acks plus backing-store and frame upcalls. A new WebContent<->Compositor channel carries display lists, scroll state, video, compositor surfaces, async scrolling, presentation, and screenshots, with upcalls for delegated input and compositor loss.
2026-05-22 11:17:27 -03:00
client().dispatch_mouse_event_to_web_content(m_client_state.page_index, event);
},
[this](Web::DragEvent& event) {
auto cloned_event = event.clone_without_browser_data();
cloned_event.files = move(event.files);
client().async_drag_event(m_client_state.page_index, cloned_event);
},
[this](Web::PinchEvent const& event) {
client().async_pinch_event(m_client_state.page_index, event);
});
}
void ViewImplementation::did_finish_handling_input_event(Badge<WebContentClient>, Web::EventResult event_result)
{
auto event = m_pending_input_events.dequeue();
if (event_result == Web::EventResult::Handled)
return;
// Here we handle events that were not consumed or cancelled by the WebContent. Propagate the event back
// to the concrete view implementation.
event.visit(
[this](Web::KeyEvent const& event) {
if (on_finish_handling_key_event)
on_finish_handling_key_event(event);
},
[this](Web::DragEvent const& event) {
if (on_finish_handling_drag_event)
on_finish_handling_drag_event(event);
},
[](auto const&) {});
}
void ViewImplementation::set_preferred_color_scheme(Web::CSS::PreferredColorScheme color_scheme)
{
m_preferred_color_scheme = color_scheme;
set_page_background_color(preferred_canvas_background_color());
client().async_set_preferred_color_scheme(page_id(), color_scheme);
}
2024-06-12 20:03:56 -03:00
void ViewImplementation::set_preferred_contrast(Web::CSS::PreferredContrast contrast)
{
client().async_set_preferred_contrast(page_id(), contrast);
}
void ViewImplementation::set_preferred_motion(Web::CSS::PreferredMotion motion)
{
client().async_set_preferred_motion(page_id(), motion);
}
void ViewImplementation::notify_cookies_changed(HashTable<String> const& changed_domains, ReadonlySpan<HTTP::Cookie::Cookie> page_cookies, ReadonlySpan<HTTP::Cookie::Cookie> host_cookies)
{
for (auto const& domain : changed_domains) {
if (auto document_index = m_document_cookie_version_indices.get(domain); document_index.has_value())
Core::increment_shared_version(m_document_cookie_version_buffer, *document_index);
}
if (!page_cookies.is_empty())
client().async_cookies_changed(page_id(), page_cookies);
if (m_on_host_cookie_change)
m_on_host_cookie_change(Vector<HTTP::Cookie::Cookie> { host_cookies });
}
void ViewImplementation::listen_for_host_cookie_changes(DevTools::DevToolsDelegate::OnHostCookieChange on_cookie_change)
{
m_on_host_cookie_change = move(on_cookie_change);
}
void ViewImplementation::stop_listening_for_host_cookie_changes()
{
m_on_host_cookie_change = nullptr;
}
void ViewImplementation::notify_storage_changed(DevTools::DevToolsDelegate::StorageChange change)
{
for (auto& listener : m_storage_change_listeners)
listener.value(change);
}
u64 ViewImplementation::add_storage_change_listener(DevTools::DevToolsDelegate::OnStorageChange on_storage_change)
{
auto listener_id = m_next_storage_change_listener_id++;
m_storage_change_listeners.set(listener_id, move(on_storage_change));
return listener_id;
}
void ViewImplementation::remove_storage_change_listener(u64 listener_id)
{
m_storage_change_listeners.remove(listener_id);
}
ErrorOr<Core::SharedVersionIndex> ViewImplementation::ensure_document_cookie_version_index(Badge<WebContentClient>, String const& domain)
{
return m_document_cookie_version_indices.try_ensure(domain, [&]() -> ErrorOr<Core::SharedVersionIndex> {
Core::SharedVersionIndex document_index = m_document_cookie_version_indices.size();
if (!Core::initialize_shared_version(m_document_cookie_version_buffer, document_index)) {
dbgln("Reached maximum document cookie version count for {}, cannot create new version for {}", m_url, domain);
return Error::from_string_literal("Reached maximum document cookie version count");
}
return document_index;
});
}
Optional<Core::SharedVersion> ViewImplementation::document_cookie_version(URL::URL const& url) const
{
auto domain = HTTP::Cookie::canonicalize_domain(url);
if (!domain.has_value())
return {};
auto document_index = m_document_cookie_version_indices.get(*domain);
if (!document_index.has_value())
return {};
return Core::get_shared_version(m_document_cookie_version_buffer, *document_index);
}
ByteString ViewImplementation::selected_text()
{
return client().get_selected_text(page_id());
}
ByteString ViewImplementation::cut_selected_text()
{
return client().cut_selected_text(page_id());
}
Optional<String> ViewImplementation::selected_text_with_whitespace_collapsed()
{
auto selected_text = MUST(Web::Infra::strip_and_collapse_whitespace(this->selected_text()));
if (selected_text.is_empty())
return OptionalNone {};
return selected_text;
}
void ViewImplementation::select_all()
{
client().async_select_all(page_id());
}
void ViewImplementation::find_in_page(String const& query, CaseSensitivity case_sensitivity)
{
client().async_find_in_page(page_id(), query, case_sensitivity);
}
void ViewImplementation::find_in_page_next_match()
{
client().async_find_in_page_next_match(page_id());
}
void ViewImplementation::find_in_page_previous_match()
{
client().async_find_in_page_previous_match(page_id());
}
void ViewImplementation::get_source()
{
client().async_get_source(page_id());
}
void ViewImplementation::inspect_dom_tree()
{
client().async_inspect_dom_tree(page_id());
}
void ViewImplementation::inspect_storage(Web::StorageAPI::StorageEndpointType storage_endpoint, u64 request_id)
{
client().async_inspect_storage(page_id(), storage_endpoint, request_id);
}
Optional<StorageSetResult> ViewImplementation::set_session_storage_item(String const& key, String const& value)
{
return client().set_session_storage_item(page_id(), key, value);
}
Optional<String> ViewImplementation::remove_session_storage_item(String const& key)
{
return client().remove_session_storage_item(page_id(), key);
}
bool ViewImplementation::clear_session_storage()
{
return client().clear_session_storage(page_id());
}
void ViewImplementation::inspect_accessibility_tree()
{
client().async_inspect_accessibility_tree(page_id());
}
void ViewImplementation::get_hovered_node_id()
{
client().async_get_hovered_node_id(page_id());
}
void ViewImplementation::start_node_picker(DevTools::DevToolsDelegate::OnNodePickerEvent on_node_picker_event)
{
m_node_picker_active = true;
m_node_picker_hovered_node_id.clear();
m_pending_node_picker_requests.clear();
m_on_node_picker_event = move(on_node_picker_event);
}
void ViewImplementation::stop_node_picker()
{
if (!m_node_picker_active)
return;
clear_node_picker();
m_node_picker_active = false;
m_pending_node_picker_requests.clear();
m_on_node_picker_event = nullptr;
}
void ViewImplementation::clear_node_picker()
{
m_node_picker_hovered_node_id.clear();
clear_highlighted_dom_node();
}
void ViewImplementation::node_picker_hover(Web::DevicePixelPoint position)
{
request_node_picker_hit_test(NodePickerRequestType::Hovered, position);
}
void ViewImplementation::node_picker_pick(Web::DevicePixelPoint position)
{
request_node_picker_hit_test(NodePickerRequestType::Picked, position);
}
void ViewImplementation::node_picker_preview(Web::DevicePixelPoint position)
{
request_node_picker_hit_test(NodePickerRequestType::Previewed, position);
}
void ViewImplementation::node_picker_cancel()
{
if (!m_node_picker_active)
return;
if (m_on_node_picker_event) {
m_on_node_picker_event({
.type = DevTools::DevToolsDelegate::NodePickerEvent::Type::Canceled,
.node_id = {},
});
}
}
void ViewImplementation::request_node_picker_hit_test(NodePickerRequestType type, Web::DevicePixelPoint position)
{
if (!m_node_picker_active)
return;
auto request_id = m_next_node_picker_request_id++;
m_pending_node_picker_requests.set(request_id, type);
client().async_get_node_id_at_position(page_id(), request_id, position);
}
void ViewImplementation::did_receive_node_picker_hit_test(u64 request_id, Web::UniqueNodeID node_id)
{
auto request_type = m_pending_node_picker_requests.take(request_id);
if (!request_type.has_value() || !m_node_picker_active)
return;
if (*request_type == NodePickerRequestType::Hovered) {
if (node_id == m_node_picker_hovered_node_id.value_or(0))
return;
m_node_picker_hovered_node_id = node_id;
if (node_id == 0) {
clear_node_picker();
return;
}
} else if (node_id == 0) {
return;
}
if (!m_on_node_picker_event)
return;
DevTools::DevToolsDelegate::NodePickerEvent::Type event_type;
switch (*request_type) {
case NodePickerRequestType::Hovered:
event_type = DevTools::DevToolsDelegate::NodePickerEvent::Type::Hovered;
break;
case NodePickerRequestType::Picked:
event_type = DevTools::DevToolsDelegate::NodePickerEvent::Type::Picked;
break;
case NodePickerRequestType::Previewed:
event_type = DevTools::DevToolsDelegate::NodePickerEvent::Type::Previewed;
break;
}
m_on_node_picker_event({
.type = event_type,
.node_id = node_id,
});
}
void ViewImplementation::inspect_dom_node(Web::UniqueNodeID node_id, DOMNodeProperties::Type property_type, Optional<Web::CSS::PseudoElement> pseudo_element, JsonValue options)
{
client().async_inspect_dom_node(page_id(), property_type, node_id, pseudo_element, move(options));
}
void ViewImplementation::inspect_grid_layouts(Web::UniqueNodeID root_node_id)
{
client().async_inspect_grid_layouts(page_id(), root_node_id);
}
void ViewImplementation::inspect_current_grid(Web::UniqueNodeID node_id)
{
client().async_inspect_current_grid(page_id(), node_id);
}
void ViewImplementation::inspect_current_flexbox(Web::UniqueNodeID node_id, bool only_look_at_parents)
{
client().async_inspect_current_flexbox(page_id(), node_id, only_look_at_parents);
}
void ViewImplementation::clear_inspected_dom_node()
{
client().async_clear_inspected_dom_node(page_id());
}
void ViewImplementation::highlight_dom_node(Web::UniqueNodeID node_id, Optional<Web::CSS::PseudoElement> pseudo_element)
{
client().async_highlight_dom_node(page_id(), node_id, pseudo_element);
}
void ViewImplementation::clear_highlighted_dom_node()
{
highlight_dom_node(0, {});
}
void ViewImplementation::highlight_flexbox(Web::UniqueNodeID node_id, JsonValue options)
{
client().async_highlight_flexbox(page_id(), node_id, move(options));
}
void ViewImplementation::clear_flexbox_highlight(Web::UniqueNodeID node_id)
{
client().async_clear_flexbox_highlight(page_id(), node_id);
}
void ViewImplementation::highlight_grid(Web::UniqueNodeID node_id, JsonValue options)
{
client().async_highlight_grid(page_id(), node_id, move(options));
}
void ViewImplementation::clear_grid_highlight(Web::UniqueNodeID node_id)
{
client().async_clear_grid_highlight(page_id(), node_id);
}
void ViewImplementation::set_listen_for_dom_mutations(bool listen_for_dom_mutations)
{
client().async_set_listen_for_dom_mutations(page_id(), listen_for_dom_mutations);
}
void ViewImplementation::did_connect_devtools_client()
{
m_devtools_connected = true;
client().async_did_connect_devtools_client(page_id());
}
void ViewImplementation::did_disconnect_devtools_client()
{
m_devtools_connected = false;
client().async_did_disconnect_devtools_client(page_id());
}
void ViewImplementation::get_dom_node_inner_html(Web::UniqueNodeID node_id)
{
client().async_get_dom_node_inner_html(page_id(), node_id);
}
void ViewImplementation::get_dom_node_outer_html(Web::UniqueNodeID node_id)
{
client().async_get_dom_node_outer_html(page_id(), node_id);
}
void ViewImplementation::set_dom_node_outer_html(Web::UniqueNodeID node_id, String const& html)
{
client().async_set_dom_node_outer_html(page_id(), node_id, html);
}
void ViewImplementation::set_dom_node_text(Web::UniqueNodeID node_id, String const& text)
{
client().async_set_dom_node_text(page_id(), node_id, text);
}
void ViewImplementation::set_dom_node_tag(Web::UniqueNodeID node_id, String const& name)
{
client().async_set_dom_node_tag(page_id(), node_id, name);
}
void ViewImplementation::add_dom_node_attributes(Web::UniqueNodeID node_id, ReadonlySpan<Attribute> attributes)
{
client().async_add_dom_node_attributes(page_id(), node_id, attributes);
}
void ViewImplementation::replace_dom_node_attribute(Web::UniqueNodeID node_id, String const& name, ReadonlySpan<Attribute> replacement_attributes)
{
client().async_replace_dom_node_attribute(page_id(), node_id, name, replacement_attributes);
}
void ViewImplementation::create_child_element(Web::UniqueNodeID node_id)
{
client().async_create_child_element(page_id(), node_id);
}
void ViewImplementation::create_child_text_node(Web::UniqueNodeID node_id)
{
client().async_create_child_text_node(page_id(), node_id);
}
void ViewImplementation::insert_dom_node_before(Web::UniqueNodeID node_id, Web::UniqueNodeID parent_node_id, Optional<Web::UniqueNodeID> sibling_node_id)
{
client().async_insert_dom_node_before(page_id(), node_id, parent_node_id, sibling_node_id);
}
void ViewImplementation::clone_dom_node(Web::UniqueNodeID node_id)
{
client().async_clone_dom_node(page_id(), node_id);
}
void ViewImplementation::remove_dom_node(Web::UniqueNodeID node_id)
{
client().async_remove_dom_node(page_id(), node_id);
}
void ViewImplementation::list_style_sheets()
{
client().async_list_style_sheets(page_id());
}
void ViewImplementation::request_style_sheet_source(Web::CSS::StyleSheetIdentifier const& identifier)
{
client().async_request_style_sheet_source(page_id(), identifier);
}
void ViewImplementation::debug_request(ByteString const& request, ByteString const& argument)
{
if (request == "dump-session-history"sv)
dump_session_history("debug-request"sv, SessionHistoryDumpMode::Always);
client().async_debug_request(page_id(), request, argument);
}
void ViewImplementation::set_content_blockers(Core::AnonymousBuffer const& patterns)
{
client().async_set_content_blockers(page_id(), patterns);
}
void ViewImplementation::run_javascript(String const& js_source)
{
client().async_run_javascript(page_id(), js_source);
}
void ViewImplementation::js_console_input(String const& js_source)
{
client().async_js_console_input(page_id(), js_source);
}
void ViewImplementation::exit_fullscreen()
{
client().async_exit_fullscreen(page_id());
}
void ViewImplementation::set_is_fullscreen(Web::ViewportIsFullscreen is_fullscreen)
{
if (m_is_fullscreen == is_fullscreen)
return;
m_is_fullscreen = is_fullscreen;
handle_resize();
did_update_window_rect();
}
void ViewImplementation::alert_closed()
{
client().async_alert_closed(page_id());
}
void ViewImplementation::confirm_closed(bool accepted)
{
client().async_confirm_closed(page_id(), accepted);
}
void ViewImplementation::prompt_closed(Optional<String> const& response)
{
client().async_prompt_closed(page_id(), response);
}
void ViewImplementation::color_picker_update(Optional<Color> picked_color, Web::HTML::ColorPickerUpdateState state)
{
client().async_color_picker_update(page_id(), picked_color, state);
}
void ViewImplementation::file_picker_closed(Vector<Web::HTML::SelectedFile> selected_files)
{
client().async_file_picker_closed(page_id(), move(selected_files));
}
void ViewImplementation::select_dropdown_closed(Optional<u32> const& selected_item_id)
{
client().async_select_dropdown_closed(page_id(), selected_item_id);
}
void ViewImplementation::paste_text_from_clipboard()
{
client().async_paste(page_id(), Application::the().clipboard_text());
}
void ViewImplementation::set_marked_text_from_input_method(Utf16String const& text)
{
client().async_set_marked_text_from_input_method(page_id(), text);
}
void ViewImplementation::commit_text_from_input_method(Utf16String const& text)
{
client().async_commit_text_from_input_method(page_id(), text);
}
void ViewImplementation::unmark_text_from_input_method()
{
client().async_unmark_text_from_input_method(page_id());
}
Optional<Web::DevicePixelRect> ViewImplementation::get_input_caret_rect()
{
// Returns the most-recent caret position pushed by WebContent (see set_input_caret_rect). Deliberately makes no
// synchronous IPC request: This is read from inside AppKit text-input callbacks — where blocking can re-enter the
// run loop and deadlock the input method.
return m_input_caret_rect;
}
void ViewImplementation::set_input_caret_rect(Badge<WebContentClient>, Optional<Web::DevicePixelRect> rect)
{
m_input_caret_rect = rect;
}
void ViewImplementation::retrieved_clipboard_entries(u64 request_id, ReadonlySpan<Web::Clipboard::SystemClipboardItem> items)
{
client().async_retrieved_clipboard_entries(page_id(), request_id, items);
}
void ViewImplementation::toggle_page_mute_state()
{
m_mute_state = Web::HTML::invert_mute_state(m_mute_state);
client().async_toggle_page_mute_state(page_id());
}
void ViewImplementation::did_change_audio_play_state(Badge<WebContentClient>, Web::HTML::AudioPlayState play_state)
{
bool state_changed = false;
switch (play_state) {
case Web::HTML::AudioPlayState::Paused:
if (--m_number_of_elements_playing_audio == 0) {
m_audio_play_state = play_state;
state_changed = true;
}
break;
case Web::HTML::AudioPlayState::Playing:
if (m_number_of_elements_playing_audio++ == 0) {
m_audio_play_state = play_state;
state_changed = true;
}
break;
}
if (state_changed && on_audio_play_state_changed)
on_audio_play_state_changed(m_audio_play_state);
}
void ViewImplementation::did_update_navigation_buttons_state(Badge<WebContentClient>, bool back_enabled, bool forward_enabled)
{
VERIFY(!should_manage_session_history_in_ui_process());
m_navigate_back_action->set_enabled(back_enabled);
m_navigate_forward_action->set_enabled(forward_enabled);
dump_session_history("did-update-navigation-buttons-state-using-webcontent"sv);
}
static Optional<size_t> current_top_level_history_entry_index_for_step(Vector<Web::HTML::SessionHistoryEntryDescriptor> const&, Optional<i32> current_step);
TraversableSessionHistory::UpdateResult ViewImplementation::update_session_history_from_web_content(Vector<Web::HTML::SessionHistoryEntryDescriptor> entries, Vector<i32> used_steps, size_t current_used_step_index, bool pending_step_after_fallback_load_was_restored, bool seed_web_content_on_invalid_snapshot)
{
auto update_result = m_session_history.update_from_web_content(move(entries), move(used_steps), current_used_step_index);
m_current_web_content_session_history_matches_mirror = update_result == TraversableSessionHistory::UpdateResult::CompleteSnapshot
&& m_session_history.web_content_history_matches_mirror();
if (update_result != TraversableSessionHistory::UpdateResult::InvalidSnapshot) {
// NB: A complete WebContent snapshot means the UI-owned navigation settled, including redirected navigations
// whose final URL differs from the original pending navigation URL. A partial snapshot only updates the UI
// mirror with the current WebContent-visible subset.
if (update_result == TraversableSessionHistory::UpdateResult::CompleteSnapshot)
m_pending_session_history_navigation.clear();
if (auto* current_entry = m_session_history.current_entry()) {
auto current_url = current_entry->url;
auto const url_changed = m_url != current_url;
set_url(current_url);
if (url_changed && on_url_change)
on_url_change(m_url);
if (m_webdriver_pending_navigation_url.has_value() && *m_webdriver_pending_navigation_url != current_url)
m_webdriver_pending_navigation_url = current_url;
if (m_webdriver_pending_navigation_completes_with_session_history_update)
complete_webdriver_pending_navigation_if_url_matches(m_url);
}
if (pending_step_after_fallback_load_was_restored)
m_pending_web_content_session_history_seed.step_after_loading_top_level_entry.clear();
} else if (seed_web_content_on_invalid_snapshot) {
if (auto const* current_entry = m_session_history.current_entry(); current_entry && current_entry->url == m_url) {
prepare_to_seed_web_content_session_history_from_ui_process();
seed_web_content_session_history_from_ui_process();
}
}
update_navigation_action_state();
return update_result;
}
bool ViewImplementation::adopt_web_content_session_history_after_rejected_seed(Vector<Web::HTML::SessionHistoryEntryDescriptor> entries, Vector<i32> used_steps, size_t current_used_step_index)
{
if (entries.is_empty())
return false;
auto entries_from_web_content = entries;
auto used_steps_from_web_content = used_steps;
auto update_result = update_session_history_from_web_content(move(entries), move(used_steps), current_used_step_index, false, false);
if (update_result == TraversableSessionHistory::UpdateResult::InvalidSnapshot && current_used_step_index < used_steps_from_web_content.size()) {
auto current_top_level_entry_index = current_top_level_history_entry_index_for_step(entries_from_web_content, used_steps_from_web_content[current_used_step_index]);
if (current_top_level_entry_index.has_value() && entries_from_web_content[*current_top_level_entry_index].url == m_url) {
m_session_history.clear();
update_result = update_session_history_from_web_content(move(entries_from_web_content), move(used_steps_from_web_content), current_used_step_index, false, false);
}
}
if (update_result == TraversableSessionHistory::UpdateResult::InvalidSnapshot)
return false;
m_pending_web_content_session_history_seed.clear();
m_pending_session_history_traversal.clear();
return true;
}
void ViewImplementation::did_update_session_history(Badge<WebContentClient>, Vector<Web::HTML::SessionHistoryEntryDescriptor> entries, Vector<i32> used_steps, size_t current_used_step_index)
{
if (!should_manage_session_history_in_ui_process())
return;
if (history_debug_enabled()) {
dbgln("[History] UI received WebContent session history snapshot page={} pid={} current_used_step={} entries={} used_steps={}",
page_id(),
client().pid(),
current_used_step_index,
history_log_entries(entries),
history_log_steps(used_steps, current_used_step_index));
}
if (m_pending_web_content_session_history_seed.waiting_for_ack) {
dump_session_history("ignored-session-history-before-ui-seed-ack"sv);
update_navigation_action_state();
return;
}
auto pending_step_after_fallback_load_was_restored = false;
if (m_pending_web_content_session_history_seed.step_after_loading_top_level_entry.has_value()) {
if (current_used_step_index >= used_steps.size() || used_steps[current_used_step_index] != *m_pending_web_content_session_history_seed.step_after_loading_top_level_entry) {
dump_session_history("ignored-partial-session-history-before-fallback-seed"sv);
update_navigation_action_state();
return;
}
pending_step_after_fallback_load_was_restored = true;
}
if (m_pending_web_content_session_history_seed.ignore_updates_until_seed) {
dump_session_history("ignored-session-history-before-ui-seed"sv);
update_navigation_action_state();
return;
}
update_session_history_from_web_content(move(entries), move(used_steps), current_used_step_index, pending_step_after_fallback_load_was_restored, true);
dump_session_history("did-update-session-history"sv);
}
void ViewImplementation::did_update_session_history_for_testing(Badge<WebContentClient>, Vector<Web::HTML::SessionHistoryEntryDescriptor> entries, Vector<i32> used_steps, size_t current_used_step_index)
{
if (!should_manage_session_history_in_ui_process())
return;
// NB: dumpUIProcessSessionHistory() first sends WebContent's current snapshot to the UI process, then returns
// the UI mirror. If a stale seed ack is still pending, normal async snapshots are intentionally ignored, so
// use the same convergence path as a rejected seed ack to make this testing hook deterministic.
if (m_pending_web_content_session_history_seed.waiting_for_ack) {
if (adopt_web_content_session_history_after_rejected_seed(move(entries), move(used_steps), current_used_step_index))
dump_session_history("did-update-session-history-for-testing-after-rejected-seed"sv);
else
dump_session_history("ignored-session-history-for-testing-before-ui-seed-ack"sv);
return;
}
update_session_history_from_web_content(move(entries), move(used_steps), current_used_step_index, false, true);
dump_session_history("did-update-session-history-for-testing"sv);
}
void ViewImplementation::did_change_needs_beforeunload_check(Badge<WebContentClient>, bool needs_beforeunload_check)
{
m_needs_beforeunload_check = needs_beforeunload_check;
}
void ViewImplementation::did_change_background_color(Badge<WebContentClient>, Gfx::Color color)
{
set_page_background_color(color);
}
void ViewImplementation::set_page_background_color_to_system_canvas(bool dark)
{
auto color_scheme = dark ? Web::CSS::PreferredColorScheme::Dark : Web::CSS::PreferredColorScheme::Light;
m_system_canvas_background_color = Web::CSS::SystemColor::canvas(color_scheme);
set_page_background_color(preferred_canvas_background_color());
}
void ViewImplementation::set_page_background_color(Gfx::Color color)
{
m_page_background_color = color;
if (on_page_background_color_change)
on_page_background_color_change(m_page_background_color);
}
Gfx::Color ViewImplementation::preferred_canvas_background_color() const
{
if (m_preferred_color_scheme == Web::CSS::PreferredColorScheme::Dark)
return Web::CSS::SystemColor::canvas(Web::CSS::PreferredColorScheme::Dark);
if (m_preferred_color_scheme == Web::CSS::PreferredColorScheme::Light)
return Web::CSS::SystemColor::canvas(Web::CSS::PreferredColorScheme::Light);
return m_system_canvas_background_color;
}
void ViewImplementation::did_allocate_backing_stores(Badge<WebContentClient>, i32 front_bitmap_id, Gfx::SharedImage front_backing_store, i32 back_bitmap_id, Gfx::SharedImage back_backing_store)
{
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] UI installing backing stores for page {} front={} back={} had_usable_bitmap={}",
page_id(), front_bitmap_id, back_bitmap_id, m_client_state.has_usable_bitmap);
if (m_client_state.has_usable_bitmap) {
// NOTE: We keep the outgoing front bitmap as a backup so we have something to paint until we get a new one.
m_backup_shared_image_buffer = move(m_client_state.front_bitmap.shared_image_buffer);
m_backup_bitmap_size = m_client_state.front_bitmap.last_painted_size;
}
m_client_state.has_usable_bitmap = false;
m_client_state.front_bitmap.id = front_bitmap_id;
m_client_state.back_bitmap.id = back_bitmap_id;
m_client_state.front_bitmap.shared_image_buffer = make<Gfx::SharedImageBuffer>(Gfx::SharedImageBuffer::import_from_shared_image(move(front_backing_store)));
m_client_state.back_bitmap.shared_image_buffer = make<Gfx::SharedImageBuffer>(Gfx::SharedImageBuffer::import_from_shared_image(move(back_backing_store)));
}
2025-09-10 08:23:15 -03:00
void ViewImplementation::update_zoom()
{
if (m_zoom_level != 1.0) {
2025-09-10 08:23:15 -03:00
m_reset_zoom_action->set_text(MUST(String::formatted("{}%", round_to<int>(m_zoom_level * 100))));
m_reset_zoom_action->set_visible(true);
} else {
m_reset_zoom_action->set_visible(false);
}
client().async_set_zoom_level(m_client_state.page_index, m_zoom_level);
2025-09-10 08:23:15 -03:00
}
String ViewImplementation::current_host() const
{
if (!m_url.host().has_value())
return {};
return m_url.serialized_host();
}
void ViewImplementation::apply_zoom_for_current_host()
{
auto& settings = Application::settings();
auto zoom_level = settings.zoom_for_host(current_host()).value_or(settings.default_zoom_level_factor());
if (zoom_level == m_zoom_level)
return;
m_zoom_level = zoom_level;
update_zoom();
}
void ViewImplementation::handle_resize()
{
client().async_set_viewport(page_id(), viewport_size(), m_device_pixel_ratio, m_is_fullscreen);
Application::the().update_compositor_viewport(client().compositor_context_id_for_page(page_id()), viewport_size().to_type<int>(), Web::Compositor::WindowResizingInProgress::Yes);
}
void ViewImplementation::initialize_client(CreateNewClient create_new_client)
{
m_needs_beforeunload_check = true;
if (create_new_client == CreateNewClient::Yes) {
auto client_handle = m_client_state.client_handle;
m_client_state = {};
m_client_state.client_handle = move(client_handle);
// FIXME: Fail to open the tab, rather than crashing the whole application if this fails.
m_client_state.client = Application::the().launch_web_content_process(*this).release_value_but_fixme_should_propagate_errors();
} else {
m_client_state.client->register_view(m_client_state.page_index, *this);
}
if (m_client_state.client_handle.is_empty())
m_client_state.client_handle = Web::Crypto::generate_random_uuid();
client().async_set_window_handle(m_client_state.page_index, m_client_state.client_handle);
client().async_set_zoom_level(m_client_state.page_index, m_zoom_level);
client().async_set_viewport(m_client_state.page_index, viewport_size(), m_device_pixel_ratio, m_is_fullscreen);
client().async_set_maximum_frames_per_second(m_client_state.page_index, m_maximum_frames_per_second);
client().async_set_system_visibility_state(m_client_state.page_index, m_system_visibility_state);
auto compositor_context_id = client().compositor_context_id_for_page(m_client_state.page_index);
Application::the().update_compositor_viewport(compositor_context_id, viewport_size().to_type<int>());
client().async_set_document_cookie_version_buffer(m_client_state.page_index, m_document_cookie_version_buffer);
if (auto webdriver_endpoint = Application::browser_options().webdriver_endpoint; webdriver_endpoint.has_value())
client().async_connect_to_webdriver(m_client_state.page_index, *webdriver_endpoint);
Application::the().apply_view_options({}, *this);
default_zoom_level_factor_changed();
languages_changed();
browsing_behavior_changed();
autoplay_settings_changed();
global_privacy_control_changed();
// If DevTools is connected, notify the new WebContent process.
if (m_devtools_connected)
client().async_did_connect_devtools_client(page_id());
}
void ViewImplementation::did_start_navigation(URL::URL const& url, Variant<Empty, String, Web::HTML::POSTResource> document_resource, bool is_redirect, Web::Bindings::NavigationHistoryBehavior history_handling)
{
if (!should_manage_session_history_in_ui_process())
return;
if (m_should_suppress_history_for_next_load || m_should_suppress_history_for_current_load)
return;
if (m_session_history_entry_url_loading_from_ui_process.has_value()) {
if (*m_session_history_entry_url_loading_from_ui_process != url) {
// NB: An earlier UI-process history fallback load can start after a newer back/forward request has
// superseded it. Chromium, WebKit, and Firefox all give pending history loads an identity so stale
// traversals cannot consume state belonging to the current one. Keep the UI-owned history authoritative
// here and wait for the load matching the latest requested entry.
dump_session_history("ignored-stale-ui-history-load-start"sv);
return;
}
auto should_keep_preseeded_web_content_history = m_pending_web_content_session_history_seed.waiting_for_ack || m_session_history.web_content_uses_ui_step_coordinates();
m_session_history_entry_url_loading_from_ui_process.clear();
if (!should_keep_preseeded_web_content_history) {
m_current_web_content_session_history_matches_mirror = false;
m_session_history.forget_web_content_state();
}
dump_session_history("did-start-navigation-from-ui-history-load"sv);
return;
}
if (m_pending_web_content_session_history_seed.should_send_entries || m_pending_web_content_session_history_seed.ignore_updates_until_seed || m_pending_web_content_session_history_seed.waiting_for_ack) {
if (auto const* current_entry = m_session_history.current_entry(); current_entry && current_entry->url != url) {
dump_session_history("ignored-navigation-start-before-ui-history-seed"sv);
return;
}
}
if (m_is_showing_crash_page) {
m_is_showing_crash_page = false;
if (auto const* current_entry = m_session_history.current_entry(); current_entry && current_entry->url == url) {
prepare_to_seed_web_content_session_history_from_ui_process();
dump_session_history("did-start-navigation-from-crash-page"sv);
return;
}
}
if (is_redirect) {
m_session_history.replace_current_entry_url(url);
if (m_pending_session_history_navigation.has_value())
m_pending_session_history_navigation->url = url;
if (m_webdriver_pending_navigation_url.has_value())
m_webdriver_pending_navigation_url = url;
m_current_web_content_session_history_matches_mirror = false;
update_navigation_action_state();
dump_session_history("did-start-navigation-redirect"sv);
return;
}
if (auto const* current_entry = m_session_history.current_entry(); current_entry && current_entry->url == url) {
if (m_pending_session_history_navigation.has_value() && m_pending_session_history_navigation->url == url)
return;
if (history_handling == Web::Bindings::NavigationHistoryBehavior::Push && m_current_web_content_session_history_matches_mirror)
m_pending_session_history_navigation = PendingSessionHistoryNavigation { url, m_session_history };
else
m_pending_session_history_navigation.clear();
if (history_handling == Web::Bindings::NavigationHistoryBehavior::Replace) {
m_session_history.replace_current_entry(url, move(document_resource));
m_current_web_content_session_history_matches_mirror = false;
update_navigation_action_state();
dump_session_history("did-start-navigation-replace-current-url"sv);
} else if (history_handling == Web::Bindings::NavigationHistoryBehavior::Push) {
m_session_history.navigate(url, move(document_resource));
m_current_web_content_session_history_matches_mirror = false;
update_navigation_action_state();
dump_session_history("did-start-navigation-push-current-url"sv);
}
return;
}
if (m_session_history.current_entry())
m_pending_session_history_navigation = PendingSessionHistoryNavigation { url, m_session_history };
else
m_pending_session_history_navigation.clear();
if (history_handling == Web::Bindings::NavigationHistoryBehavior::Replace)
m_session_history.replace_current_entry(url, move(document_resource));
else
m_session_history.navigate(url, move(document_resource));
m_current_web_content_session_history_matches_mirror = false;
update_navigation_action_state();
dump_session_history("did-start-navigation"sv);
}
void ViewImplementation::did_cancel_navigation(URL::URL const& url)
{
if (m_pending_session_history_navigation.has_value() && m_pending_session_history_navigation->url == url) {
restore_pending_session_history_navigation("did-cancel-navigation"sv);
return;
}
if (m_session_history_entry_url_loading_from_ui_process.has_value() && *m_session_history_entry_url_loading_from_ui_process == url) {
m_session_history_entry_url_loading_from_ui_process.clear();
abandon_pending_web_content_session_history_seed();
m_current_web_content_session_history_matches_mirror = false;
m_session_history.forget_web_content_state();
update_navigation_action_state();
dump_session_history("did-cancel-ui-history-load"sv);
return;
}
if (m_webdriver_pending_navigation_url.has_value()) {
m_session_history.clear_current_entry_reload_pending();
m_webdriver_pending_navigation_url = m_url;
m_webdriver_pending_navigation_completes_with_session_history_update = false;
complete_webdriver_pending_navigation_if_url_matches(m_url);
}
dump_session_history("did-cancel-navigation-ignored"sv);
}
void ViewImplementation::did_finish_navigation(URL::URL const& url)
{
if (m_webdriver_pending_navigation_url.has_value() && *m_webdriver_pending_navigation_url == url && !m_webdriver_pending_navigation_completes_with_session_history_update)
complete_webdriver_pending_navigation_if_url_matches(url);
if (m_pending_session_history_navigation.has_value() && m_pending_session_history_navigation->url == url)
m_pending_session_history_navigation.clear();
if (should_manage_session_history_in_ui_process() && m_pending_web_content_session_history_seed.should_send_entries) {
if (auto const* current_entry = m_session_history.current_entry(); current_entry && current_entry->url == url) {
m_session_history.clear_current_entry_reload_pending();
auto allow_current_entry_reconstruction = m_pending_web_content_session_history_seed.should_reseed_after_current_history_load
? AllowCurrentEntryReconstruction::Yes
: AllowCurrentEntryReconstruction::No;
m_pending_web_content_session_history_seed.should_reseed_after_current_history_load = false;
seed_web_content_session_history_from_ui_process(allow_current_entry_reconstruction);
} else {
// NB: The first finish notification from a fresh WebContent process can still report about:blank before the
// traversed-to entry is ready. Keep the pending seed state intact so partial snapshots remain ignored
// until we can seed the full UI-owned history.
dump_session_history("skip-seed-webcontent-session-history"sv);
}
}
}
bool ViewImplementation::restore_pending_session_history_navigation(StringView reason)
{
if (!m_pending_session_history_navigation.has_value())
return false;
auto web_content_restore_mode = m_pending_session_history_navigation->web_content_restore_mode;
m_session_history = move(m_pending_session_history_navigation->previous_session_history);
m_pending_session_history_navigation.clear();
m_pending_session_history_traversal.clear();
if (auto* current_entry = m_session_history.current_entry()) {
auto current_url = current_entry->url;
auto const url_changed = m_url != current_url;
set_url(current_url);
if (url_changed && on_url_change)
on_url_change(m_url);
if (web_content_restore_mode == PendingSessionHistoryNavigation::WebContentRestoreMode::RestoreFromUIProcess) {
prepare_to_seed_web_content_session_history_from_ui_process();
m_should_suppress_history_for_current_load = false;
m_should_suppress_history_for_next_load = false;
m_webdriver_pending_navigation_url = current_url;
m_webdriver_pending_navigation_completes_with_session_history_update = true;
load_current_session_history_entry_from_ui_process();
} else {
m_session_history_entry_url_loading_from_ui_process.clear();
abandon_pending_web_content_session_history_seed();
m_current_web_content_session_history_matches_mirror = m_session_history.web_content_history_matches_mirror();
m_webdriver_pending_navigation_url.clear();
m_webdriver_pending_navigation_completes_with_session_history_update = false;
complete_webdriver_pending_navigation_if_url_matches(current_url);
}
} else {
m_current_web_content_session_history_matches_mirror = false;
}
update_navigation_action_state();
dump_session_history(reason);
return true;
}
void ViewImplementation::abandon_pending_web_content_session_history_seed()
{
m_session_history_entry_url_loading_from_ui_process.clear();
m_pending_web_content_session_history_seed.clear();
}
StringView ViewImplementation::pending_session_history_navigation_web_content_restore_mode_to_string(PendingSessionHistoryNavigation::WebContentRestoreMode mode)
{
switch (mode) {
case PendingSessionHistoryNavigation::WebContentRestoreMode::PreserveCurrentProcessState:
return "preserve-current-process-state"sv;
case PendingSessionHistoryNavigation::WebContentRestoreMode::RestoreFromUIProcess:
return "restore-from-ui-process"sv;
}
VERIFY_NOT_REACHED();
}
StringView ViewImplementation::pending_session_history_traversal_stage_to_string(PendingSessionHistoryTraversal::Stage stage)
{
switch (stage) {
case PendingSessionHistoryTraversal::Stage::ApplyingInWebContent:
return "applying-in-webcontent"sv;
case PendingSessionHistoryTraversal::Stage::CheckingCancelation:
return "checking-cancelation"sv;
case PendingSessionHistoryTraversal::Stage::LoadingEntryFromUIProcess:
return "loading-entry-from-ui-process"sv;
case PendingSessionHistoryTraversal::Stage::ReplacingWebContentProcess:
return "replacing-webcontent-process"sv;
case PendingSessionHistoryTraversal::Stage::RestoringNestedStepAfterSeed:
return "restoring-nested-step-after-seed"sv;
}
VERIFY_NOT_REACHED();
}
static Optional<size_t> current_top_level_history_entry_index_for_step(Vector<Web::HTML::SessionHistoryEntryDescriptor> const& entries, Optional<i32> current_step)
{
if (!current_step.has_value())
return {};
Optional<size_t> current_entry_index;
for (size_t i = 0; i < entries.size(); ++i) {
if (entries[i].step > *current_step)
break;
current_entry_index = i;
}
return current_entry_index;
}
void ViewImplementation::did_start_webdriver_navigation(Badge<WebContentClient>, URL::URL const& url)
{
m_webdriver_pending_navigation_url = url;
m_webdriver_pending_navigation_completes_with_session_history_update = false;
}
void ViewImplementation::wait_for_webdriver_navigation_completion(Badge<WebContentClient>, Optional<u64> page_load_timeout, Function<void(Web::WebDriver::Response)> on_complete)
{
if (!m_webdriver_pending_navigation_url.has_value()) {
on_complete(JsonValue {});
return;
}
auto request_id = m_next_webdriver_navigation_completion_request_id++;
auto request = make<WebDriverNavigationCompletionRequest>();
request->on_complete = move(on_complete);
m_pending_webdriver_navigation_completion_requests.set(request_id, move(request));
auto listener_id = add_navigation_listener({
.on_load_start = nullptr,
.on_load_finish = [this](URL::URL const& url) {
complete_webdriver_pending_navigation_if_url_matches(url);
},
});
m_pending_webdriver_navigation_completion_requests.get(request_id).value()->navigation_listener_id = listener_id;
if (page_load_timeout.has_value()) {
auto timer_interval = *page_load_timeout > NumericLimits<int>::max() ? NumericLimits<int>::max() : static_cast<int>(*page_load_timeout);
auto timer = Core::Timer::create_single_shot(timer_interval, [this, request_id] {
complete_webdriver_navigation_completion(request_id, Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::Timeout, "Navigation timed out"sv));
});
timer->start();
m_pending_webdriver_navigation_completion_requests.get(request_id).value()->timer = move(timer);
}
}
void ViewImplementation::complete_webdriver_navigation_completion(u64 request_id, Web::WebDriver::Response response)
{
auto maybe_request = m_pending_webdriver_navigation_completion_requests.take(request_id);
if (!maybe_request.has_value())
return;
auto request = maybe_request.release_value();
remove_navigation_listener(request->navigation_listener_id);
if (request->timer)
request->timer->stop();
request->on_complete(move(response));
}
void ViewImplementation::complete_webdriver_pending_navigation_if_url_matches(URL::URL const& url)
{
if (m_webdriver_pending_navigation_url.has_value() && *m_webdriver_pending_navigation_url != url)
return;
m_webdriver_pending_navigation_url.clear();
m_webdriver_pending_navigation_completes_with_session_history_update = false;
Vector<u64> request_ids;
request_ids.ensure_capacity(m_pending_webdriver_navigation_completion_requests.size());
for (auto const& request : m_pending_webdriver_navigation_completion_requests)
request_ids.unchecked_append(request.key);
auto view_id = m_view_id;
for (auto request_id : request_ids) {
Core::EventLoop::current().deferred_invoke([view_id, request_id] {
auto view = ViewImplementation::find_view_by_id(view_id);
if (!view.has_value())
return;
view->complete_webdriver_navigation_completion(request_id, JsonValue {});
});
}
}
JsonValue ViewImplementation::webdriver_session_history() const
{
JsonObject serialized;
serialized.set("currentURL"sv, m_url.serialize());
serialized.set("webContentProcessID"sv, client().pid());
serialized.set("backButtonEnabled"sv, m_navigate_back_action->enabled());
serialized.set("forwardButtonEnabled"sv, m_navigate_forward_action->enabled());
serialized.set("webContentHistoryMatchesUI"sv, m_current_web_content_session_history_matches_mirror);
serialized.set("loadingSessionHistoryEntryFromUI"sv, m_session_history_entry_url_loading_from_ui_process.has_value());
serialized.set("waitingToSeedWebContent"sv, m_pending_web_content_session_history_seed.should_send_entries);
serialized.set("waitingForWebContentSeedAck"sv, m_pending_web_content_session_history_seed.waiting_for_ack);
serialized.set("ignoringWebContentUpdatesUntilSeed"sv, m_pending_web_content_session_history_seed.ignore_updates_until_seed);
serialized.set("reseedAfterCurrentHistoryLoad"sv, m_pending_web_content_session_history_seed.should_reseed_after_current_history_load);
serialized.set("hasOnlyTopLevelUsedSteps"sv, m_session_history.has_only_top_level_used_steps());
serialized.set("webContentUsesUIStepCoordinates"sv, m_session_history.web_content_uses_ui_step_coordinates());
auto web_content_known_entries = m_session_history.web_content_known_entries();
auto web_content_known_used_steps = m_session_history.web_content_known_used_steps();
auto web_content_current_step = m_session_history.web_content_current_step();
Optional<size_t> web_content_current_step_index;
if (web_content_current_step.has_value())
web_content_current_step_index = web_content_known_used_steps.find_first_index(*web_content_current_step);
serialized.set("webContentKnownEntries"sv, history_json_entries(web_content_known_entries, current_top_level_history_entry_index_for_step(web_content_known_entries, web_content_current_step)));
serialized.set("webContentKnownUsedSteps"sv, history_json_steps(web_content_known_used_steps, web_content_current_step_index));
if (web_content_current_step.has_value())
serialized.set("webContentCurrentStep"sv, *web_content_current_step);
else
serialized.set("webContentCurrentStep"sv, JsonValue {});
if (auto current_used_step_index = m_session_history.current_used_step_index(); current_used_step_index.has_value())
serialized.set("currentUsedStepIndex"sv, *current_used_step_index);
else
serialized.set("currentUsedStepIndex"sv, JsonValue {});
if (auto pending_step = m_pending_web_content_session_history_seed.step_after_loading_top_level_entry; pending_step.has_value())
serialized.set("pendingWebContentHistoryStepAfterFallbackLoad"sv, *pending_step);
else
serialized.set("pendingWebContentHistoryStepAfterFallbackLoad"sv, JsonValue {});
if (m_pending_session_history_navigation.has_value()) {
JsonObject pending_navigation;
pending_navigation.set("url"sv, m_pending_session_history_navigation->url.serialize());
pending_navigation.set("webContentRestoreMode"sv, pending_session_history_navigation_web_content_restore_mode_to_string(m_pending_session_history_navigation->web_content_restore_mode));
if (auto const* previous_current_entry = m_pending_session_history_navigation->previous_session_history.current_entry())
pending_navigation.set("previousCurrentURL"sv, previous_current_entry->url.serialize());
else
pending_navigation.set("previousCurrentURL"sv, JsonValue {});
serialized.set("pendingSessionHistoryNavigation"sv, move(pending_navigation));
} else {
serialized.set("pendingSessionHistoryNavigation"sv, JsonValue {});
}
if (m_pending_session_history_traversal.has_value()) {
JsonObject pending_traversal;
pending_traversal.set("targetStep"sv, m_pending_session_history_traversal->target_step);
pending_traversal.set("targetStepIndex"sv, m_pending_session_history_traversal->target_step_index);
pending_traversal.set("willChangeTopLevelEntry"sv, m_pending_session_history_traversal->will_change_top_level_entry);
pending_traversal.set("willReplaceWebContentProcess"sv, m_pending_session_history_traversal->will_replace_web_content_process);
pending_traversal.set("stage"sv, pending_session_history_traversal_stage_to_string(m_pending_session_history_traversal->stage));
serialized.set("pendingSessionHistoryTraversal"sv, move(pending_traversal));
} else {
serialized.set("pendingSessionHistoryTraversal"sv, JsonValue {});
}
serialized.set("entries"sv, history_json_entries(m_session_history));
serialized.set("usedSteps"sv, history_json_steps(m_session_history));
return serialized;
}
String ViewImplementation::ui_process_session_history_for_testing(Badge<WebContentClient>) const
{
return webdriver_session_history().serialized();
}
void ViewImplementation::update_navigation_action_state()
{
m_navigate_back_action->set_enabled(m_session_history.can_go_back());
m_navigate_forward_action->set_enabled(m_session_history.can_go_forward());
}
void ViewImplementation::seed_web_content_session_history_from_ui_process(AllowCurrentEntryReconstruction allow_current_entry_reconstruction)
{
auto current_top_level_entry_index = m_session_history.current_top_level_entry_index();
if (!current_top_level_entry_index.has_value()) {
abandon_pending_web_content_session_history_seed();
m_current_web_content_session_history_matches_mirror = false;
m_session_history.forget_web_content_state();
update_navigation_action_state();
dump_session_history("skip-webcontent-session-history-seed-without-current-entry"sv);
return;
}
auto entries = m_session_history.entries();
if (entries.is_empty()) {
abandon_pending_web_content_session_history_seed();
m_current_web_content_session_history_matches_mirror = false;
m_session_history.forget_web_content_state();
update_navigation_action_state();
dump_session_history("skip-webcontent-session-history-seed-without-entries"sv);
return;
}
if (history_debug_enabled()) {
dbgln("[History] UI seeds WebContent session history page={} pid={} current={} entries={}",
page_id(),
client().pid(),
*current_top_level_entry_index,
history_log_entries(entries, current_top_level_entry_index));
}
// NB: A fallback traversal or crash recovery can restore the current top-level document before WebContent has the
// UI-owned session history that surrounds it. Allow WebContent to reconstruct that current entry only while the
// UI process is actively restoring that authoritative history, including the follow-up child navigable step for
// nested history. After crash recovery there might not be a pending traversal object anymore, but a pending
// nested step still means the top-level document is only a staging point for restoring the current history
// step.
auto is_restoring_traversal_target = m_pending_session_history_traversal.has_value()
&& (m_pending_session_history_traversal->stage == PendingSessionHistoryTraversal::Stage::LoadingEntryFromUIProcess
|| m_pending_session_history_traversal->stage == PendingSessionHistoryTraversal::Stage::ReplacingWebContentProcess
|| m_pending_session_history_traversal->stage == PendingSessionHistoryTraversal::Stage::RestoringNestedStepAfterSeed);
auto allow_reconstructing_current_entry = is_restoring_traversal_target
|| m_pending_web_content_session_history_seed.step_after_loading_top_level_entry.has_value()
|| allow_current_entry_reconstruction == AllowCurrentEntryReconstruction::Yes;
client().async_set_top_level_session_history(page_id(), move(entries), *current_top_level_entry_index, allow_reconstructing_current_entry);
m_pending_web_content_session_history_seed.waiting_for_ack = true;
m_pending_web_content_session_history_seed.should_send_entries = false;
update_navigation_action_state();
dump_session_history("sent-webcontent-session-history-seed"sv);
}
void ViewImplementation::prepare_to_seed_web_content_session_history_from_ui_process()
{
m_current_web_content_session_history_matches_mirror = false;
m_session_history.forget_web_content_state();
m_pending_session_history_navigation.clear();
m_pending_web_content_session_history_seed.clear();
// NB: A fresh or repaired WebContent process reaches the current top-level session history entry after loading or
// reseeding m_url. If the traversable's current session history step is nested, finish restoration by
// traversing to that step after seeding the top-level entries.
m_pending_web_content_session_history_seed.step_after_loading_top_level_entry = m_session_history.current_step_to_restore_after_loading_top_level_entry();
m_pending_web_content_session_history_seed.should_send_entries = true;
m_pending_web_content_session_history_seed.ignore_updates_until_seed = true;
}
void ViewImplementation::restore_current_session_history_entry_from_ui_process()
{
m_webdriver_pending_navigation_url = m_url;
if (!m_pending_web_content_session_history_seed.step_after_loading_top_level_entry.has_value()) {
m_pending_web_content_session_history_seed.should_reseed_after_current_history_load = true;
seed_web_content_session_history_from_ui_process();
}
load_current_session_history_entry_from_ui_process();
}
void ViewImplementation::load_current_session_history_entry_from_ui_process()
{
auto const* current_entry = m_session_history.current_entry();
if (!current_entry) {
m_session_history_entry_url_loading_from_ui_process = m_url;
client().async_load_url(page_id(), m_url, Web::Bindings::NavigationHistoryBehavior::Auto);
return;
}
m_session_history_entry_url_loading_from_ui_process = current_entry->url;
auto history_handling = m_pending_web_content_session_history_seed.waiting_for_ack || m_session_history.web_content_uses_ui_step_coordinates()
? Web::Bindings::NavigationHistoryBehavior::Replace
: Web::Bindings::NavigationHistoryBehavior::Auto;
client().async_load_url_with_document_resource(
page_id(),
current_entry->url,
current_entry->document_state.resource,
history_handling);
}
void ViewImplementation::load_session_history_traversal_target_from_ui_process(TraversableSessionHistory::TraversalTarget const& target, StringView dump_reason)
{
if (!m_pending_session_history_traversal.has_value() || m_pending_session_history_traversal->target_step != target.target_step) {
m_pending_session_history_traversal = PendingSessionHistoryTraversal {
.target_step = target.target_step,
.target_step_index = target.target_step_index,
.will_change_top_level_entry = target.changes_top_level_entry,
.will_replace_web_content_process = !is_url_suitable_for_same_process_navigation(m_url, target.target_top_level_entry->url),
.stage = PendingSessionHistoryTraversal::Stage::LoadingEntryFromUIProcess,
.on_cancelation_check_complete = nullptr,
};
} else {
m_pending_session_history_traversal->stage = PendingSessionHistoryTraversal::Stage::LoadingEntryFromUIProcess;
}
auto previous_session_history = m_session_history;
m_session_history.traverse_to(target.target_step_index);
update_navigation_action_state();
prepare_to_seed_web_content_session_history_from_ui_process();
m_pending_session_history_navigation = PendingSessionHistoryNavigation {
target.target_top_level_entry->url,
move(previous_session_history),
};
m_webdriver_pending_navigation_url = target.target_top_level_entry->url;
// NB: A UI-process fallback traversal is only fully observable once the replacement WebContent process has
// accepted the UI-owned history seed. Completing WebDriver at load finish would let tests, and callers doing
// immediate history inspection, observe the fresh process before it has consumed the authoritative history.
m_webdriver_pending_navigation_completes_with_session_history_update = true;
set_url(target.target_top_level_entry->url);
dump_session_history(dump_reason);
load_current_session_history_entry_from_ui_process();
}
NonnullRefPtr<Core::Promise<Empty>> ViewImplementation::reset_session_history_for_testing()
{
m_pending_session_history_reset_for_testing = Core::Promise<Empty>::construct();
client().async_reset_session_history_for_testing(page_id());
return *m_pending_session_history_reset_for_testing;
}
void ViewImplementation::did_set_top_level_session_history(Badge<WebContentClient>, bool accepted, Vector<Web::HTML::SessionHistoryEntryDescriptor> entries, Vector<i32> used_steps, size_t current_used_step_index)
{
if (!should_manage_session_history_in_ui_process())
return;
if (history_debug_enabled()) {
dbgln("[History] UI received WebContent session history seed ack page={} pid={} accepted={} current_used_step={} entries={} used_steps={}",
page_id(),
client().pid(),
accepted,
current_used_step_index,
history_log_entries(entries),
history_log_steps(used_steps, current_used_step_index));
}
if (!m_pending_web_content_session_history_seed.waiting_for_ack) {
dump_session_history("ignored-webcontent-session-history-seed-ack"sv);
return;
}
if (!accepted) {
// NB: WebContent can reject a stale UI-process seed without changing its live session history. In that case
// the ack carries WebContent's current snapshot, so converge the UI mirror from the same path used by
// normal WebContent history updates. If the snapshot itself is not usable, fall back to forgetting the
// WebContent state below and wait for the next repair point.
if (adopt_web_content_session_history_after_rejected_seed(move(entries), move(used_steps), current_used_step_index)) {
dump_session_history("webcontent-session-history-seed-rejected-with-current-snapshot"sv);
return;
}
abandon_pending_web_content_session_history_seed();
m_current_web_content_session_history_matches_mirror = false;
m_session_history.forget_web_content_state();
m_pending_session_history_traversal.clear();
update_navigation_action_state();
dump_session_history("webcontent-session-history-seed-rejected"sv);
return;
}
if (!m_session_history.did_seed_web_content_from_ui_process(move(entries), move(used_steps), current_used_step_index)) {
if (m_pending_web_content_session_history_seed.should_reseed_after_current_history_load) {
m_pending_web_content_session_history_seed.waiting_for_ack = false;
m_pending_web_content_session_history_seed.should_send_entries = true;
m_pending_web_content_session_history_seed.ignore_updates_until_seed = true;
m_current_web_content_session_history_matches_mirror = false;
update_navigation_action_state();
dump_session_history("webcontent-session-history-preload-seed-ack-mismatch"sv);
return;
}
abandon_pending_web_content_session_history_seed();
m_current_web_content_session_history_matches_mirror = false;
m_session_history.forget_web_content_state();
m_pending_session_history_traversal.clear();
update_navigation_action_state();
dump_session_history("webcontent-session-history-seed-ack-mismatch"sv);
return;
}
m_pending_web_content_session_history_seed.waiting_for_ack = false;
if (m_pending_web_content_session_history_seed.should_reseed_after_current_history_load) {
m_pending_web_content_session_history_seed.should_send_entries = true;
m_pending_web_content_session_history_seed.ignore_updates_until_seed = true;
m_current_web_content_session_history_matches_mirror = false;
update_navigation_action_state();
dump_session_history("webcontent-session-history-preload-seed-ack"sv);
return;
}
m_pending_web_content_session_history_seed.ignore_updates_until_seed = false;
// NB: A seed ack can arrive while a top-level navigation is still blocked. Keep the mirror provisional until that
// navigation settles.
m_current_web_content_session_history_matches_mirror = !m_pending_web_content_session_history_seed.step_after_loading_top_level_entry.has_value()
&& !m_pending_session_history_navigation.has_value();
if (m_pending_web_content_session_history_seed.step_after_loading_top_level_entry.has_value()) {
if (m_pending_session_history_traversal.has_value())
m_pending_session_history_traversal->stage = PendingSessionHistoryTraversal::Stage::RestoringNestedStepAfterSeed;
client().async_traverse_the_history_to_step(page_id(), *m_pending_web_content_session_history_seed.step_after_loading_top_level_entry);
} else {
auto is_waiting_for_history_step_cancelation_check = m_pending_session_history_traversal.has_value()
&& m_pending_session_history_traversal->stage == PendingSessionHistoryTraversal::Stage::CheckingCancelation;
if (!is_waiting_for_history_step_cancelation_check) {
m_pending_session_history_traversal.clear();
if (!m_pending_session_history_navigation.has_value())
complete_webdriver_pending_navigation_if_url_matches(m_url);
}
}
update_navigation_action_state();
dump_session_history("webcontent-session-history-seed-ack"sv);
}
void ViewImplementation::did_traverse_the_history_to_step(Badge<WebContentClient>, i32 step, bool step_was_available, Web::HTML::HistoryStepResult result)
{
if (!should_manage_session_history_in_ui_process())
return;
if (!m_pending_web_content_session_history_seed.step_after_loading_top_level_entry.has_value()) {
if (!m_pending_session_history_traversal.has_value() || m_pending_session_history_traversal->target_step != step) {
dump_session_history("ignored-stale-webcontent-history-step-result"sv);
return;
}
if (!step_was_available) {
auto target = m_session_history.traversal_target_for_step(step);
if (target.has_value()) {
load_session_history_traversal_target_from_ui_process(*target, "webcontent-history-step-unavailable-fallback-load"sv);
return;
}
m_current_web_content_session_history_matches_mirror = false;
m_session_history.forget_web_content_state();
m_pending_session_history_traversal.clear();
update_navigation_action_state();
dump_session_history("webcontent-history-step-unavailable"sv);
return;
}
if (result != Web::HTML::HistoryStepResult::Applied) {
if (m_webdriver_pending_navigation_url.has_value())
m_webdriver_pending_navigation_url = m_url;
m_webdriver_pending_navigation_completes_with_session_history_update = false;
complete_webdriver_pending_navigation_if_url_matches(m_url);
m_pending_session_history_traversal.clear();
update_navigation_action_state();
dump_session_history("webcontent-history-step-canceled"sv);
} else {
if (!m_session_history.did_apply_web_content_traversal_to_step(step)) {
if (auto target = m_session_history.traversal_target_for_step(step); target.has_value()) {
load_session_history_traversal_target_from_ui_process(*target, "webcontent-history-step-applied-with-stale-mirror-fallback-load"sv);
return;
}
m_current_web_content_session_history_matches_mirror = false;
m_session_history.forget_web_content_state();
m_pending_session_history_traversal.clear();
update_navigation_action_state();
dump_session_history("webcontent-history-step-applied-without-ui-target"sv);
return;
}
m_current_web_content_session_history_matches_mirror = true;
if (auto const* current_entry = m_session_history.current_entry()) {
auto current_url = current_entry->url;
auto const url_changed = m_url != current_url;
set_url(current_url);
if (url_changed && on_url_change)
on_url_change(m_url);
if (m_webdriver_pending_navigation_url.has_value() && *m_webdriver_pending_navigation_url != current_url)
m_webdriver_pending_navigation_url = current_url;
}
if (!m_pending_session_history_traversal->will_change_top_level_entry)
complete_webdriver_pending_navigation_if_url_matches(m_url);
m_pending_session_history_traversal.clear();
update_navigation_action_state();
dump_session_history("webcontent-history-step-applied"sv);
}
return;
}
if (*m_pending_web_content_session_history_seed.step_after_loading_top_level_entry != step) {
dump_session_history("ignored-stale-webcontent-history-step-result"sv);
return;
}
if (step_was_available && result == Web::HTML::HistoryStepResult::Applied) {
m_pending_web_content_session_history_seed.step_after_loading_top_level_entry.clear();
m_current_web_content_session_history_matches_mirror = m_session_history.did_restore_web_content_to_current_step(step);
m_pending_session_history_traversal.clear();
complete_webdriver_pending_navigation_if_url_matches(m_url);
update_navigation_action_state();
dump_session_history("webcontent-history-step-restored"sv);
} else {
auto reason = step_was_available ? "webcontent-pending-history-step-canceled"sv : "webcontent-history-step-unavailable"sv;
if (restore_pending_session_history_navigation(reason))
return;
m_pending_web_content_session_history_seed.step_after_loading_top_level_entry.clear();
m_current_web_content_session_history_matches_mirror = false;
m_session_history.forget_web_content_state();
m_pending_session_history_traversal.clear();
update_navigation_action_state();
dump_session_history(reason);
}
}
void ViewImplementation::did_check_if_traverse_history_step_is_canceled(
Badge<WebContentClient>, u64 request_id, i32 step, bool canceled)
{
if (!should_manage_session_history_in_ui_process())
return;
if (!m_pending_session_history_traversal.has_value()
|| m_pending_session_history_traversal->stage != PendingSessionHistoryTraversal::Stage::CheckingCancelation
|| m_pending_session_history_traversal->cancelation_check_request_id != request_id
|| m_pending_session_history_traversal->target_step != step) {
dump_session_history("ignored-stale-history-step-cancelation-check-result"sv);
return;
}
if (canceled) {
auto on_cancelation_check_complete = move(m_pending_session_history_traversal->on_cancelation_check_complete);
if (m_webdriver_pending_navigation_url.has_value())
m_webdriver_pending_navigation_url = m_url;
m_webdriver_pending_navigation_completes_with_session_history_update = false;
complete_webdriver_pending_navigation_if_url_matches(m_url);
m_pending_session_history_traversal.clear();
update_navigation_action_state();
dump_session_history("traverse-fallback-canceled-by-webcontent"sv);
if (on_cancelation_check_complete)
on_cancelation_check_complete({
.status = HistoryTraversalStatus::Canceled,
});
return;
}
auto target = m_session_history.traversal_target_for_step(step);
if (!target.has_value()) {
auto on_cancelation_check_complete = move(m_pending_session_history_traversal->on_cancelation_check_complete);
m_current_web_content_session_history_matches_mirror = false;
m_session_history.forget_web_content_state();
m_pending_session_history_traversal.clear();
update_navigation_action_state();
dump_session_history("traverse-fallback-cancelation-check-without-ui-target"sv);
if (on_cancelation_check_complete)
on_cancelation_check_complete({
.status = HistoryTraversalStatus::NoEntry,
});
return;
}
auto on_cancelation_check_complete = move(m_pending_session_history_traversal->on_cancelation_check_complete);
if (on_cancelation_check_complete)
on_cancelation_check_complete({
.status = HistoryTraversalStatus::Started,
.will_replace_web_content_process = m_pending_session_history_traversal->will_replace_web_content_process,
.will_change_top_level_entry = m_pending_session_history_traversal->will_change_top_level_entry,
});
load_session_history_traversal_target_from_ui_process(*target, "traverse-fallback-load-after-cancelation-check"sv);
}
void ViewImplementation::did_reset_session_history_for_testing(Badge<WebContentClient>)
{
auto promise = move(m_pending_session_history_reset_for_testing);
m_session_history.clear();
m_current_web_content_session_history_matches_mirror = false;
m_pending_session_history_navigation.clear();
m_pending_session_history_traversal.clear();
m_session_history_entry_url_loading_from_ui_process.clear();
abandon_pending_web_content_session_history_seed();
m_webdriver_pending_navigation_url.clear();
m_webdriver_pending_navigation_completes_with_session_history_update = false;
update_navigation_action_state();
if (promise)
promise->resolve({});
}
void ViewImplementation::mark_web_content_session_history_stale_for_testing(Badge<WebContentClient>)
{
m_current_web_content_session_history_matches_mirror = false;
update_navigation_action_state();
dump_session_history("marked-webcontent-session-history-stale-for-testing"sv);
}
void ViewImplementation::dump_session_history(StringView reason, SessionHistoryDumpMode mode) const
{
if (mode == SessionHistoryDumpMode::IfDebuggingEnabled && !history_debug_enabled())
return;
auto web_content_known_used_steps = m_session_history.web_content_known_used_steps();
auto web_content_current_step = m_session_history.web_content_current_step();
Optional<size_t> web_content_current_step_index;
if (web_content_current_step.has_value())
web_content_current_step_index = web_content_known_used_steps.find_first_index(*web_content_current_step);
auto web_content_known_entries = m_session_history.web_content_known_entries();
auto web_content_current_top_level_entry_index = current_top_level_history_entry_index_for_step(web_content_known_entries, web_content_current_step);
auto pending_navigation_url = "none"sv;
auto pending_navigation_restore_mode = "none"sv;
String pending_navigation_url_storage;
if (m_pending_session_history_navigation.has_value()) {
pending_navigation_url_storage = m_pending_session_history_navigation->url.serialize();
pending_navigation_url = pending_navigation_url_storage.bytes_as_string_view();
pending_navigation_restore_mode = pending_session_history_navigation_web_content_restore_mode_to_string(m_pending_session_history_navigation->web_content_restore_mode);
}
dbgln("[History] UI session history page={} pid={} reason={} url='{}' webcontent_matches={} webcontent_uses_ui_steps={} loading_from_ui={} waiting_to_seed={} waiting_for_seed_ack={} ignore_until_seed={} reseed_after_current_load={} pending_webcontent_step={} pending_navigation_url={} pending_navigation_restore={} pending_traversal_target={} pending_traversal_stage={} webcontent_current_step={} back={} forward={} entries={} webcontent_known_entries={} webcontent_known_used_steps={}",
page_id(),
client().pid(),
reason,
m_url,
m_current_web_content_session_history_matches_mirror,
m_session_history.web_content_uses_ui_step_coordinates(),
m_session_history_entry_url_loading_from_ui_process.has_value(),
m_pending_web_content_session_history_seed.should_send_entries,
m_pending_web_content_session_history_seed.waiting_for_ack,
m_pending_web_content_session_history_seed.ignore_updates_until_seed,
m_pending_web_content_session_history_seed.should_reseed_after_current_history_load,
m_pending_web_content_session_history_seed.step_after_loading_top_level_entry,
pending_navigation_url,
pending_navigation_restore_mode,
m_pending_session_history_traversal.has_value() ? Optional<i32> { m_pending_session_history_traversal->target_step } : Optional<i32> {},
m_pending_session_history_traversal.has_value() ? pending_session_history_traversal_stage_to_string(m_pending_session_history_traversal->stage) : "none"sv,
web_content_current_step,
m_navigate_back_action->enabled(),
m_navigate_forward_action->enabled(),
history_log_entries(m_session_history),
history_log_entries(web_content_known_entries, web_content_current_top_level_entry_index),
history_log_steps(web_content_known_used_steps, web_content_current_step_index));
}
void ViewImplementation::handle_web_content_process_crash(LoadErrorPage load_error_page)
{
auto const headless_mode = Application::browser_options().headless_mode.has_value();
if (!headless_mode) {
dbgln("\033[31;1mWebContent process crashed!\033[0m Last page loaded: {}", m_url);
dbgln("Consider raising an issue at https://github.com/LadybirdBrowser/ladybird/issues/new/choose");
}
++m_crash_count;
constexpr size_t max_reasonable_crash_count = 5U;
if (m_crash_count >= max_reasonable_crash_count) {
if (!headless_mode) {
dbgln("WebContent has crashed {} times in quick succession! Not restarting...", m_crash_count);
m_repeated_crash_timer->stop();
return;
}
// In headless mode, always respawn - tests need a working WebContent for each test.
// Reset the crash count so we can continue running tests.
m_crash_count = 0;
}
m_repeated_crash_timer->restart();
// In headless mode, respawn WebContent but skip the error page.
if (headless_mode)
load_error_page = LoadErrorPage::No;
initialize_client();
VERIFY(m_client_state.client);
// Don't keep a stale backup bitmap around.
m_backup_shared_image_buffer = nullptr;
if (should_manage_session_history_in_ui_process()) {
m_session_history_entry_url_loading_from_ui_process.clear();
prepare_to_seed_web_content_session_history_from_ui_process();
}
handle_resize();
if (load_error_page == LoadErrorPage::Yes) {
auto escaped_url = escape_html_entities(m_url.serialize());
StringBuilder builder;
builder.appendff(ERROR_HTML_HEADER, NO_FALLBACK_FAVICON_LINK, CRASH_ERROR_SVG, "Ladybird flew off-course!"sv);
2025-03-26 18:46:48 -03:00
builder.appendff("<p>The web page <a href=\"{}\">{}</a> has crashed.<br><br>You can reload the page to try again.</p>", escaped_url, escaped_url);
builder.append(ERROR_HTML_FOOTER);
load_crash_page_html(builder.string_view(), m_url);
} else if (should_manage_session_history_in_ui_process()) {
m_should_suppress_history_for_current_load = false;
m_should_suppress_history_for_next_load = false;
restore_current_session_history_entry_from_ui_process();
}
}
void ViewImplementation::default_zoom_level_factor_changed()
{
apply_zoom_for_current_host();
}
void ViewImplementation::zoom_per_host_changed(StringView host)
{
if (current_host() != host)
return;
apply_zoom_for_current_host();
}
void ViewImplementation::languages_changed()
{
auto const& languages = Application::settings().languages();
client().async_set_preferred_languages(page_id(), languages);
}
void ViewImplementation::browsing_behavior_changed()
{
auto const& browsing_behavior = Application::settings().browsing_behavior();
client().async_set_browsing_behavior(page_id(), browsing_behavior);
}
void ViewImplementation::autoplay_settings_changed()
{
auto const& autoplay_settings = Application::settings().autoplay_settings();
auto const& web_content_options = Application::web_content_options();
if (autoplay_settings.enabled_globally || web_content_options.enable_autoplay == EnableAutoplay::Yes)
client().async_set_autoplay_allowed_on_all_websites(page_id());
else
client().async_set_autoplay_allowlist(page_id(), autoplay_settings.site_filters.values());
}
void ViewImplementation::global_privacy_control_changed()
{
auto global_privacy_control = Application::settings().global_privacy_control();
client().async_set_enable_global_privacy_control(page_id(), global_privacy_control == GlobalPrivacyControl::Yes);
}
void ViewImplementation::bookmarks_changed()
{
update_bookmark_action();
}
void ViewImplementation::update_bookmark_action()
{
Application::the().update_bookmark_action_for_current_web_view();
auto is_bookmarked = Application::bookmark_store().is_bookmarked(url());
m_toggle_bookmark_action->set_tooltip(is_bookmarked ? "Remove bookmark"sv : "Bookmark this page"sv);
m_toggle_bookmark_action->set_engaged(is_bookmarked);
}
static ErrorOr<LexicalPath> save_screenshot(Gfx::Bitmap const* bitmap)
{
if (!bitmap)
return Error::from_string_literal("Failed to take a screenshot");
auto path = TRY([] -> ErrorOr<LexicalPath> {
if (auto const& screenshot_path = Application::browser_options().screenshot_path; screenshot_path.has_value())
return LexicalPath { *screenshot_path };
auto file = AK::UnixDateTime::now().to_byte_string("screenshot-%Y-%m-%d-%H-%M-%S.png"sv);
return Application::the().path_for_downloaded_file(file);
}());
auto encoded = TRY(Gfx::PNGWriter::encode(*bitmap));
auto dump_file = TRY(Core::File::open(path.string(), Core::File::OpenMode::Write));
TRY(dump_file->write_until_depleted(encoded));
return path;
}
NonnullRefPtr<Core::Promise<LexicalPath>> ViewImplementation::take_screenshot(ScreenshotType type)
{
auto promise = Core::Promise<LexicalPath>::construct();
if (m_pending_screenshot) {
2025-04-07 07:46:22 -03:00
// For simplicity, only allow taking one screenshot at a time for now. Revisit if we need
// to allow spamming screenshot requests for some reason.
promise->reject(Error::from_string_literal("A screenshot request is already in progress"));
return promise;
}
switch (type) {
case ScreenshotType::Visible: {
Gfx::Bitmap const* visible_bitmap = nullptr;
if (m_client_state.has_usable_bitmap) {
VERIFY(m_client_state.front_bitmap.shared_image_buffer);
visible_bitmap = m_client_state.front_bitmap.shared_image_buffer->bitmap().ptr();
} else if (m_backup_shared_image_buffer) {
visible_bitmap = m_backup_shared_image_buffer->bitmap().ptr();
}
if (visible_bitmap) {
if (auto result = save_screenshot(visible_bitmap); result.is_error())
promise->reject(result.release_error());
else
promise->resolve(result.release_value());
}
break;
}
case ScreenshotType::Full:
m_pending_screenshot = promise;
client().async_take_document_screenshot(page_id());
break;
}
return promise;
}
NonnullRefPtr<Core::Promise<LexicalPath>> ViewImplementation::take_dom_node_screenshot(Web::UniqueNodeID node_id)
{
auto promise = Core::Promise<LexicalPath>::construct();
if (m_pending_screenshot) {
2025-04-07 07:46:22 -03:00
// For simplicity, only allow taking one screenshot at a time for now. Revisit if we need
// to allow spamming screenshot requests for some reason.
promise->reject(Error::from_string_literal("A screenshot request is already in progress"));
return promise;
}
m_pending_screenshot = promise;
client().async_take_dom_node_screenshot(page_id(), node_id);
return promise;
}
void ViewImplementation::did_receive_screenshot(Badge<WebContentClient>, Gfx::ShareableBitmap const& screenshot)
{
VERIFY(m_pending_screenshot);
if (auto result = save_screenshot(screenshot.bitmap()); result.is_error())
m_pending_screenshot->reject(result.release_error());
else
m_pending_screenshot->resolve(result.release_value());
m_pending_screenshot = nullptr;
}
NonnullRefPtr<Core::Promise<String>> ViewImplementation::request_internal_page_info(PageInfoType type)
{
auto promise = Core::Promise<String>::construct();
if (m_pending_info_request) {
2025-04-07 07:46:22 -03:00
// For simplicity, only allow one info request at a time for now.
promise->reject(Error::from_string_literal("A page info request is already in progress"));
return promise;
}
m_pending_info_request = promise;
client().async_request_internal_page_info(page_id(), type);
return promise;
}
void ViewImplementation::did_receive_internal_page_info(Badge<WebContentClient>, PageInfoType, Optional<Core::AnonymousBuffer> const& info)
{
VERIFY(m_pending_info_request);
String info_string;
if (!info.has_value()) {
info_string = "(no page)"_string;
} else {
info_string = MUST(String::from_utf8(info->bytes()));
}
m_pending_info_request->resolve(move(info_string));
m_pending_info_request = nullptr;
}
ErrorOr<LexicalPath> ViewImplementation::dump_gc_graph()
{
auto promise = request_internal_page_info(PageInfoType::GCGraph);
auto gc_graph_json = TRY(promise->await());
LexicalPath path { Core::StandardPaths::tempfile_directory() };
path = path.append(TRY(AK::UnixDateTime::now().to_string("gc-graph-%Y-%m-%d-%H-%M-%S.js"sv)));
// Write as a .js file so gc-heap-explorer.html can load it via <script> tag (avoiding CORS issues with file:// URLs)
auto dump_file = TRY(Core::File::open(path.string(), Core::File::OpenMode::Write));
TRY(dump_file->write_until_depleted("var GC_GRAPH_DUMP = "sv.bytes()));
TRY(dump_file->write_until_depleted(gc_graph_json.bytes()));
TRY(dump_file->write_until_depleted(";\n"sv.bytes()));
return path;
}
void ViewImplementation::set_user_style_sheet(String const& source)
{
client().async_set_user_style(page_id(), source);
}
void ViewImplementation::initialize_context_menus()
{
auto& application = Application::the();
m_navigate_back_action = Action::create("Go Back"sv, ActionID::NavigateBack, [this]() {
(void)traverse_the_history_by_delta(-1);
});
m_navigate_forward_action = Action::create("Go Forward"sv, ActionID::NavigateForward, [this]() {
(void)traverse_the_history_by_delta(+1);
});
m_navigate_back_action->set_enabled(false);
m_navigate_forward_action->set_enabled(false);
m_toggle_bookmark_action = Action::create("Toggle Bookmark"sv, ActionID::ToggleBookmarkViaToolbar, [this]() {
auto& bookmark_store = Application::bookmark_store();
if (auto bookmark = bookmark_store.find_bookmark_by_url(url()); bookmark.has_value())
bookmark_store.remove_item(bookmark->id);
else
bookmark_store.add_bookmark(url(), title().to_utf8(), favicon_base64_png());
});
update_bookmark_action();
2025-09-10 08:23:15 -03:00
m_reset_zoom_action = Action::create("100%"sv, ActionID::ResetZoomViaToolbar, [this]() {
reset_zoom();
});
m_reset_zoom_action->set_tooltip("Reset zoom level"sv);
m_reset_zoom_action->set_visible(false);
m_search_selected_text_action = Action::create("Search Selected Text"sv, ActionID::SearchSelectedText, [this]() {
auto const& search_engine = Application::settings().search_engine();
if (!search_engine.has_value())
return;
auto url_string = search_engine->format_search_query_for_navigation(*m_search_text);
auto url = URL::Parser::basic_parse(url_string);
VERIFY(url.has_value());
Application::the().open_url_in_new_tab(*url, Web::HTML::ActivateTab::Yes);
});
m_search_selected_text_action->set_visible(false);
auto take_and_save_screenshot = [this](auto type) {
take_screenshot(type)
->when_resolved([](auto const& path) {
Application::the().display_download_confirmation_dialog("Screenshot"sv, path);
})
.when_rejected([](auto const& error) {
if (error.is_errno() && error.code() == ECANCELED)
return;
auto error_message = MUST(String::formatted("{}", error));
Application::the().display_error_dialog(error_message);
});
};
m_take_visible_screenshot_action = Action::create("Take Visible Screenshot"sv, ActionID::TakeVisibleScreenshot, [take_and_save_screenshot]() {
take_and_save_screenshot(ScreenshotType::Visible);
});
m_take_full_screenshot_action = Action::create("Take Full Screenshot"sv, ActionID::TakeFullScreenshot, [take_and_save_screenshot]() {
take_and_save_screenshot(ScreenshotType::Full);
});
m_open_in_new_tab_action = Action::create("Open in New Tab"sv, ActionID::OpenInNewTab, [this]() {
Application::the().open_url_in_new_tab(m_context_menu_url, Web::HTML::ActivateTab::No);
});
m_open_in_new_window_action = Action::create("Open in New Window"sv, ActionID::OpenInNewWindow, [this]() {
Application::the().open_url_in_new_window(m_context_menu_url);
});
m_copy_url_action = Action::create("Copy URL"sv, ActionID::CopyURL, [this]() {
Application::the().insert_clipboard_entry({ url_text_to_copy(m_context_menu_url), "text/plain"_string });
});
m_open_image_action = Action::create("Open Image"sv, ActionID::OpenImage, [this]() {
load(m_context_menu_url);
});
m_save_image_action = Action::create("Save Image As..."sv, ActionID::SaveImage, [this]() {
auto download_path = Application::the().path_for_downloaded_file(m_context_menu_url.basename());
if (download_path.is_error())
return;
Application::the().file_downloader().download_file(m_context_menu_url, download_path.release_value());
});
m_copy_image_action = Action::create("Copy Image"sv, ActionID::CopyImage, [this]() {
if (!m_image_context_menu_bitmap.has_value())
return;
auto bitmap = m_image_context_menu_bitmap.release_value();
if (!bitmap.is_valid())
return;
auto encoded = Gfx::PNGWriter::encode(*bitmap.bitmap());
if (encoded.is_error())
return;
Application::the().insert_clipboard_entry({ ByteString { encoded.value().bytes() }, "image/png"_string });
});
m_open_audio_action = Action::create("Open Audio"sv, ActionID::OpenAudio, [this]() {
load(m_context_menu_url);
});
m_open_video_action = Action::create("Open Video"sv, ActionID::OpenVideo, [this]() {
load(m_context_menu_url);
});
m_media_play_action = Action::create("Play"sv, ActionID::PlayMedia, [this]() {
client().async_toggle_media_play_state(page_id());
});
m_media_pause_action = Action::create("Pause"sv, ActionID::PauseMedia, [this]() {
client().async_toggle_media_play_state(page_id());
});
m_media_mute_action = Action::create("Mute"sv, ActionID::MuteMedia, [this]() {
client().async_toggle_media_mute_state(page_id());
});
m_media_unmute_action = Action::create("Unmute"sv, ActionID::UnmuteMedia, [this]() {
client().async_toggle_media_mute_state(page_id());
});
m_media_show_controls_action = Action::create("Show Controls"sv, ActionID::ShowControls, [this]() {
client().async_toggle_media_controls_state(page_id());
});
m_media_hide_controls_action = Action::create("Hide Controls"sv, ActionID::HideControls, [this]() {
client().async_toggle_media_controls_state(page_id());
});
m_media_loop_action = Action::create_checkable("Loop"sv, ActionID::ToggleMediaLoopState, [this]() {
client().async_toggle_media_loop_state(page_id());
});
m_media_enter_fullscreen_action = Action::create("Full Screen"sv, ActionID::EnterFullscreen, [this]() {
client().async_toggle_media_fullscreen_state(page_id());
});
m_media_exit_fullscreen_action = Action::create("Exit Full Screen"sv, ActionID::ExitFullscreen, [this]() {
client().async_toggle_media_fullscreen_state(page_id());
});
m_page_context_menu = Menu::create("Page Context Menu"sv);
m_page_context_menu->add_action(*m_navigate_back_action);
m_page_context_menu->add_action(*m_navigate_forward_action);
m_page_context_menu->add_action(application.reload_action());
m_page_context_menu->add_separator();
m_page_context_menu->add_action(application.cut_selection_action());
m_page_context_menu->add_action(application.copy_selection_action());
m_page_context_menu->add_action(application.paste_action());
m_page_context_menu->add_action(application.select_all_action());
m_page_context_menu->add_separator();
m_page_context_menu->add_action(*m_search_selected_text_action);
m_page_context_menu->add_separator();
m_page_context_menu->add_action(*m_take_visible_screenshot_action);
m_page_context_menu->add_action(*m_take_full_screenshot_action);
m_page_context_menu->add_separator();
m_page_context_menu->add_action(application.view_source_action());
m_link_context_menu = Menu::create("Link Context Menu"sv);
m_link_context_menu->add_action(*m_open_in_new_tab_action);
m_link_context_menu->add_action(*m_open_in_new_window_action);
m_link_context_menu->add_action(*m_copy_url_action);
m_image_context_menu = Menu::create("Image Context Menu"sv);
m_image_context_menu->add_action(*m_open_image_action);
m_image_context_menu->add_action(*m_open_in_new_tab_action);
m_image_context_menu->add_separator();
m_image_context_menu->add_action(*m_save_image_action);
m_image_context_menu->add_separator();
m_image_context_menu->add_action(*m_copy_image_action);
m_image_context_menu->add_action(*m_copy_url_action);
m_media_context_menu = Menu::create("Media Context Menu"sv);
m_media_context_menu->add_action(*m_media_play_action);
m_media_context_menu->add_action(*m_media_pause_action);
m_media_context_menu->add_action(*m_media_mute_action);
m_media_context_menu->add_action(*m_media_unmute_action);
m_media_context_menu->add_action(*m_media_show_controls_action);
m_media_context_menu->add_action(*m_media_hide_controls_action);
m_media_context_menu->add_action(*m_media_loop_action);
m_media_context_menu->add_action(*m_media_enter_fullscreen_action);
m_media_context_menu->add_action(*m_media_exit_fullscreen_action);
m_media_context_menu->add_separator();
m_media_context_menu->add_action(*m_open_audio_action);
m_media_context_menu->add_action(*m_open_video_action);
m_media_context_menu->add_action(*m_open_in_new_tab_action);
m_media_context_menu->add_separator();
m_media_context_menu->add_action(*m_copy_url_action);
}
void ViewImplementation::did_request_page_context_menu(Badge<WebContentClient>, Gfx::IntPoint content_position, Web::ContextMenuForInputEventsTarget for_input_events_target)
{
auto& cut_selection_action = Application::the().cut_selection_action();
cut_selection_action.set_visible(for_input_events_target == Web::ContextMenuForInputEventsTarget::Yes);
auto const& search_engine = Application::settings().search_engine();
m_search_text = search_engine.has_value() ? selected_text_with_whitespace_collapsed() : OptionalNone {};
ScopeGuard guard { [&]() {
cut_selection_action.set_visible(true);
m_search_text.clear();
} };
if (m_search_text.has_value()) {
m_search_selected_text_action->set_text(search_engine->format_search_query_for_display(*m_search_text));
m_search_selected_text_action->set_visible(true);
} else {
m_search_selected_text_action->set_visible(false);
}
if (m_page_context_menu->on_activation)
m_page_context_menu->on_activation(to_widget_position(content_position));
}
void ViewImplementation::did_request_link_context_menu(Badge<WebContentClient>, Gfx::IntPoint content_position, URL::URL url)
{
m_context_menu_url = move(url);
m_open_in_new_tab_action->set_text("Open in New Tab"sv);
switch (url_type(m_context_menu_url)) {
case URLType::Email:
m_copy_url_action->set_text("Copy Email Address"sv);
break;
case URLType::Telephone:
m_copy_url_action->set_text("Copy Phone Number"sv);
break;
case URLType::Other:
m_copy_url_action->set_text("Copy Link Address"sv);
break;
}
if (m_link_context_menu->on_activation)
m_link_context_menu->on_activation(to_widget_position(content_position));
}
void ViewImplementation::did_request_image_context_menu(Badge<WebContentClient>, Gfx::IntPoint content_position, URL::URL url, Optional<Gfx::ShareableBitmap> bitmap)
{
m_context_menu_url = move(url);
m_image_context_menu_bitmap = move(bitmap);
m_open_in_new_tab_action->set_text("Open Image in New Tab"sv);
m_copy_url_action->set_text("Copy Image URL"sv);
m_copy_image_action->set_enabled(m_image_context_menu_bitmap.has_value());
if (m_image_context_menu->on_activation)
m_image_context_menu->on_activation(to_widget_position(content_position));
}
void ViewImplementation::did_request_media_context_menu(Badge<WebContentClient>, Gfx::IntPoint content_position, Web::Page::MediaContextMenu menu)
{
m_context_menu_url = move(menu.media_url);
m_open_in_new_tab_action->set_text(menu.is_video ? "Open Video in New Tab"sv : "Open Audio in new Tab"sv);
m_copy_url_action->set_text(menu.is_video ? "Copy Video URL"sv : "Copy Audio URL"sv);
m_open_audio_action->set_visible(!menu.is_video);
m_open_video_action->set_visible(menu.is_video);
m_media_play_action->set_visible(!menu.is_playing);
m_media_pause_action->set_visible(menu.is_playing);
m_media_mute_action->set_visible(!menu.is_muted);
m_media_unmute_action->set_visible(menu.is_muted);
m_media_show_controls_action->set_visible(!menu.has_user_agent_controls);
m_media_hide_controls_action->set_visible(menu.has_user_agent_controls);
m_media_loop_action->set_checked(menu.is_looping);
m_media_enter_fullscreen_action->set_visible(menu.is_video && !menu.is_fullscreen);
m_media_exit_fullscreen_action->set_visible(menu.is_video && menu.is_fullscreen);
if (m_media_context_menu->on_activation)
m_media_context_menu->on_activation(to_widget_position(content_position));
}
u64 ViewImplementation::add_navigation_listener(NavigationListener listener)
{
auto id = m_next_navigation_listener_id++;
m_navigation_listeners.set(id, move(listener));
return id;
}
void ViewImplementation::remove_navigation_listener(u64 listener_id)
{
m_navigation_listeners.remove(listener_id);
}
void ViewImplementation::request_close()
{
if (needs_beforeunload_check()) {
client().async_request_close(page_id());
return;
}
client().request_close(page_id());
}
Function<void()> ViewImplementation::prepare_for_immediate_close()
{
VERIFY(!needs_beforeunload_check());
auto client = m_client_state.client;
auto page_id = m_client_state.page_index;
client->prepare_for_detached_close(page_id);
return [client = move(client), page_id] {
client->async_request_close(page_id);
};
}
}