2023-01-12 16:27:17 -03:00
|
|
|
/*
|
|
|
|
|
* Copyright (c) 2023, Linus Groh <linusg@serenityos.org>
|
|
|
|
|
*
|
|
|
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
|
|
|
*/
|
|
|
|
|
|
2026-05-11 16:45:34 -03:00
|
|
|
#include <AK/Debug.h>
|
2023-01-12 17:22:14 -03:00
|
|
|
#include <AK/Error.h>
|
2026-06-04 05:40:32 -03:00
|
|
|
#include <AK/NeverDestroyed.h>
|
2026-06-13 11:06:49 -03:00
|
|
|
#include <AK/NumericLimits.h>
|
2026-05-06 13:00:59 -03:00
|
|
|
#include <AK/ScopeGuard.h>
|
2023-01-12 17:22:14 -03:00
|
|
|
#include <AK/String.h>
|
2025-06-19 16:05:42 -03:00
|
|
|
#include <AK/Time.h>
|
2026-06-13 11:06:49 -03:00
|
|
|
#include <LibCore/EventLoop.h>
|
2023-05-17 10:53:13 -03:00
|
|
|
#include <LibCore/StandardPaths.h>
|
2024-06-28 09:24:49 -03:00
|
|
|
#include <LibCore/Timer.h>
|
2023-05-17 10:53:13 -03:00
|
|
|
#include <LibGfx/ImageFormats/PNGWriter.h>
|
2026-04-07 13:35:26 -03:00
|
|
|
#include <LibGfx/SharedImageBuffer.h>
|
2025-09-01 09:20:14 -03:00
|
|
|
#include <LibURL/Parser.h>
|
2026-05-26 20:11:14 -03:00
|
|
|
#include <LibWeb/CSS/SystemColor.h>
|
2024-11-13 18:35:17 -03:00
|
|
|
#include <LibWeb/Crypto/Crypto.h>
|
2023-10-23 17:52:57 -03:00
|
|
|
#include <LibWeb/Infra/Strings.h>
|
2026-06-13 11:06:49 -03:00
|
|
|
#include <LibWeb/WebDriver/Error.h>
|
2024-08-28 16:32:50 -03:00
|
|
|
#include <LibWebView/Application.h>
|
2026-03-22 12:44:54 -03:00
|
|
|
#include <LibWebView/BookmarkStore.h>
|
2026-04-24 09:36:55 -03:00
|
|
|
#include <LibWebView/ErrorHTML.h>
|
2024-11-13 18:35:17 -03:00
|
|
|
#include <LibWebView/HelperProcess.h>
|
2026-06-13 11:06:49 -03:00
|
|
|
#include <LibWebView/HistoryDebug.h>
|
2026-04-11 19:33:00 -03:00
|
|
|
#include <LibWebView/HistoryStore.h>
|
2025-09-01 09:20:14 -03:00
|
|
|
#include <LibWebView/Menu.h>
|
2026-06-13 11:06:49 -03:00
|
|
|
#include <LibWebView/SiteIsolation.h>
|
2025-09-01 09:20:14 -03:00
|
|
|
#include <LibWebView/URL.h>
|
2024-11-13 18:35:17 -03:00
|
|
|
#include <LibWebView/UserAgent.h>
|
2023-01-12 16:27:17 -03:00
|
|
|
#include <LibWebView/ViewImplementation.h>
|
|
|
|
|
|
|
|
|
|
namespace WebView {
|
|
|
|
|
|
2026-06-04 05:40:32 -03:00
|
|
|
static HashMap<u64, ViewImplementation*>& all_views()
|
|
|
|
|
{
|
|
|
|
|
static NeverDestroyed<HashMap<u64, ViewImplementation*>> views;
|
|
|
|
|
return *views;
|
|
|
|
|
}
|
2025-02-15 10:06:36 -03:00
|
|
|
static u64 s_view_count = 1; // This has to start at 1 for Firefox DevTools.
|
|
|
|
|
|
|
|
|
|
void ViewImplementation::for_each_view(Function<IterationDecision(ViewImplementation&)> callback)
|
|
|
|
|
{
|
2026-06-04 05:40:32 -03:00
|
|
|
for (auto& view : all_views()) {
|
2025-02-15 10:06:36 -03:00
|
|
|
if (callback(*view.value) == IterationDecision::Break)
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Optional<ViewImplementation&> ViewImplementation::find_view_by_id(u64 id)
|
|
|
|
|
{
|
2026-06-04 05:40:32 -03:00
|
|
|
if (auto view = all_views().get(id); view.has_value())
|
2025-02-15 10:06:36 -03:00
|
|
|
return *view.value();
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-07 11:44:20 -03:00
|
|
|
ViewImplementation::ViewImplementation()
|
2026-02-01 15:27:19 -03:00
|
|
|
: m_document_cookie_version_buffer(Core::create_shared_version_buffer())
|
|
|
|
|
, m_view_id(s_view_count++)
|
2023-05-15 02:59:51 -03:00
|
|
|
{
|
2026-06-04 05:40:32 -03:00
|
|
|
all_views().set(m_view_id, this);
|
2025-02-15 10:06:36 -03:00
|
|
|
|
2025-09-01 09:20:14 -03:00
|
|
|
initialize_context_menus();
|
|
|
|
|
|
2023-05-27 13:46:45 -03:00
|
|
|
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;
|
2024-04-16 15:34:01 -03:00
|
|
|
});
|
2023-08-23 07:54:22 -03:00
|
|
|
|
|
|
|
|
on_request_file = [this](auto const& path, auto request_id) {
|
|
|
|
|
auto file = Core::File::open(path, Core::File::OpenMode::Read);
|
|
|
|
|
|
|
|
|
|
if (file.is_error())
|
2024-02-02 22:00:48 -03:00
|
|
|
client().async_handle_file_return(page_id(), file.error().code(), {}, request_id);
|
2023-08-23 07:54:22 -03:00
|
|
|
else
|
2024-04-16 16:46:30 -03:00
|
|
|
client().async_handle_file_return(page_id(), 0, IPC::File::adopt_file(file.release_value()), request_id);
|
2023-08-23 07:54:22 -03:00
|
|
|
};
|
2023-05-15 02:59:51 -03:00
|
|
|
}
|
|
|
|
|
|
2024-02-02 22:00:48 -03:00
|
|
|
ViewImplementation::~ViewImplementation()
|
|
|
|
|
{
|
2026-06-04 05:40:32 -03:00
|
|
|
all_views().remove(m_view_id);
|
2025-02-15 10:06:36 -03:00
|
|
|
|
2024-02-02 22:00:48 -03:00
|
|
|
if (m_client_state.client)
|
|
|
|
|
m_client_state.client->unregister_view(m_client_state.page_index);
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-12 16:27:17 -03:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-02 22:00:48 -03:00
|
|
|
u64 ViewImplementation::page_id() const
|
|
|
|
|
{
|
|
|
|
|
VERIFY(m_client_state.client);
|
|
|
|
|
return m_client_state.page_index;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-22 12:44:54 -03:00
|
|
|
void ViewImplementation::set_url(URL::URL url)
|
|
|
|
|
{
|
|
|
|
|
if (m_url == url)
|
|
|
|
|
return;
|
|
|
|
|
|
2026-05-04 15:59:22 -03:00
|
|
|
auto previous_host = current_host();
|
2026-03-22 12:44:54 -03:00
|
|
|
m_url = move(url);
|
|
|
|
|
update_bookmark_action();
|
2026-05-04 15:59:22 -03:00
|
|
|
|
|
|
|
|
if (current_host() != previous_host)
|
|
|
|
|
apply_zoom_for_current_host();
|
2026-03-22 12:44:54 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 19:33:00 -03:00
|
|
|
if (m_favicon_base64_png.has_value()) {
|
2026-03-22 12:44:54 -03:00
|
|
|
Application::bookmark_store().update_favicon(m_url, *m_favicon_base64_png);
|
2026-05-20 11:42:08 -03:00
|
|
|
if (!m_should_suppress_history_for_current_load)
|
|
|
|
|
Application::history_store().update_favicon(m_url, *m_favicon_base64_png);
|
2026-04-11 19:33:00 -03:00
|
|
|
}
|
2026-03-22 12:44:54 -03:00
|
|
|
|
|
|
|
|
if (on_favicon_change)
|
|
|
|
|
on_favicon_change(favicon);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-13 11:06:49 -03:00
|
|
|
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)
|
2025-03-09 12:40:34 -03:00
|
|
|
{
|
2026-06-13 11:06:49 -03:00
|
|
|
dump_session_history("before-process-swap"sv);
|
|
|
|
|
m_webdriver_pending_navigation_url = url;
|
|
|
|
|
|
2026-05-26 20:39:43 -03:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-30 04:55:00 -03:00
|
|
|
if (m_client_state.client) {
|
|
|
|
|
m_client_state.client->unregister_view(m_client_state.page_index);
|
|
|
|
|
}
|
2025-03-09 12:40:34 -03:00
|
|
|
|
|
|
|
|
initialize_client();
|
|
|
|
|
VERIFY(m_client_state.client);
|
|
|
|
|
|
2026-02-20 12:21:22 -03:00
|
|
|
if (on_web_content_process_change_for_cross_site_navigation)
|
|
|
|
|
on_web_content_process_change_for_cross_site_navigation();
|
|
|
|
|
|
2025-03-09 12:40:34 -03:00
|
|
|
handle_resize();
|
|
|
|
|
|
2026-06-13 11:06:49 -03:00
|
|
|
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;
|
2026-06-16 08:03:40 -03:00
|
|
|
if (m_session_history_entry_url_loading_from_ui_process.has_value())
|
2026-06-13 11:06:49 -03:00
|
|
|
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;
|
2026-06-16 08:03:40 -03:00
|
|
|
if (!m_session_history_entry_url_loading_from_ui_process.has_value())
|
2026-06-13 11:06:49 -03:00
|
|
|
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;
|
2026-06-16 08:03:40 -03:00
|
|
|
if (should_manage_session_history_in_ui_process() && !m_session_history_entry_url_loading_from_ui_process.has_value()) {
|
2026-06-13 11:06:49 -03:00
|
|
|
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();
|
|
|
|
|
}
|
2026-06-16 08:03:40 -03:00
|
|
|
if (!m_session_history_entry_url_loading_from_ui_process.has_value())
|
2026-06-13 11:06:49 -03:00
|
|
|
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);
|
2025-03-09 12:40:34 -03:00
|
|
|
}
|
|
|
|
|
|
2023-05-20 11:50:05 -03:00
|
|
|
void ViewImplementation::server_did_paint(Badge<WebContentClient>, i32 bitmap_id, Gfx::IntSize size)
|
|
|
|
|
{
|
2026-05-11 16:02:29 -03:00
|
|
|
bool did_swap_bitmap = false;
|
2023-12-21 18:58:54 -03:00
|
|
|
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);
|
2026-04-07 13:35:26 -03:00
|
|
|
m_backup_shared_image_buffer = nullptr;
|
2026-05-11 16:02:29 -03:00
|
|
|
did_swap_bitmap = true;
|
2023-05-20 11:50:05 -03:00
|
|
|
}
|
2024-01-07 12:35:45 -03:00
|
|
|
|
2026-05-11 16:48:43 -03:00
|
|
|
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);
|
|
|
|
|
|
2026-05-11 16:02:29 -03:00
|
|
|
client().notify_presented_bitmap_ready_to_paint(page_id(), bitmap_id);
|
|
|
|
|
|
|
|
|
|
if (did_swap_bitmap && on_ready_to_paint)
|
|
|
|
|
on_ready_to_paint();
|
2023-05-20 11:50:05 -03:00
|
|
|
}
|
|
|
|
|
|
2024-10-28 08:48:49 -03:00
|
|
|
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>());
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-29 00:37:11 -03:00
|
|
|
void ViewImplementation::did_update_window_rect()
|
|
|
|
|
{
|
|
|
|
|
client().async_did_update_window_rect(m_client_state.page_index);
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-13 13:24:16 -03:00
|
|
|
void ViewImplementation::set_system_visibility_state(Web::HTML::VisibilityState visibility_state)
|
|
|
|
|
{
|
2026-05-27 08:19:17 -03:00
|
|
|
if (m_system_visibility_state == visibility_state)
|
|
|
|
|
return;
|
|
|
|
|
|
2024-11-13 13:24:16 -03:00
|
|
|
m_system_visibility_state = visibility_state;
|
|
|
|
|
client().async_set_system_visibility_state(m_client_state.page_index, m_system_visibility_state);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-13 11:06:49 -03:00
|
|
|
void ViewImplementation::load(URL::URL const& url, Web::Bindings::NavigationHistoryBehavior history_handling)
|
2023-01-12 17:30:06 -03:00
|
|
|
{
|
2026-06-13 11:06:49 -03:00
|
|
|
m_is_showing_crash_page = false;
|
2026-05-20 11:42:08 -03:00
|
|
|
m_should_suppress_history_for_current_load = false;
|
|
|
|
|
m_should_suppress_history_for_next_load = false;
|
2026-06-13 11:06:49 -03:00
|
|
|
auto should_defer_ui_process_history_update = false;
|
2026-06-16 08:03:40 -03:00
|
|
|
if (!m_session_history_entry_url_loading_from_ui_process.has_value())
|
2026-06-13 11:06:49 -03:00
|
|
|
abandon_pending_web_content_session_history_seed();
|
2026-06-16 08:03:40 -03:00
|
|
|
if (should_manage_session_history_in_ui_process() && !m_session_history_entry_url_loading_from_ui_process.has_value()) {
|
2026-06-13 11:06:49 -03:00
|
|
|
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);
|
2023-01-12 17:30:06 -03:00
|
|
|
}
|
|
|
|
|
|
2023-09-17 12:12:17 -03:00
|
|
|
void ViewImplementation::load_html(StringView html)
|
2023-01-12 17:30:06 -03:00
|
|
|
{
|
2026-06-13 11:06:49 -03:00
|
|
|
m_is_showing_crash_page = false;
|
2026-05-20 11:42:08 -03:00
|
|
|
m_should_suppress_history_for_current_load = false;
|
|
|
|
|
m_should_suppress_history_for_next_load = false;
|
2026-06-13 11:06:49 -03:00
|
|
|
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();
|
|
|
|
|
}
|
2024-02-02 22:00:48 -03:00
|
|
|
client().async_load_html(page_id(), html);
|
2023-01-12 17:30:06 -03:00
|
|
|
}
|
|
|
|
|
|
2026-05-20 11:42:08 -03:00
|
|
|
void ViewImplementation::load_crash_page_html(StringView html, URL::URL const& crashed_url)
|
|
|
|
|
{
|
2026-06-13 11:06:49 -03:00
|
|
|
m_is_showing_crash_page = true;
|
2026-05-20 11:42:08 -03:00
|
|
|
m_should_suppress_history_for_current_load = true;
|
|
|
|
|
m_should_suppress_history_for_next_load = true;
|
2026-06-13 11:06:49 -03:00
|
|
|
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();
|
|
|
|
|
}
|
2026-05-20 11:42:08 -03:00
|
|
|
set_url(crashed_url);
|
|
|
|
|
client().async_load_html_with_url(page_id(), html, crashed_url);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-24 09:36:55 -03:00
|
|
|
void ViewImplementation::load_navigation_error_page(StringView text)
|
|
|
|
|
{
|
|
|
|
|
auto message = MUST(String::formatted("Failed to load \"{}\"", text));
|
|
|
|
|
|
|
|
|
|
StringBuilder builder;
|
2026-05-20 11:42:08 -03:00
|
|
|
builder.appendff(ERROR_HTML_HEADER, ""sv, ERROR_SVG, message);
|
2026-04-24 09:36:55 -03:00
|
|
|
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());
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-13 13:51:27 -03:00
|
|
|
void ViewImplementation::reload()
|
|
|
|
|
{
|
2026-06-13 11:06:49 -03:00
|
|
|
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;
|
2026-05-20 11:42:08 -03:00
|
|
|
m_should_suppress_history_for_current_load = false;
|
|
|
|
|
m_should_suppress_history_for_next_load = false;
|
2026-06-13 11:06:49 -03:00
|
|
|
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);
|
|
|
|
|
}
|
2024-04-13 13:51:27 -03:00
|
|
|
client().async_reload(page_id());
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-13 11:06:49 -03:00
|
|
|
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 };
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-20 11:42:08 -03:00
|
|
|
m_should_suppress_history_for_current_load = false;
|
|
|
|
|
m_should_suppress_history_for_next_load = false;
|
2026-06-13 11:06:49 -03:00
|
|
|
|
|
|
|
|
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
|
2026-06-16 08:03:40 -03:00
|
|
|
&& !m_session_history_entry_url_loading_from_ui_process.has_value()
|
2026-06-13 11:06:49 -03:00
|
|
|
&& !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,
|
|
|
|
|
};
|
2024-04-13 17:33:48 -03:00
|
|
|
}
|
|
|
|
|
|
2026-06-15 20:35:03 -03:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-12 16:49:49 -03:00
|
|
|
void ViewImplementation::zoom_in()
|
|
|
|
|
{
|
|
|
|
|
if (m_zoom_level >= ZOOM_MAX_LEVEL)
|
|
|
|
|
return;
|
2026-01-05 22:05:10 -03:00
|
|
|
m_zoom_level = round_to<int>((m_zoom_level + ZOOM_STEP) * 100) / 100.0;
|
2023-01-12 16:49:49 -03:00
|
|
|
update_zoom();
|
2026-05-04 15:59:22 -03:00
|
|
|
Application::settings().set_zoom_for_host(current_host(), m_zoom_level);
|
2023-01-12 16:49:49 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ViewImplementation::zoom_out()
|
|
|
|
|
{
|
|
|
|
|
if (m_zoom_level <= ZOOM_MIN_LEVEL)
|
|
|
|
|
return;
|
2026-01-05 22:05:10 -03:00
|
|
|
m_zoom_level = round_to<int>((m_zoom_level - ZOOM_STEP) * 100) / 100.0;
|
2023-01-12 16:49:49 -03:00
|
|
|
update_zoom();
|
2026-05-04 15:59:22 -03:00
|
|
|
Application::settings().set_zoom_for_host(current_host(), m_zoom_level);
|
2023-01-12 16:49:49 -03:00
|
|
|
}
|
|
|
|
|
|
2024-12-23 18:15:06 -03:00
|
|
|
void ViewImplementation::set_zoom(double zoom_level)
|
|
|
|
|
{
|
|
|
|
|
m_zoom_level = max(ZOOM_MIN_LEVEL, min(zoom_level, ZOOM_MAX_LEVEL));
|
|
|
|
|
update_zoom();
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-12 16:49:49 -03:00
|
|
|
void ViewImplementation::reset_zoom()
|
|
|
|
|
{
|
2026-01-05 22:05:10 -03:00
|
|
|
m_zoom_level = 1.0;
|
2023-01-12 16:49:49 -03:00
|
|
|
update_zoom();
|
2025-10-09 15:29:12 -03:00
|
|
|
client().async_reset_zoom(m_client_state.page_index);
|
2026-05-04 15:59:22 -03:00
|
|
|
Application::settings().set_zoom_for_host(current_host(), m_zoom_level);
|
2023-01-12 16:49:49 -03:00
|
|
|
}
|
|
|
|
|
|
2024-03-04 00:07:49 -03:00
|
|
|
void ViewImplementation::enqueue_input_event(Web::InputEvent event)
|
|
|
|
|
{
|
2026-05-31 22:07:38 -03:00
|
|
|
auto* mouse_event = event.get_pointer<Web::MouseEvent>();
|
2026-06-14 16:30:28 -03:00
|
|
|
auto* pinch_event = event.get_pointer<Web::PinchEvent>();
|
2026-05-31 22:07:38 -03:00
|
|
|
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
|
2026-05-11 16:45:34 -03:00
|
|
|
&& m_client_state.has_usable_bitmap
|
2026-05-12 16:49:42 -03:00
|
|
|
&& 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)) {
|
2026-05-12 16:49:42 -03:00
|
|
|
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;
|
|
|
|
|
}
|
2026-05-11 16:45:34 -03:00
|
|
|
}
|
2026-06-14 16:30:28 -03:00
|
|
|
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);
|
|
|
|
|
}
|
2026-05-11 16:45:34 -03:00
|
|
|
|
2024-03-04 00:07:49 -03:00
|
|
|
// 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) {
|
2025-03-15 18:07:53 -03:00
|
|
|
client().async_key_event(m_client_state.page_index, event.clone_without_browser_data());
|
2024-03-04 00:07:49 -03:00
|
|
|
},
|
|
|
|
|
[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);
|
2024-08-17 14:36:28 -03:00
|
|
|
},
|
|
|
|
|
[this](Web::DragEvent& event) {
|
2025-03-15 18:07:53 -03:00
|
|
|
auto cloned_event = event.clone_without_browser_data();
|
2024-08-17 14:36:28 -03:00
|
|
|
cloned_event.files = move(event.files);
|
|
|
|
|
|
2025-03-08 13:00:03 -03:00
|
|
|
client().async_drag_event(m_client_state.page_index, cloned_event);
|
2025-10-08 18:59:50 -03:00
|
|
|
},
|
|
|
|
|
[this](Web::PinchEvent const& event) {
|
|
|
|
|
client().async_pinch_event(m_client_state.page_index, event);
|
2024-03-04 00:07:49 -03:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-12 13:56:31 -03:00
|
|
|
void ViewImplementation::did_finish_handling_input_event(Badge<WebContentClient>, Web::EventResult event_result)
|
2024-03-04 00:07:49 -03:00
|
|
|
{
|
|
|
|
|
auto event = m_pending_input_events.dequeue();
|
|
|
|
|
|
2024-09-12 13:56:31 -03:00
|
|
|
if (event_result == Web::EventResult::Handled)
|
2024-08-17 14:36:28 -03:00
|
|
|
return;
|
2024-03-04 00:07:49 -03:00
|
|
|
|
2024-08-17 14:36:28 -03:00
|
|
|
// 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&) {});
|
2024-03-04 00:07:49 -03:00
|
|
|
}
|
|
|
|
|
|
2023-01-12 17:44:42 -03:00
|
|
|
void ViewImplementation::set_preferred_color_scheme(Web::CSS::PreferredColorScheme color_scheme)
|
|
|
|
|
{
|
2026-05-27 05:25:08 -03:00
|
|
|
m_preferred_color_scheme = color_scheme;
|
|
|
|
|
set_page_background_color(preferred_canvas_background_color());
|
2026-05-26 20:11:14 -03:00
|
|
|
|
2024-02-02 22:00:48 -03:00
|
|
|
client().async_set_preferred_color_scheme(page_id(), color_scheme);
|
2023-01-12 17:44:42 -03:00
|
|
|
}
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-13 11:15:59 -03:00
|
|
|
void ViewImplementation::set_preferred_motion(Web::CSS::PreferredMotion motion)
|
|
|
|
|
{
|
|
|
|
|
client().async_set_preferred_motion(page_id(), motion);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-10 07:19:44 -03:00
|
|
|
void ViewImplementation::notify_cookies_changed(HashTable<String> const& changed_domains, ReadonlySpan<HTTP::Cookie::Cookie> page_cookies, ReadonlySpan<HTTP::Cookie::Cookie> host_cookies)
|
2026-01-30 10:52:34 -03:00
|
|
|
{
|
2026-02-01 15:27:19 -03:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-10 07:19:44 -03:00
|
|
|
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;
|
2026-02-01 15:27:19 -03:00
|
|
|
}
|
|
|
|
|
|
2026-06-11 10:12:37 -03:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-11 12:51:05 -03:00
|
|
|
void ViewImplementation::notify_indexed_database_changed(JsonObject update)
|
|
|
|
|
{
|
|
|
|
|
for (auto& listener : m_indexed_database_change_listeners)
|
|
|
|
|
listener.value(update);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
u64 ViewImplementation::add_indexed_database_change_listener(DevTools::DevToolsDelegate::OnIndexedDatabaseChange on_indexed_database_change)
|
|
|
|
|
{
|
|
|
|
|
auto listener_id = m_next_indexed_database_change_listener_id++;
|
|
|
|
|
m_indexed_database_change_listeners.set(listener_id, move(on_indexed_database_change));
|
|
|
|
|
return listener_id;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ViewImplementation::remove_indexed_database_change_listener(u64 listener_id)
|
|
|
|
|
{
|
|
|
|
|
m_indexed_database_change_listeners.remove(listener_id);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-01 15:27:19 -03:00
|
|
|
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
|
|
|
|
|
{
|
2026-02-07 13:13:47 -03:00
|
|
|
auto domain = HTTP::Cookie::canonicalize_domain(url);
|
2026-02-01 15:27:19 -03:00
|
|
|
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);
|
2026-01-30 10:52:34 -03:00
|
|
|
}
|
|
|
|
|
|
2023-12-16 11:19:34 -03:00
|
|
|
ByteString ViewImplementation::selected_text()
|
2023-01-12 17:42:42 -03:00
|
|
|
{
|
2024-02-02 22:00:48 -03:00
|
|
|
return client().get_selected_text(page_id());
|
2023-01-12 17:42:42 -03:00
|
|
|
}
|
|
|
|
|
|
2026-05-06 13:00:59 -03:00
|
|
|
ByteString ViewImplementation::cut_selected_text()
|
|
|
|
|
{
|
|
|
|
|
return client().cut_selected_text(page_id());
|
|
|
|
|
}
|
|
|
|
|
|
2023-10-23 17:52:57 -03:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-12 17:42:42 -03:00
|
|
|
void ViewImplementation::select_all()
|
|
|
|
|
{
|
2024-02-02 22:00:48 -03:00
|
|
|
client().async_select_all(page_id());
|
2023-01-12 17:42:42 -03:00
|
|
|
}
|
|
|
|
|
|
2024-05-31 17:01:34 -03:00
|
|
|
void ViewImplementation::find_in_page(String const& query, CaseSensitivity case_sensitivity)
|
2024-05-29 16:09:33 -03:00
|
|
|
{
|
2024-05-31 17:01:34 -03:00
|
|
|
client().async_find_in_page(page_id(), query, case_sensitivity);
|
2024-05-29 16:09:33 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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());
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-12 16:58:00 -03:00
|
|
|
void ViewImplementation::get_source()
|
|
|
|
|
{
|
2024-02-02 22:00:48 -03:00
|
|
|
client().async_get_source(page_id());
|
2023-01-12 16:58:00 -03:00
|
|
|
}
|
|
|
|
|
|
2023-01-12 17:22:14 -03:00
|
|
|
void ViewImplementation::inspect_dom_tree()
|
|
|
|
|
{
|
2024-02-02 22:00:48 -03:00
|
|
|
client().async_inspect_dom_tree(page_id());
|
2023-01-12 17:22:14 -03:00
|
|
|
}
|
|
|
|
|
|
2026-06-10 12:15:21 -03:00
|
|
|
void ViewImplementation::inspect_storage(Web::StorageAPI::StorageEndpointType storage_endpoint, u64 request_id)
|
|
|
|
|
{
|
|
|
|
|
client().async_inspect_storage(page_id(), storage_endpoint, request_id);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-10 12:27:25 -03:00
|
|
|
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());
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-21 11:34:02 -03:00
|
|
|
void ViewImplementation::inspect_accessibility_tree()
|
2023-01-12 17:22:14 -03:00
|
|
|
{
|
2025-02-21 11:34:02 -03:00
|
|
|
client().async_inspect_accessibility_tree(page_id());
|
2023-01-12 17:22:14 -03:00
|
|
|
}
|
|
|
|
|
|
2025-02-21 11:34:02 -03:00
|
|
|
void ViewImplementation::get_hovered_node_id()
|
2023-01-12 17:22:14 -03:00
|
|
|
{
|
2025-02-21 11:34:02 -03:00
|
|
|
client().async_get_hovered_node_id(page_id());
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-28 08:14:41 -03:00
|
|
|
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,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-04 11:17:17 -03:00
|
|
|
void ViewImplementation::inspect_dom_node(Web::UniqueNodeID node_id, DOMNodeProperties::Type property_type, Optional<Web::CSS::PseudoElement> pseudo_element, JsonValue options)
|
2025-02-21 11:34:02 -03:00
|
|
|
{
|
2026-06-04 11:17:17 -03:00
|
|
|
client().async_inspect_dom_node(page_id(), property_type, node_id, pseudo_element, move(options));
|
2023-01-12 17:22:14 -03:00
|
|
|
}
|
|
|
|
|
|
2026-05-22 10:42:29 -03:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-27 12:41:13 -03:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-17 08:39:37 -03:00
|
|
|
void ViewImplementation::inspect_indexed_database_storage(DevTools::DevToolsDelegate::OnIndexedDBInspectionComplete on_complete)
|
|
|
|
|
{
|
|
|
|
|
auto request_id = m_next_indexed_database_inspection_request_id++;
|
|
|
|
|
m_pending_indexed_database_inspection_requests.set(request_id, move(on_complete));
|
|
|
|
|
client().async_inspect_indexed_database_storage(page_id(), request_id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ViewImplementation::inspect_indexed_database_objects(String const& host, Optional<JsonArray> names, JsonObject options, DevTools::DevToolsDelegate::OnIndexedDBInspectionComplete on_complete)
|
|
|
|
|
{
|
|
|
|
|
auto request_id = m_next_indexed_database_inspection_request_id++;
|
|
|
|
|
m_pending_indexed_database_inspection_requests.set(request_id, move(on_complete));
|
|
|
|
|
if (names.has_value())
|
|
|
|
|
client().async_inspect_indexed_database_objects(page_id(), request_id, host, JsonValue { names.release_value() }, JsonValue { move(options) });
|
|
|
|
|
else
|
|
|
|
|
client().async_inspect_indexed_database_objects(page_id(), request_id, host, JsonValue {}, JsonValue { move(options) });
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-11 12:59:48 -03:00
|
|
|
void ViewImplementation::delete_indexed_database(String const& host, String const& name, DevTools::DevToolsDelegate::OnIndexedDBInspectionComplete on_complete)
|
|
|
|
|
{
|
|
|
|
|
auto request_id = m_next_indexed_database_inspection_request_id++;
|
|
|
|
|
m_pending_indexed_database_inspection_requests.set(request_id, move(on_complete));
|
|
|
|
|
client().async_delete_indexed_database(page_id(), request_id, host, name);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ViewImplementation::clear_indexed_database_object_store(String const& host, String const& name, DevTools::DevToolsDelegate::OnIndexedDBInspectionComplete on_complete)
|
|
|
|
|
{
|
|
|
|
|
auto request_id = m_next_indexed_database_inspection_request_id++;
|
|
|
|
|
m_pending_indexed_database_inspection_requests.set(request_id, move(on_complete));
|
|
|
|
|
client().async_clear_indexed_database_object_store(page_id(), request_id, host, name);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ViewImplementation::delete_indexed_database_record(String const& host, String const& name, DevTools::DevToolsDelegate::OnIndexedDBInspectionComplete on_complete)
|
|
|
|
|
{
|
|
|
|
|
auto request_id = m_next_indexed_database_inspection_request_id++;
|
|
|
|
|
m_pending_indexed_database_inspection_requests.set(request_id, move(on_complete));
|
|
|
|
|
client().async_delete_indexed_database_record(page_id(), request_id, host, name);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-17 08:39:37 -03:00
|
|
|
void ViewImplementation::did_receive_indexed_database_inspection(u64 request_id, JsonObject result)
|
|
|
|
|
{
|
|
|
|
|
auto callback = m_pending_indexed_database_inspection_requests.take(request_id);
|
|
|
|
|
if (!callback.has_value())
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
if (result.has_string("error"sv)) {
|
|
|
|
|
(*callback)(Error::from_string_literal("IndexedDB operation failed"));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
(*callback)(move(result));
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-12 17:22:14 -03:00
|
|
|
void ViewImplementation::clear_inspected_dom_node()
|
|
|
|
|
{
|
2025-03-19 16:50:56 -03:00
|
|
|
client().async_clear_inspected_dom_node(page_id());
|
2023-01-12 17:22:14 -03:00
|
|
|
}
|
|
|
|
|
|
2025-03-20 13:56:46 -03:00
|
|
|
void ViewImplementation::highlight_dom_node(Web::UniqueNodeID node_id, Optional<Web::CSS::PseudoElement> pseudo_element)
|
2023-01-12 17:22:14 -03:00
|
|
|
{
|
2025-03-08 13:00:03 -03:00
|
|
|
client().async_highlight_dom_node(page_id(), node_id, pseudo_element);
|
2025-02-21 11:34:02 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ViewImplementation::clear_highlighted_dom_node()
|
|
|
|
|
{
|
|
|
|
|
highlight_dom_node(0, {});
|
2023-01-12 17:22:14 -03:00
|
|
|
}
|
|
|
|
|
|
2026-05-27 13:12:14 -03:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-22 10:42:29 -03:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-06 19:32:43 -03:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-15 10:28:42 -03:00
|
|
|
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());
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-10 19:15:31 -03:00
|
|
|
void ViewImplementation::get_dom_node_inner_html(Web::UniqueNodeID node_id)
|
|
|
|
|
{
|
|
|
|
|
client().async_get_dom_node_inner_html(page_id(), node_id);
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-10 18:36:41 -03:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-08 13:00:03 -03:00
|
|
|
void ViewImplementation::set_dom_node_text(Web::UniqueNodeID node_id, String const& text)
|
2023-11-19 12:42:11 -03:00
|
|
|
{
|
2025-03-08 13:00:03 -03:00
|
|
|
client().async_set_dom_node_text(page_id(), node_id, text);
|
2023-11-19 12:42:11 -03:00
|
|
|
}
|
|
|
|
|
|
2025-03-08 13:00:03 -03:00
|
|
|
void ViewImplementation::set_dom_node_tag(Web::UniqueNodeID node_id, String const& name)
|
2023-11-19 12:42:11 -03:00
|
|
|
{
|
2025-03-08 13:00:03 -03:00
|
|
|
client().async_set_dom_node_tag(page_id(), node_id, name);
|
2023-11-19 12:42:11 -03:00
|
|
|
}
|
|
|
|
|
|
2025-03-08 13:00:03 -03:00
|
|
|
void ViewImplementation::add_dom_node_attributes(Web::UniqueNodeID node_id, ReadonlySpan<Attribute> attributes)
|
2023-12-05 18:49:47 -03:00
|
|
|
{
|
2025-03-08 13:00:03 -03:00
|
|
|
client().async_add_dom_node_attributes(page_id(), node_id, attributes);
|
2023-12-05 18:49:47 -03:00
|
|
|
}
|
|
|
|
|
|
2025-03-08 13:00:03 -03:00
|
|
|
void ViewImplementation::replace_dom_node_attribute(Web::UniqueNodeID node_id, String const& name, ReadonlySpan<Attribute> replacement_attributes)
|
2023-11-19 12:42:11 -03:00
|
|
|
{
|
2025-03-08 13:00:03 -03:00
|
|
|
client().async_replace_dom_node_attribute(page_id(), node_id, name, replacement_attributes);
|
2023-11-19 12:42:11 -03:00
|
|
|
}
|
|
|
|
|
|
2024-10-20 05:37:44 -03:00
|
|
|
void ViewImplementation::create_child_element(Web::UniqueNodeID node_id)
|
2023-12-07 13:00:57 -03:00
|
|
|
{
|
2024-02-02 22:00:48 -03:00
|
|
|
client().async_create_child_element(page_id(), node_id);
|
2023-12-07 13:00:57 -03:00
|
|
|
}
|
|
|
|
|
|
2024-10-20 05:37:44 -03:00
|
|
|
void ViewImplementation::create_child_text_node(Web::UniqueNodeID node_id)
|
2023-12-07 13:00:57 -03:00
|
|
|
{
|
2024-02-02 22:00:48 -03:00
|
|
|
client().async_create_child_text_node(page_id(), node_id);
|
2023-12-07 13:00:57 -03:00
|
|
|
}
|
|
|
|
|
|
2025-03-10 19:01:29 -03:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-20 05:37:44 -03:00
|
|
|
void ViewImplementation::clone_dom_node(Web::UniqueNodeID node_id)
|
2023-12-07 13:12:43 -03:00
|
|
|
{
|
2024-02-02 22:00:48 -03:00
|
|
|
client().async_clone_dom_node(page_id(), node_id);
|
2023-12-07 13:12:43 -03:00
|
|
|
}
|
|
|
|
|
|
2024-10-20 05:37:44 -03:00
|
|
|
void ViewImplementation::remove_dom_node(Web::UniqueNodeID node_id)
|
2023-12-05 16:59:54 -03:00
|
|
|
{
|
2024-02-02 22:00:48 -03:00
|
|
|
client().async_remove_dom_node(page_id(), node_id);
|
2023-12-05 16:59:54 -03:00
|
|
|
}
|
|
|
|
|
|
2024-08-23 07:18:35 -03:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-16 11:19:34 -03:00
|
|
|
void ViewImplementation::debug_request(ByteString const& request, ByteString const& argument)
|
2023-01-12 17:34:08 -03:00
|
|
|
{
|
2026-06-13 11:06:49 -03:00
|
|
|
if (request == "dump-session-history"sv)
|
|
|
|
|
dump_session_history("debug-request"sv, SessionHistoryDumpMode::Always);
|
|
|
|
|
|
2024-02-02 22:00:48 -03:00
|
|
|
client().async_debug_request(page_id(), request, argument);
|
2023-01-12 17:34:08 -03:00
|
|
|
}
|
|
|
|
|
|
2026-05-21 12:46:17 -03:00
|
|
|
void ViewImplementation::set_content_blockers(Core::AnonymousBuffer const& patterns)
|
|
|
|
|
{
|
|
|
|
|
client().async_set_content_blockers(page_id(), patterns);
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-08 13:00:03 -03:00
|
|
|
void ViewImplementation::run_javascript(String const& js_source)
|
2023-01-12 17:39:08 -03:00
|
|
|
{
|
2025-03-08 13:00:03 -03:00
|
|
|
client().async_run_javascript(page_id(), js_source);
|
2023-01-12 17:39:08 -03:00
|
|
|
}
|
|
|
|
|
|
2025-03-08 13:00:03 -03:00
|
|
|
void ViewImplementation::js_console_input(String const& js_source)
|
2023-05-17 12:54:36 -03:00
|
|
|
{
|
2025-03-08 13:00:03 -03:00
|
|
|
client().async_js_console_input(page_id(), js_source);
|
2023-05-17 12:54:36 -03:00
|
|
|
}
|
|
|
|
|
|
2025-04-01 09:23:04 -03:00
|
|
|
void ViewImplementation::exit_fullscreen()
|
|
|
|
|
{
|
|
|
|
|
client().async_exit_fullscreen(page_id());
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-16 21:28:02 -03:00
|
|
|
void ViewImplementation::set_is_fullscreen(Web::ViewportIsFullscreen is_fullscreen)
|
|
|
|
|
{
|
|
|
|
|
if (m_is_fullscreen == is_fullscreen)
|
|
|
|
|
return;
|
2026-03-20 20:46:50 -03:00
|
|
|
m_is_fullscreen = is_fullscreen;
|
2026-03-16 21:28:02 -03:00
|
|
|
|
|
|
|
|
handle_resize();
|
|
|
|
|
did_update_window_rect();
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-17 15:23:34 -03:00
|
|
|
void ViewImplementation::alert_closed()
|
|
|
|
|
{
|
2024-02-02 22:00:48 -03:00
|
|
|
client().async_alert_closed(page_id());
|
2023-05-17 15:23:34 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ViewImplementation::confirm_closed(bool accepted)
|
|
|
|
|
{
|
2024-02-02 22:00:48 -03:00
|
|
|
client().async_confirm_closed(page_id(), accepted);
|
2023-05-17 15:23:34 -03:00
|
|
|
}
|
|
|
|
|
|
2025-03-08 13:00:03 -03:00
|
|
|
void ViewImplementation::prompt_closed(Optional<String> const& response)
|
2023-05-17 15:23:34 -03:00
|
|
|
{
|
2025-03-08 13:00:03 -03:00
|
|
|
client().async_prompt_closed(page_id(), response);
|
2023-05-17 15:23:34 -03:00
|
|
|
}
|
|
|
|
|
|
2023-12-11 02:53:10 -03:00
|
|
|
void ViewImplementation::color_picker_update(Optional<Color> picked_color, Web::HTML::ColorPickerUpdateState state)
|
2023-09-04 06:32:40 -03:00
|
|
|
{
|
2024-02-02 22:00:48 -03:00
|
|
|
client().async_color_picker_update(page_id(), picked_color, state);
|
2023-09-04 06:32:40 -03:00
|
|
|
}
|
|
|
|
|
|
2024-02-25 15:02:47 -03:00
|
|
|
void ViewImplementation::file_picker_closed(Vector<Web::HTML::SelectedFile> selected_files)
|
|
|
|
|
{
|
|
|
|
|
client().async_file_picker_closed(page_id(), move(selected_files));
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-03 14:19:08 -03:00
|
|
|
void ViewImplementation::select_dropdown_closed(Optional<u32> const& selected_item_id)
|
2023-12-07 11:53:49 -03:00
|
|
|
{
|
2024-04-03 14:19:08 -03:00
|
|
|
client().async_select_dropdown_closed(page_id(), selected_item_id);
|
2023-12-07 11:53:49 -03:00
|
|
|
}
|
|
|
|
|
|
2025-09-01 09:20:14 -03:00
|
|
|
void ViewImplementation::paste_text_from_clipboard()
|
2023-06-16 12:29:54 -03:00
|
|
|
{
|
2025-09-18 09:16:36 -03:00
|
|
|
client().async_paste(page_id(), Application::the().clipboard_text());
|
2023-06-16 12:29:54 -03:00
|
|
|
}
|
|
|
|
|
|
2026-05-28 11:48:50 -03:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-01 09:20:14 -03:00
|
|
|
void ViewImplementation::retrieved_clipboard_entries(u64 request_id, ReadonlySpan<Web::Clipboard::SystemClipboardItem> items)
|
2023-05-15 12:17:58 -03:00
|
|
|
{
|
2025-09-01 09:20:14 -03:00
|
|
|
client().async_retrieved_clipboard_entries(page_id(), request_id, items);
|
2023-05-15 12:17:58 -03:00
|
|
|
}
|
|
|
|
|
|
2024-03-30 10:41:15 -03:00
|
|
|
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());
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-26 20:27:06 -03:00
|
|
|
void ViewImplementation::did_change_audio_play_state(Badge<WebContentClient>, Web::HTML::AudioPlayState play_state)
|
|
|
|
|
{
|
2024-03-30 10:34:23 -03:00
|
|
|
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;
|
|
|
|
|
}
|
2024-03-26 20:27:06 -03:00
|
|
|
|
2024-03-30 10:34:23 -03:00
|
|
|
if (state_changed && on_audio_play_state_changed)
|
2024-03-26 20:27:06 -03:00
|
|
|
on_audio_play_state_changed(m_audio_play_state);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-13 11:06:49 -03:00
|
|
|
void ViewImplementation::did_update_navigation_buttons_state(Badge<WebContentClient>, bool back_enabled, bool forward_enabled)
|
2024-04-13 18:12:55 -03:00
|
|
|
{
|
2026-06-13 11:06:49 -03:00
|
|
|
VERIFY(!should_manage_session_history_in_ui_process());
|
|
|
|
|
|
2025-09-01 09:20:14 -03:00
|
|
|
m_navigate_back_action->set_enabled(back_enabled);
|
|
|
|
|
m_navigate_forward_action->set_enabled(forward_enabled);
|
2026-06-13 11:06:49 -03:00
|
|
|
dump_session_history("did-update-navigation-buttons-state-using-webcontent"sv);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-15 06:27:47 -03:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-13 11:06:49 -03:00
|
|
|
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;
|
|
|
|
|
}
|
2026-06-15 06:27:47 -03:00
|
|
|
update_session_history_from_web_content(move(entries), move(used_steps), current_used_step_index, pending_step_after_fallback_load_was_restored, true);
|
2026-06-13 11:06:49 -03:00
|
|
|
dump_session_history("did-update-session-history"sv);
|
2024-04-13 18:12:55 -03:00
|
|
|
}
|
|
|
|
|
|
2026-06-15 06:27:47 -03:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-26 13:03:07 -03:00
|
|
|
void ViewImplementation::did_change_needs_beforeunload_check(Badge<WebContentClient>, bool needs_beforeunload_check)
|
|
|
|
|
{
|
|
|
|
|
m_needs_beforeunload_check = needs_beforeunload_check;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-24 15:44:30 -03:00
|
|
|
void ViewImplementation::did_change_background_color(Badge<WebContentClient>, Gfx::Color color)
|
2026-05-26 20:11:14 -03:00
|
|
|
{
|
|
|
|
|
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);
|
2026-05-27 05:25:08 -03:00
|
|
|
set_page_background_color(preferred_canvas_background_color());
|
2026-05-26 20:11:14 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ViewImplementation::set_page_background_color(Gfx::Color color)
|
2026-05-24 15:44:30 -03:00
|
|
|
{
|
|
|
|
|
m_page_background_color = color;
|
|
|
|
|
if (on_page_background_color_change)
|
2026-05-26 20:11:14 -03:00
|
|
|
on_page_background_color_change(m_page_background_color);
|
2026-05-24 15:44:30 -03:00
|
|
|
}
|
|
|
|
|
|
2026-05-27 05:25:08 -03:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-07 13:35:26 -03:00
|
|
|
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)
|
2023-05-15 02:59:51 -03:00
|
|
|
{
|
2026-05-11 16:48:43 -03:00
|
|
|
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);
|
2023-05-15 02:59:51 -03:00
|
|
|
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.
|
2026-04-07 13:35:26 -03:00
|
|
|
m_backup_shared_image_buffer = move(m_client_state.front_bitmap.shared_image_buffer);
|
2023-05-15 02:59:51 -03:00
|
|
|
m_backup_bitmap_size = m_client_state.front_bitmap.last_painted_size;
|
|
|
|
|
}
|
|
|
|
|
m_client_state.has_usable_bitmap = false;
|
2024-06-17 12:50:57 -03:00
|
|
|
m_client_state.front_bitmap.id = front_bitmap_id;
|
|
|
|
|
m_client_state.back_bitmap.id = back_bitmap_id;
|
2026-04-07 13:35:26 -03:00
|
|
|
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)));
|
2026-03-23 17:58:03 -03:00
|
|
|
}
|
2024-06-20 15:34:51 -03:00
|
|
|
|
2025-09-10 08:23:15 -03:00
|
|
|
void ViewImplementation::update_zoom()
|
|
|
|
|
{
|
2026-01-05 22:05:10 -03:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-05 22:05:10 -03:00
|
|
|
client().async_set_zoom_level(m_client_state.page_index, m_zoom_level);
|
2025-09-10 08:23:15 -03:00
|
|
|
}
|
|
|
|
|
|
2026-05-04 15:59:22 -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();
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-17 12:50:57 -03:00
|
|
|
void ViewImplementation::handle_resize()
|
|
|
|
|
{
|
2026-03-16 21:28:02 -03:00
|
|
|
client().async_set_viewport(page_id(), viewport_size(), m_device_pixel_ratio, m_is_fullscreen);
|
2026-05-25 21:56:25 -03:00
|
|
|
Application::the().update_compositor_viewport(client().compositor_context_id_for_page(page_id()), viewport_size().to_type<int>(), Web::Compositor::WindowResizingInProgress::Yes);
|
2023-05-15 02:59:51 -03:00
|
|
|
}
|
|
|
|
|
|
2024-11-13 18:35:17 -03:00
|
|
|
void ViewImplementation::initialize_client(CreateNewClient create_new_client)
|
|
|
|
|
{
|
2026-05-26 13:03:07 -03:00
|
|
|
m_needs_beforeunload_check = true;
|
|
|
|
|
|
2024-11-13 18:35:17 -03:00
|
|
|
if (create_new_client == CreateNewClient::Yes) {
|
2026-06-13 11:06:49 -03:00
|
|
|
auto client_handle = m_client_state.client_handle;
|
2024-11-13 18:35:17 -03:00
|
|
|
m_client_state = {};
|
2026-06-13 11:06:49 -03:00
|
|
|
m_client_state.client_handle = move(client_handle);
|
2024-11-13 18:35:17 -03:00
|
|
|
|
2025-03-09 19:01:25 -03:00
|
|
|
// 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();
|
2024-11-13 18:35:17 -03:00
|
|
|
} else {
|
|
|
|
|
m_client_state.client->register_view(m_client_state.page_index, *this);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-13 11:06:49 -03:00
|
|
|
if (m_client_state.client_handle.is_empty())
|
|
|
|
|
m_client_state.client_handle = Web::Crypto::generate_random_uuid();
|
2024-11-13 18:35:17 -03:00
|
|
|
client().async_set_window_handle(m_client_state.page_index, m_client_state.client_handle);
|
2026-01-05 22:05:10 -03:00
|
|
|
client().async_set_zoom_level(m_client_state.page_index, m_zoom_level);
|
2026-03-16 21:28:02 -03:00
|
|
|
client().async_set_viewport(m_client_state.page_index, viewport_size(), m_device_pixel_ratio, m_is_fullscreen);
|
2025-07-25 09:10:00 -03:00
|
|
|
client().async_set_maximum_frames_per_second(m_client_state.page_index, m_maximum_frames_per_second);
|
2024-11-13 18:35:17 -03:00
|
|
|
client().async_set_system_visibility_state(m_client_state.page_index, m_system_visibility_state);
|
2026-05-24 16:51:33 -03:00
|
|
|
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>());
|
2026-02-01 15:27:19 -03:00
|
|
|
client().async_set_document_cookie_version_buffer(m_client_state.page_index, m_document_cookie_version_buffer);
|
2024-11-13 18:35:17 -03:00
|
|
|
|
2026-03-22 15:59:08 -03:00
|
|
|
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);
|
2024-11-13 18:35:17 -03:00
|
|
|
|
2025-09-03 10:00:52 -03:00
|
|
|
Application::the().apply_view_options({}, *this);
|
2025-03-29 09:19:00 -03:00
|
|
|
|
2025-08-23 13:15:31 -03:00
|
|
|
default_zoom_level_factor_changed();
|
2025-04-03 14:53:02 -03:00
|
|
|
languages_changed();
|
2026-04-13 11:55:24 -03:00
|
|
|
browsing_behavior_changed();
|
2025-03-29 09:19:00 -03:00
|
|
|
autoplay_settings_changed();
|
2025-04-02 10:30:34 -03:00
|
|
|
global_privacy_control_changed();
|
2026-01-15 10:28:42 -03:00
|
|
|
|
|
|
|
|
// If DevTools is connected, notify the new WebContent process.
|
|
|
|
|
if (m_devtools_connected)
|
|
|
|
|
client().async_did_connect_devtools_client(page_id());
|
2024-11-13 18:35:17 -03:00
|
|
|
}
|
|
|
|
|
|
2026-06-13 11:06:49 -03:00
|
|
|
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;
|
|
|
|
|
|
2026-06-16 08:03:40 -03:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-13 11:06:49 -03:00
|
|
|
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();
|
2026-06-16 08:03:40 -03:00
|
|
|
m_session_history_entry_url_loading_from_ui_process.clear();
|
2026-06-13 11:06:49 -03:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-16 08:03:40 -03:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-13 11:06:49 -03:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-16 08:03:40 -03:00
|
|
|
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();
|
2026-06-13 11:06:49 -03:00
|
|
|
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();
|
2026-06-15 06:27:47 -03:00
|
|
|
auto allow_current_entry_reconstruction = m_pending_web_content_session_history_seed.should_reseed_after_current_history_load
|
|
|
|
|
? AllowCurrentEntryReconstruction::Yes
|
|
|
|
|
: AllowCurrentEntryReconstruction::No;
|
2026-06-13 11:06:49 -03:00
|
|
|
m_pending_web_content_session_history_seed.should_reseed_after_current_history_load = false;
|
2026-06-15 06:27:47 -03:00
|
|
|
seed_web_content_session_history_from_ui_process(allow_current_entry_reconstruction);
|
2026-06-13 11:06:49 -03:00
|
|
|
} else {
|
2026-06-15 06:27:47 -03:00
|
|
|
// 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.
|
2026-06-13 11:06:49 -03:00
|
|
|
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 {
|
2026-06-16 08:03:40 -03:00
|
|
|
m_session_history_entry_url_loading_from_ui_process.clear();
|
2026-06-13 11:06:49 -03:00
|
|
|
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()
|
|
|
|
|
{
|
2026-06-16 08:03:40 -03:00
|
|
|
m_session_history_entry_url_loading_from_ui_process.clear();
|
2026-06-13 11:06:49 -03:00
|
|
|
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);
|
|
|
|
|
|
2026-06-16 08:03:40 -03:00
|
|
|
auto view_id = m_view_id;
|
2026-06-13 11:06:49 -03:00
|
|
|
for (auto request_id : request_ids) {
|
2026-06-16 08:03:40 -03:00
|
|
|
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 {});
|
2026-06-13 11:06:49 -03:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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);
|
2026-06-16 08:03:40 -03:00
|
|
|
serialized.set("loadingSessionHistoryEntryFromUI"sv, m_session_history_entry_url_loading_from_ui_process.has_value());
|
2026-06-13 11:06:49 -03:00
|
|
|
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());
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-15 06:27:47 -03:00
|
|
|
void ViewImplementation::seed_web_content_session_history_from_ui_process(AllowCurrentEntryReconstruction allow_current_entry_reconstruction)
|
2026-06-13 11:06:49 -03:00
|
|
|
{
|
|
|
|
|
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));
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-15 06:27:47 -03:00
|
|
|
// 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);
|
2026-06-13 11:06:49 -03:00
|
|
|
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();
|
2026-06-15 06:27:47 -03:00
|
|
|
// 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.
|
2026-06-13 11:06:49 -03:00
|
|
|
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) {
|
2026-06-16 08:03:40 -03:00
|
|
|
m_session_history_entry_url_loading_from_ui_process = m_url;
|
2026-06-13 11:06:49 -03:00
|
|
|
client().async_load_url(page_id(), m_url, Web::Bindings::NavigationHistoryBehavior::Auto);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-16 08:03:40 -03:00
|
|
|
m_session_history_entry_url_loading_from_ui_process = current_entry->url;
|
2026-06-13 11:06:49 -03:00
|
|
|
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;
|
2026-06-15 06:27:47 -03:00
|
|
|
// 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;
|
2026-06-13 11:06:49 -03:00
|
|
|
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) {
|
2026-06-15 06:27:47 -03:00
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-13 11:06:49 -03:00
|
|
|
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;
|
2026-06-15 06:27:47 -03:00
|
|
|
// NB: A seed ack can arrive while a top-level navigation is still blocked. Keep the mirror provisional until that
|
|
|
|
|
// navigation settles.
|
2026-06-13 11:06:49 -03:00
|
|
|
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;
|
|
|
|
|
}
|
2026-06-14 04:58:59 -03:00
|
|
|
if (!m_pending_session_history_traversal->will_change_top_level_entry)
|
|
|
|
|
complete_webdriver_pending_navigation_if_url_matches(m_url);
|
2026-06-13 11:06:49 -03:00
|
|
|
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();
|
2026-06-16 08:03:40 -03:00
|
|
|
m_session_history_entry_url_loading_from_ui_process.clear();
|
2026-06-13 11:06:49 -03:00
|
|
|
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(),
|
2026-06-16 08:03:40 -03:00
|
|
|
m_session_history_entry_url_loading_from_ui_process.has_value(),
|
2026-06-13 11:06:49 -03:00
|
|
|
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));
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-12 10:43:37 -03:00
|
|
|
void ViewImplementation::handle_web_content_process_crash(LoadErrorPage load_error_page)
|
2023-05-17 10:53:13 -03:00
|
|
|
{
|
2026-01-12 20:15:04 -03:00
|
|
|
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");
|
|
|
|
|
}
|
2023-05-17 10:53:13 -03:00
|
|
|
|
2023-05-27 13:46:45 -03:00
|
|
|
++m_crash_count;
|
|
|
|
|
constexpr size_t max_reasonable_crash_count = 5U;
|
|
|
|
|
if (m_crash_count >= max_reasonable_crash_count) {
|
2026-01-12 20:15:04 -03:00
|
|
|
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;
|
2023-05-27 13:46:45 -03:00
|
|
|
}
|
|
|
|
|
m_repeated_crash_timer->restart();
|
|
|
|
|
|
2026-01-12 20:15:04 -03:00
|
|
|
// In headless mode, respawn WebContent but skip the error page.
|
|
|
|
|
if (headless_mode)
|
|
|
|
|
load_error_page = LoadErrorPage::No;
|
|
|
|
|
|
2024-01-30 13:12:14 -03:00
|
|
|
initialize_client();
|
2023-05-17 10:53:13 -03:00
|
|
|
VERIFY(m_client_state.client);
|
|
|
|
|
|
|
|
|
|
// Don't keep a stale backup bitmap around.
|
2026-04-07 13:35:26 -03:00
|
|
|
m_backup_shared_image_buffer = nullptr;
|
2023-05-17 10:53:13 -03:00
|
|
|
|
2026-06-13 11:06:49 -03:00
|
|
|
if (should_manage_session_history_in_ui_process()) {
|
2026-06-16 08:03:40 -03:00
|
|
|
m_session_history_entry_url_loading_from_ui_process.clear();
|
2026-06-13 11:06:49 -03:00
|
|
|
prepare_to_seed_web_content_session_history_from_ui_process();
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-17 10:53:13 -03:00
|
|
|
handle_resize();
|
2024-11-12 10:43:37 -03:00
|
|
|
|
|
|
|
|
if (load_error_page == LoadErrorPage::Yes) {
|
2026-04-24 09:36:55 -03:00
|
|
|
auto escaped_url = escape_html_entities(m_url.serialize());
|
|
|
|
|
|
2024-11-12 10:43:37 -03:00
|
|
|
StringBuilder builder;
|
2026-05-20 11:42:08 -03:00
|
|
|
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);
|
2026-04-24 09:36:55 -03:00
|
|
|
builder.append(ERROR_HTML_FOOTER);
|
2026-05-20 11:42:08 -03:00
|
|
|
load_crash_page_html(builder.string_view(), m_url);
|
2026-06-13 11:06:49 -03:00
|
|
|
} 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();
|
2023-05-17 10:53:13 -03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-23 13:15:31 -03:00
|
|
|
void ViewImplementation::default_zoom_level_factor_changed()
|
|
|
|
|
{
|
2026-05-04 15:59:22 -03:00
|
|
|
apply_zoom_for_current_host();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ViewImplementation::zoom_per_host_changed(StringView host)
|
|
|
|
|
{
|
|
|
|
|
if (current_host() != host)
|
|
|
|
|
return;
|
|
|
|
|
apply_zoom_for_current_host();
|
2025-08-23 13:15:31 -03:00
|
|
|
}
|
|
|
|
|
|
2025-04-03 14:53:02 -03:00
|
|
|
void ViewImplementation::languages_changed()
|
|
|
|
|
{
|
|
|
|
|
auto const& languages = Application::settings().languages();
|
|
|
|
|
client().async_set_preferred_languages(page_id(), languages);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-13 11:55:24 -03:00
|
|
|
void ViewImplementation::browsing_behavior_changed()
|
|
|
|
|
{
|
|
|
|
|
auto const& browsing_behavior = Application::settings().browsing_behavior();
|
|
|
|
|
client().async_set_browsing_behavior(page_id(), browsing_behavior);
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-29 09:19:00 -03:00
|
|
|
void ViewImplementation::autoplay_settings_changed()
|
|
|
|
|
{
|
|
|
|
|
auto const& autoplay_settings = Application::settings().autoplay_settings();
|
|
|
|
|
auto const& web_content_options = Application::web_content_options();
|
|
|
|
|
|
2026-06-09 13:55:09 -03:00
|
|
|
auto policy = autoplay_settings.policy;
|
|
|
|
|
if (web_content_options.enable_autoplay == EnableAutoplay::Yes)
|
|
|
|
|
policy = Web::HTML::AutoplayPolicy::AllowAudioAndVideo;
|
|
|
|
|
|
|
|
|
|
client().async_set_autoplay_settings(page_id(), policy, autoplay_settings.site_filters.values());
|
2025-03-29 09:19:00 -03:00
|
|
|
}
|
|
|
|
|
|
2025-04-02 10:30:34 -03:00
|
|
|
void ViewImplementation::global_privacy_control_changed()
|
2025-04-02 10:47:44 -03:00
|
|
|
{
|
2025-04-02 10:30:34 -03:00
|
|
|
auto global_privacy_control = Application::settings().global_privacy_control();
|
|
|
|
|
client().async_set_enable_global_privacy_control(page_id(), global_privacy_control == GlobalPrivacyControl::Yes);
|
2025-04-02 10:47:44 -03:00
|
|
|
}
|
|
|
|
|
|
2026-03-22 12:44:54 -03:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-29 14:30:45 -03:00
|
|
|
static ErrorOr<LexicalPath> save_screenshot(Gfx::Bitmap const* bitmap)
|
2023-12-06 13:51:44 -03:00
|
|
|
{
|
2025-05-29 14:30:45 -03:00
|
|
|
if (!bitmap)
|
2024-09-05 08:06:15 -03:00
|
|
|
return Error::from_string_literal("Failed to take a screenshot");
|
2023-12-06 13:51:44 -03:00
|
|
|
|
2026-05-12 19:57:55 -03:00
|
|
|
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);
|
|
|
|
|
}());
|
2023-12-06 13:51:44 -03:00
|
|
|
|
2025-05-29 14:30:45 -03:00
|
|
|
auto encoded = TRY(Gfx::PNGWriter::encode(*bitmap));
|
2023-12-06 13:51:44 -03:00
|
|
|
|
2023-12-12 09:25:06 -03:00
|
|
|
auto dump_file = TRY(Core::File::open(path.string(), Core::File::OpenMode::Write));
|
|
|
|
|
TRY(dump_file->write_until_depleted(encoded));
|
2023-12-06 13:51:44 -03:00
|
|
|
|
2023-12-09 18:57:57 -03:00
|
|
|
return path;
|
2023-12-06 13:51:44 -03:00
|
|
|
}
|
|
|
|
|
|
2023-12-31 11:51:02 -03:00
|
|
|
NonnullRefPtr<Core::Promise<LexicalPath>> ViewImplementation::take_screenshot(ScreenshotType type)
|
2023-05-17 10:53:13 -03:00
|
|
|
{
|
2023-12-31 11:51:02 -03:00
|
|
|
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
|
2023-12-31 11:51:02 -03:00
|
|
|
// to allow spamming screenshot requests for some reason.
|
|
|
|
|
promise->reject(Error::from_string_literal("A screenshot request is already in progress"));
|
|
|
|
|
return promise;
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-17 10:53:13 -03:00
|
|
|
switch (type) {
|
2026-04-07 13:35:26 -03:00
|
|
|
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) {
|
2025-05-29 14:30:45 -03:00
|
|
|
if (auto result = save_screenshot(visible_bitmap); result.is_error())
|
2023-12-31 11:51:02 -03:00
|
|
|
promise->reject(result.release_error());
|
|
|
|
|
else
|
|
|
|
|
promise->resolve(result.release_value());
|
|
|
|
|
}
|
2023-05-17 10:53:13 -03:00
|
|
|
break;
|
2026-04-07 13:35:26 -03:00
|
|
|
}
|
2023-12-31 11:51:02 -03:00
|
|
|
|
2023-05-17 10:53:13 -03:00
|
|
|
case ScreenshotType::Full:
|
2023-12-31 11:51:02 -03:00
|
|
|
m_pending_screenshot = promise;
|
2024-02-02 22:00:48 -03:00
|
|
|
client().async_take_document_screenshot(page_id());
|
2023-05-17 10:53:13 -03:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-31 11:51:02 -03:00
|
|
|
return promise;
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-20 05:37:44 -03:00
|
|
|
NonnullRefPtr<Core::Promise<LexicalPath>> ViewImplementation::take_dom_node_screenshot(Web::UniqueNodeID node_id)
|
2023-12-31 11:51:02 -03:00
|
|
|
{
|
|
|
|
|
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
|
2023-12-31 11:51:02 -03:00
|
|
|
// 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;
|
2024-02-02 22:00:48 -03:00
|
|
|
client().async_take_dom_node_screenshot(page_id(), node_id);
|
2023-12-31 11:51:02 -03:00
|
|
|
|
|
|
|
|
return promise;
|
2023-12-06 13:51:44 -03:00
|
|
|
}
|
2023-05-17 10:53:13 -03:00
|
|
|
|
2023-12-31 11:51:02 -03:00
|
|
|
void ViewImplementation::did_receive_screenshot(Badge<WebContentClient>, Gfx::ShareableBitmap const& screenshot)
|
2023-12-06 13:51:44 -03:00
|
|
|
{
|
2023-12-31 11:51:02 -03:00
|
|
|
VERIFY(m_pending_screenshot);
|
|
|
|
|
|
2025-05-29 14:30:45 -03:00
|
|
|
if (auto result = save_screenshot(screenshot.bitmap()); result.is_error())
|
2023-12-31 11:51:02 -03:00
|
|
|
m_pending_screenshot->reject(result.release_error());
|
|
|
|
|
else
|
|
|
|
|
m_pending_screenshot->resolve(result.release_value());
|
|
|
|
|
|
|
|
|
|
m_pending_screenshot = nullptr;
|
2023-05-17 10:53:13 -03:00
|
|
|
}
|
|
|
|
|
|
2024-09-19 11:40:00 -03:00
|
|
|
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.
|
2024-09-19 11:40:00 -03:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-01 16:18:43 -03:00
|
|
|
void ViewImplementation::did_receive_internal_page_info(Badge<WebContentClient>, PageInfoType, Optional<Core::AnonymousBuffer> const& info)
|
2024-09-19 11:40:00 -03:00
|
|
|
{
|
|
|
|
|
VERIFY(m_pending_info_request);
|
|
|
|
|
|
2026-02-01 16:18:43 -03:00
|
|
|
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));
|
2024-09-19 11:40:00 -03:00
|
|
|
m_pending_info_request = nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-12 09:25:06 -03:00
|
|
|
ErrorOr<LexicalPath> ViewImplementation::dump_gc_graph()
|
|
|
|
|
{
|
2024-09-19 11:40:00 -03:00
|
|
|
auto promise = request_internal_page_info(PageInfoType::GCGraph);
|
|
|
|
|
auto gc_graph_json = TRY(promise->await());
|
2023-12-12 09:25:06 -03:00
|
|
|
|
|
|
|
|
LexicalPath path { Core::StandardPaths::tempfile_directory() };
|
2026-02-01 16:52:16 -03:00
|
|
|
path = path.append(TRY(AK::UnixDateTime::now().to_string("gc-graph-%Y-%m-%d-%H-%M-%S.js"sv)));
|
2023-12-12 09:25:06 -03:00
|
|
|
|
2026-02-01 16:52:16 -03:00
|
|
|
// Write as a .js file so gc-heap-explorer.html can load it via <script> tag (avoiding CORS issues with file:// URLs)
|
2024-09-19 11:40:00 -03:00
|
|
|
auto dump_file = TRY(Core::File::open(path.string(), Core::File::OpenMode::Write));
|
2026-02-01 16:52:16 -03:00
|
|
|
TRY(dump_file->write_until_depleted("var GC_GRAPH_DUMP = "sv.bytes()));
|
2024-09-19 11:40:00 -03:00
|
|
|
TRY(dump_file->write_until_depleted(gc_graph_json.bytes()));
|
2026-02-01 16:52:16 -03:00
|
|
|
TRY(dump_file->write_until_depleted(";\n"sv.bytes()));
|
2023-12-12 09:25:06 -03:00
|
|
|
|
|
|
|
|
return path;
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-08 13:00:03 -03:00
|
|
|
void ViewImplementation::set_user_style_sheet(String const& source)
|
2023-08-26 14:34:59 -03:00
|
|
|
{
|
2025-03-08 13:00:03 -03:00
|
|
|
client().async_set_user_style(page_id(), source);
|
2023-08-26 14:34:59 -03:00
|
|
|
}
|
|
|
|
|
|
2025-09-01 09:20:14 -03:00
|
|
|
void ViewImplementation::initialize_context_menus()
|
|
|
|
|
{
|
|
|
|
|
auto& application = Application::the();
|
|
|
|
|
|
|
|
|
|
m_navigate_back_action = Action::create("Go Back"sv, ActionID::NavigateBack, [this]() {
|
2026-06-13 11:06:49 -03:00
|
|
|
(void)traverse_the_history_by_delta(-1);
|
2025-09-01 09:20:14 -03:00
|
|
|
});
|
|
|
|
|
m_navigate_forward_action = Action::create("Go Forward"sv, ActionID::NavigateForward, [this]() {
|
2026-06-13 11:06:49 -03:00
|
|
|
(void)traverse_the_history_by_delta(+1);
|
2025-09-01 09:20:14 -03:00
|
|
|
});
|
|
|
|
|
m_navigate_back_action->set_enabled(false);
|
|
|
|
|
m_navigate_forward_action->set_enabled(false);
|
|
|
|
|
|
2026-03-22 12:44:54 -03:00
|
|
|
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);
|
|
|
|
|
|
2025-09-01 09:20:14 -03:00
|
|
|
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());
|
|
|
|
|
|
2025-09-13 10:08:24 -03:00
|
|
|
Application::the().open_url_in_new_tab(*url, Web::HTML::ActivateTab::Yes);
|
2025-09-01 09:20:14 -03:00
|
|
|
});
|
|
|
|
|
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]() {
|
2025-09-13 10:08:24 -03:00
|
|
|
Application::the().open_url_in_new_tab(m_context_menu_url, Web::HTML::ActivateTab::No);
|
2025-09-01 09:20:14 -03:00
|
|
|
});
|
2026-02-17 16:23:45 -03:00
|
|
|
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);
|
|
|
|
|
});
|
2025-09-01 09:20:14 -03:00
|
|
|
m_copy_url_action = Action::create("Copy URL"sv, ActionID::CopyURL, [this]() {
|
2025-09-18 09:16:36 -03:00
|
|
|
Application::the().insert_clipboard_entry({ url_text_to_copy(m_context_menu_url), "text/plain"_string });
|
2025-09-01 09:20:14 -03:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
m_open_image_action = Action::create("Open Image"sv, ActionID::OpenImage, [this]() {
|
2025-09-13 10:08:24 -03:00
|
|
|
load(m_context_menu_url);
|
2025-09-01 09:20:14 -03:00
|
|
|
});
|
2026-02-03 10:00:15 -03:00
|
|
|
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());
|
|
|
|
|
});
|
2025-09-01 09:20:14 -03:00
|
|
|
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;
|
|
|
|
|
|
2025-09-18 09:16:36 -03:00
|
|
|
Application::the().insert_clipboard_entry({ ByteString { encoded.value().bytes() }, "image/png"_string });
|
2025-09-01 09:20:14 -03:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
m_open_audio_action = Action::create("Open Audio"sv, ActionID::OpenAudio, [this]() {
|
2025-09-13 10:08:24 -03:00
|
|
|
load(m_context_menu_url);
|
2025-09-01 09:20:14 -03:00
|
|
|
});
|
|
|
|
|
m_open_video_action = Action::create("Open Video"sv, ActionID::OpenVideo, [this]() {
|
2025-09-13 10:08:24 -03:00
|
|
|
load(m_context_menu_url);
|
2025-09-01 09:20:14 -03:00
|
|
|
});
|
|
|
|
|
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());
|
|
|
|
|
});
|
2025-09-16 13:55:37 -03:00
|
|
|
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]() {
|
2025-09-01 09:20:14 -03:00
|
|
|
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());
|
|
|
|
|
});
|
2026-02-28 11:36:32 -03:00
|
|
|
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());
|
|
|
|
|
});
|
2025-09-01 09:20:14 -03:00
|
|
|
|
|
|
|
|
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();
|
2026-05-06 13:00:59 -03:00
|
|
|
m_page_context_menu->add_action(application.cut_selection_action());
|
2025-09-01 09:20:14 -03:00
|
|
|
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);
|
2026-02-17 16:23:45 -03:00
|
|
|
m_link_context_menu->add_action(*m_open_in_new_window_action);
|
2025-09-01 09:20:14 -03:00
|
|
|
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();
|
2026-02-03 10:00:15 -03:00
|
|
|
m_image_context_menu->add_action(*m_save_image_action);
|
|
|
|
|
m_image_context_menu->add_separator();
|
2025-09-01 09:20:14 -03:00
|
|
|
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);
|
2025-09-16 13:55:37 -03:00
|
|
|
m_media_context_menu->add_action(*m_media_show_controls_action);
|
|
|
|
|
m_media_context_menu->add_action(*m_media_hide_controls_action);
|
2025-09-01 09:20:14 -03:00
|
|
|
m_media_context_menu->add_action(*m_media_loop_action);
|
2026-02-28 11:36:32 -03:00
|
|
|
m_media_context_menu->add_action(*m_media_enter_fullscreen_action);
|
|
|
|
|
m_media_context_menu->add_action(*m_media_exit_fullscreen_action);
|
2025-09-01 09:20:14 -03:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-06 13:00:59 -03:00
|
|
|
void ViewImplementation::did_request_page_context_menu(Badge<WebContentClient>, Gfx::IntPoint content_position, Web::ContextMenuForInputEventsTarget for_input_events_target)
|
2025-09-01 09:20:14 -03:00
|
|
|
{
|
2026-05-06 13:00:59 -03:00
|
|
|
auto& cut_selection_action = Application::the().cut_selection_action();
|
|
|
|
|
cut_selection_action.set_visible(for_input_events_target == Web::ContextMenuForInputEventsTarget::Yes);
|
|
|
|
|
|
2025-09-01 09:20:14 -03:00
|
|
|
auto const& search_engine = Application::settings().search_engine();
|
2026-05-06 13:00:59 -03:00
|
|
|
m_search_text = search_engine.has_value() ? selected_text_with_whitespace_collapsed() : OptionalNone {};
|
2025-09-01 09:20:14 -03:00
|
|
|
|
2026-05-06 13:00:59 -03:00
|
|
|
ScopeGuard guard { [&]() {
|
|
|
|
|
cut_selection_action.set_visible(true);
|
|
|
|
|
m_search_text.clear();
|
|
|
|
|
} };
|
2025-09-01 09:20:14 -03:00
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
2025-09-16 13:55:37 -03:00
|
|
|
m_media_show_controls_action->set_visible(!menu.has_user_agent_controls);
|
|
|
|
|
m_media_hide_controls_action->set_visible(menu.has_user_agent_controls);
|
|
|
|
|
|
2025-09-01 09:20:14 -03:00
|
|
|
m_media_loop_action->set_checked(menu.is_looping);
|
|
|
|
|
|
2026-02-28 11:36:32 -03:00
|
|
|
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);
|
|
|
|
|
|
2025-09-01 09:20:14 -03:00
|
|
|
if (m_media_context_menu->on_activation)
|
|
|
|
|
m_media_context_menu->on_activation(to_widget_position(content_position));
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-14 15:03:16 -03:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-07 12:35:05 -03:00
|
|
|
void ViewImplementation::request_close()
|
|
|
|
|
{
|
2026-05-26 13:03:07 -03:00
|
|
|
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);
|
|
|
|
|
};
|
2025-11-07 12:35:05 -03:00
|
|
|
}
|
|
|
|
|
|
2023-01-12 16:27:17 -03:00
|
|
|
}
|