ladybird/Libraries/LibWebView/ViewImplementation.cpp
Timothy Flynn 72905c84d5 LibWeb+LibWebView+WebContent: Support both inspecting/highlighting nodes
Our own Inspector differs from most other DevTools implementations with
regard to highlighting DOM nodes as you hover elements in the inspected
DOM tree. In other implementations, as you change the hovered node, the
browser will render a box model overlay onto the page for that node. We
currently don't do this; we wait until you click the node, at which
point we both paint the overlay and inspect the node's properties.

This patch does not change that behavior, but separates the IPCs and
internal tracking of inspected nodes to support the standard DevTools
behavior. So the DOM document now stores an inspected node and a
highlighted node. The former is used for features such as "$0" in the
JavaScript console, and the latter is used for the box model overlay.
Our Inspector continues to set these to the same node.
2025-02-24 12:05:29 -05:00

754 lines
24 KiB
C++

/*
* Copyright (c) 2023, Linus Groh <linusg@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/Error.h>
#include <AK/String.h>
#include <LibCore/DateTime.h>
#include <LibCore/StandardPaths.h>
#include <LibCore/Timer.h>
#include <LibGfx/ImageFormats/PNGWriter.h>
#include <LibWeb/Crypto/Crypto.h>
#include <LibWeb/Infra/Strings.h>
#include <LibWebView/Application.h>
#include <LibWebView/HelperProcess.h>
#include <LibWebView/UserAgent.h>
#include <LibWebView/ViewImplementation.h>
#ifdef AK_OS_MACOS
# include <LibCore/IOSurface.h>
# include <LibCore/MachPort.h>
#endif
namespace WebView {
static HashMap<u64, ViewImplementation*> s_all_views;
static u64 s_view_count = 1; // This has to start at 1 for Firefox DevTools.
void ViewImplementation::for_each_view(Function<IterationDecision(ViewImplementation&)> callback)
{
for (auto& view : s_all_views) {
if (callback(*view.value) == IterationDecision::Break)
break;
}
}
Optional<ViewImplementation&> ViewImplementation::find_view_by_id(u64 id)
{
if (auto view = s_all_views.get(id); view.has_value())
return *view.value();
return {};
}
ViewImplementation::ViewImplementation()
: m_view_id(s_view_count++)
{
s_all_views.set(m_view_id, this);
m_repeated_crash_timer = Core::Timer::create_single_shot(1000, [this] {
// Reset the "crashing a lot" counter after 1 second in case we just
// happen to be visiting crashy websites a lot.
this->m_crash_count = 0;
});
on_request_file = [this](auto const& path, auto request_id) {
auto file = Core::File::open(path, Core::File::OpenMode::Read);
if (file.is_error())
client().async_handle_file_return(page_id(), file.error().code(), {}, request_id);
else
client().async_handle_file_return(page_id(), 0, IPC::File::adopt_file(file.release_value()), request_id);
};
}
ViewImplementation::~ViewImplementation()
{
s_all_views.remove(m_view_id);
if (m_client_state.client)
m_client_state.client->unregister_view(m_client_state.page_index);
}
WebContentClient& ViewImplementation::client()
{
VERIFY(m_client_state.client);
return *m_client_state.client;
}
WebContentClient const& ViewImplementation::client() const
{
VERIFY(m_client_state.client);
return *m_client_state.client;
}
u64 ViewImplementation::page_id() const
{
VERIFY(m_client_state.client);
return m_client_state.page_index;
}
void ViewImplementation::server_did_paint(Badge<WebContentClient>, i32 bitmap_id, Gfx::IntSize size)
{
if (m_client_state.back_bitmap.id == bitmap_id) {
m_client_state.has_usable_bitmap = true;
m_client_state.back_bitmap.last_painted_size = size.to_type<Web::DevicePixels>();
swap(m_client_state.back_bitmap, m_client_state.front_bitmap);
m_backup_bitmap = nullptr;
if (on_ready_to_paint)
on_ready_to_paint();
}
client().async_ready_to_paint(page_id());
}
void ViewImplementation::set_window_position(Gfx::IntPoint position)
{
client().async_set_window_position(m_client_state.page_index, position.to_type<Web::DevicePixels>());
}
void ViewImplementation::set_window_size(Gfx::IntSize size)
{
client().async_set_window_size(m_client_state.page_index, size.to_type<Web::DevicePixels>());
}
void ViewImplementation::did_update_window_rect()
{
client().async_did_update_window_rect(m_client_state.page_index);
}
void ViewImplementation::set_system_visibility_state(Web::HTML::VisibilityState visibility_state)
{
m_system_visibility_state = visibility_state;
client().async_set_system_visibility_state(m_client_state.page_index, m_system_visibility_state);
}
void ViewImplementation::load(URL::URL const& url)
{
m_url = url;
client().async_load_url(page_id(), url);
}
void ViewImplementation::load_html(StringView html)
{
client().async_load_html(page_id(), html);
}
void ViewImplementation::load_empty_document()
{
load_html(""sv);
}
void ViewImplementation::reload()
{
client().async_reload(page_id());
}
void ViewImplementation::traverse_the_history_by_delta(int delta)
{
client().async_traverse_the_history_by_delta(page_id(), delta);
}
void ViewImplementation::zoom_in()
{
if (m_zoom_level >= ZOOM_MAX_LEVEL)
return;
m_zoom_level = round_to<int>((m_zoom_level + ZOOM_STEP) * 100) / 100.0f;
update_zoom();
}
void ViewImplementation::zoom_out()
{
if (m_zoom_level <= ZOOM_MIN_LEVEL)
return;
m_zoom_level = round_to<int>((m_zoom_level - ZOOM_STEP) * 100) / 100.0f;
update_zoom();
}
void ViewImplementation::set_zoom(double zoom_level)
{
m_zoom_level = max(ZOOM_MIN_LEVEL, min(zoom_level, ZOOM_MAX_LEVEL));
update_zoom();
}
void ViewImplementation::reset_zoom()
{
m_zoom_level = 1.0f;
update_zoom();
}
void ViewImplementation::enqueue_input_event(Web::InputEvent event)
{
// Send the next event over to the WebContent to be handled by JS. We'll later get a message to say whether JS
// prevented the default event behavior, at which point we either discard or handle that event, and then try to
// process the next one.
m_pending_input_events.enqueue(move(event));
m_pending_input_events.tail().visit(
[this](Web::KeyEvent const& event) {
client().async_key_event(m_client_state.page_index, event.clone_without_chrome_data());
},
[this](Web::MouseEvent const& event) {
client().async_mouse_event(m_client_state.page_index, event.clone_without_chrome_data());
},
[this](Web::DragEvent& event) {
auto cloned_event = event.clone_without_chrome_data();
cloned_event.files = move(event.files);
client().async_drag_event(m_client_state.page_index, move(cloned_event));
});
}
void ViewImplementation::did_finish_handling_input_event(Badge<WebContentClient>, Web::EventResult event_result)
{
auto event = m_pending_input_events.dequeue();
if (event_result == Web::EventResult::Handled)
return;
// Here we handle events that were not consumed or cancelled by the WebContent. Propagate the event back
// to the concrete view implementation.
event.visit(
[this](Web::KeyEvent const& event) {
if (on_finish_handling_key_event)
on_finish_handling_key_event(event);
},
[this](Web::DragEvent const& event) {
if (on_finish_handling_drag_event)
on_finish_handling_drag_event(event);
},
[](auto const&) {});
}
void ViewImplementation::set_preferred_color_scheme(Web::CSS::PreferredColorScheme color_scheme)
{
client().async_set_preferred_color_scheme(page_id(), color_scheme);
}
void ViewImplementation::set_preferred_contrast(Web::CSS::PreferredContrast contrast)
{
client().async_set_preferred_contrast(page_id(), contrast);
}
void ViewImplementation::set_preferred_motion(Web::CSS::PreferredMotion motion)
{
client().async_set_preferred_motion(page_id(), motion);
}
void ViewImplementation::set_preferred_languages(Vector<String> preferred_languages)
{
client().async_set_preferred_languages(page_id(), move(preferred_languages));
}
void ViewImplementation::set_enable_do_not_track(bool enable)
{
client().async_set_enable_do_not_track(page_id(), enable);
}
void ViewImplementation::set_enable_autoplay(bool enable)
{
if (enable) {
client().async_set_autoplay_allowed_on_all_websites(page_id());
} else {
client().async_set_autoplay_allowlist(page_id(), {});
}
}
ByteString ViewImplementation::selected_text()
{
return client().get_selected_text(page_id());
}
Optional<String> ViewImplementation::selected_text_with_whitespace_collapsed()
{
auto selected_text = MUST(Web::Infra::strip_and_collapse_whitespace(this->selected_text()));
if (selected_text.is_empty())
return OptionalNone {};
return selected_text;
}
void ViewImplementation::select_all()
{
client().async_select_all(page_id());
}
void ViewImplementation::paste(String const& text)
{
client().async_paste(page_id(), text);
}
void ViewImplementation::find_in_page(String const& query, CaseSensitivity case_sensitivity)
{
client().async_find_in_page(page_id(), query, case_sensitivity);
}
void ViewImplementation::find_in_page_next_match()
{
client().async_find_in_page_next_match(page_id());
}
void ViewImplementation::find_in_page_previous_match()
{
client().async_find_in_page_previous_match(page_id());
}
void ViewImplementation::get_source()
{
client().async_get_source(page_id());
}
void ViewImplementation::inspect_dom_tree()
{
client().async_inspect_dom_tree(page_id());
}
void ViewImplementation::inspect_accessibility_tree()
{
client().async_inspect_accessibility_tree(page_id());
}
void ViewImplementation::get_hovered_node_id()
{
client().async_get_hovered_node_id(page_id());
}
void ViewImplementation::inspect_dom_node(Web::UniqueNodeID node_id, Optional<Web::CSS::Selector::PseudoElement::Type> pseudo_element)
{
client().async_inspect_dom_node(page_id(), node_id, move(pseudo_element));
}
void ViewImplementation::clear_inspected_dom_node()
{
inspect_dom_node(0, {});
}
void ViewImplementation::highlight_dom_node(Web::UniqueNodeID node_id, Optional<Web::CSS::Selector::PseudoElement::Type> pseudo_element)
{
client().async_highlight_dom_node(page_id(), node_id, move(pseudo_element));
}
void ViewImplementation::clear_highlighted_dom_node()
{
highlight_dom_node(0, {});
}
void ViewImplementation::set_dom_node_text(Web::UniqueNodeID node_id, String text)
{
client().async_set_dom_node_text(page_id(), node_id, move(text));
}
void ViewImplementation::set_dom_node_tag(Web::UniqueNodeID node_id, String name)
{
client().async_set_dom_node_tag(page_id(), node_id, move(name));
}
void ViewImplementation::add_dom_node_attributes(Web::UniqueNodeID node_id, Vector<Attribute> attributes)
{
client().async_add_dom_node_attributes(page_id(), node_id, move(attributes));
}
void ViewImplementation::replace_dom_node_attribute(Web::UniqueNodeID node_id, String name, Vector<Attribute> replacement_attributes)
{
client().async_replace_dom_node_attribute(page_id(), node_id, move(name), move(replacement_attributes));
}
void ViewImplementation::create_child_element(Web::UniqueNodeID node_id)
{
client().async_create_child_element(page_id(), node_id);
}
void ViewImplementation::create_child_text_node(Web::UniqueNodeID node_id)
{
client().async_create_child_text_node(page_id(), node_id);
}
void ViewImplementation::clone_dom_node(Web::UniqueNodeID node_id)
{
client().async_clone_dom_node(page_id(), node_id);
}
void ViewImplementation::remove_dom_node(Web::UniqueNodeID node_id)
{
client().async_remove_dom_node(page_id(), node_id);
}
void ViewImplementation::get_dom_node_html(Web::UniqueNodeID node_id)
{
client().async_get_dom_node_html(page_id(), node_id);
}
void ViewImplementation::list_style_sheets()
{
client().async_list_style_sheets(page_id());
}
void ViewImplementation::request_style_sheet_source(Web::CSS::StyleSheetIdentifier const& identifier)
{
client().async_request_style_sheet_source(page_id(), identifier);
}
void ViewImplementation::debug_request(ByteString const& request, ByteString const& argument)
{
client().async_debug_request(page_id(), request, argument);
}
void ViewImplementation::run_javascript(StringView js_source)
{
client().async_run_javascript(page_id(), js_source);
}
void ViewImplementation::js_console_input(ByteString const& js_source)
{
client().async_js_console_input(page_id(), js_source);
}
void ViewImplementation::js_console_request_messages(i32 start_index)
{
client().async_js_console_request_messages(page_id(), start_index);
}
void ViewImplementation::alert_closed()
{
client().async_alert_closed(page_id());
}
void ViewImplementation::confirm_closed(bool accepted)
{
client().async_confirm_closed(page_id(), accepted);
}
void ViewImplementation::prompt_closed(Optional<String> response)
{
client().async_prompt_closed(page_id(), move(response));
}
void ViewImplementation::color_picker_update(Optional<Color> picked_color, Web::HTML::ColorPickerUpdateState state)
{
client().async_color_picker_update(page_id(), picked_color, state);
}
void ViewImplementation::file_picker_closed(Vector<Web::HTML::SelectedFile> selected_files)
{
client().async_file_picker_closed(page_id(), move(selected_files));
}
void ViewImplementation::select_dropdown_closed(Optional<u32> const& selected_item_id)
{
client().async_select_dropdown_closed(page_id(), selected_item_id);
}
void ViewImplementation::toggle_media_play_state()
{
client().async_toggle_media_play_state(page_id());
}
void ViewImplementation::toggle_media_mute_state()
{
client().async_toggle_media_mute_state(page_id());
}
void ViewImplementation::toggle_media_loop_state()
{
client().async_toggle_media_loop_state(page_id());
}
void ViewImplementation::toggle_media_controls_state()
{
client().async_toggle_media_controls_state(page_id());
}
void ViewImplementation::toggle_page_mute_state()
{
m_mute_state = Web::HTML::invert_mute_state(m_mute_state);
client().async_toggle_page_mute_state(page_id());
}
void ViewImplementation::did_change_audio_play_state(Badge<WebContentClient>, Web::HTML::AudioPlayState play_state)
{
bool state_changed = false;
switch (play_state) {
case Web::HTML::AudioPlayState::Paused:
if (--m_number_of_elements_playing_audio == 0) {
m_audio_play_state = play_state;
state_changed = true;
}
break;
case Web::HTML::AudioPlayState::Playing:
if (m_number_of_elements_playing_audio++ == 0) {
m_audio_play_state = play_state;
state_changed = true;
}
break;
}
if (state_changed && on_audio_play_state_changed)
on_audio_play_state_changed(m_audio_play_state);
}
void ViewImplementation::did_update_navigation_buttons_state(Badge<WebContentClient>, bool back_enabled, bool forward_enabled) const
{
if (on_navigation_buttons_state_changed)
on_navigation_buttons_state_changed(back_enabled, forward_enabled);
}
void ViewImplementation::did_allocate_backing_stores(Badge<WebContentClient>, i32 front_bitmap_id, Gfx::ShareableBitmap const& front_bitmap, i32 back_bitmap_id, Gfx::ShareableBitmap const& back_bitmap)
{
if (m_client_state.has_usable_bitmap) {
// NOTE: We keep the outgoing front bitmap as a backup so we have something to paint until we get a new one.
m_backup_bitmap = m_client_state.front_bitmap.bitmap;
m_backup_bitmap_size = m_client_state.front_bitmap.last_painted_size;
}
m_client_state.has_usable_bitmap = false;
m_client_state.front_bitmap.bitmap = front_bitmap.bitmap();
m_client_state.front_bitmap.id = front_bitmap_id;
m_client_state.back_bitmap.bitmap = back_bitmap.bitmap();
m_client_state.back_bitmap.id = back_bitmap_id;
}
#ifdef AK_OS_MACOS
void ViewImplementation::did_allocate_iosurface_backing_stores(i32 front_id, Core::MachPort&& front_port, i32 back_id, Core::MachPort&& back_port)
{
if (m_client_state.has_usable_bitmap) {
// NOTE: We keep the outgoing front bitmap as a backup so we have something to paint until we get a new one.
m_backup_bitmap = m_client_state.front_bitmap.bitmap;
m_backup_bitmap_size = m_client_state.front_bitmap.last_painted_size;
}
m_client_state.has_usable_bitmap = false;
auto front_iosurface = Core::IOSurfaceHandle::from_mach_port(move(front_port));
auto back_iosurface = Core::IOSurfaceHandle::from_mach_port(move(back_port));
auto front_size = Gfx::IntSize { front_iosurface.width(), front_iosurface.height() };
auto back_size = Gfx::IntSize { back_iosurface.width(), back_iosurface.height() };
auto bytes_per_row = front_iosurface.bytes_per_row();
auto front_bitmap = Gfx::Bitmap::create_wrapper(Gfx::BitmapFormat::BGRA8888, Gfx::AlphaType::Premultiplied, front_size, bytes_per_row, front_iosurface.data(), [handle = move(front_iosurface)] { });
auto back_bitmap = Gfx::Bitmap::create_wrapper(Gfx::BitmapFormat::BGRA8888, Gfx::AlphaType::Premultiplied, back_size, bytes_per_row, back_iosurface.data(), [handle = move(back_iosurface)] { });
m_client_state.front_bitmap.bitmap = front_bitmap.release_value_but_fixme_should_propagate_errors();
m_client_state.front_bitmap.id = front_id;
m_client_state.back_bitmap.bitmap = back_bitmap.release_value_but_fixme_should_propagate_errors();
m_client_state.back_bitmap.id = back_id;
}
#endif
void ViewImplementation::handle_resize()
{
client().async_set_viewport_size(page_id(), this->viewport_size());
}
void ViewImplementation::initialize_client(CreateNewClient create_new_client)
{
if (create_new_client == CreateNewClient::Yes) {
m_client_state = {};
// FIXME: Fail to open the tab, rather than crashing the whole application if these fail.
auto request_server_socket = connect_new_request_server_client().release_value_but_fixme_should_propagate_errors();
auto image_decoder_socket = connect_new_image_decoder_client().release_value_but_fixme_should_propagate_errors();
m_client_state.client = launch_web_content_process(*this, AK::move(image_decoder_socket), AK::move(request_server_socket)).release_value_but_fixme_should_propagate_errors();
} else {
m_client_state.client->register_view(m_client_state.page_index, *this);
}
m_client_state.client->on_web_content_process_crash = [this] {
Core::deferred_invoke([this] {
handle_web_content_process_crash();
if (on_web_content_crashed)
on_web_content_crashed();
});
};
m_client_state.client_handle = MUST(Web::Crypto::generate_random_uuid());
client().async_set_window_handle(m_client_state.page_index, m_client_state.client_handle);
client().async_set_device_pixels_per_css_pixel(m_client_state.page_index, m_device_pixel_ratio);
client().async_set_system_visibility_state(m_client_state.page_index, m_system_visibility_state);
if (auto webdriver_content_ipc_path = Application::chrome_options().webdriver_content_ipc_path; webdriver_content_ipc_path.has_value())
client().async_connect_to_webdriver(m_client_state.page_index, *webdriver_content_ipc_path);
if (Application::chrome_options().allow_popups == AllowPopups::Yes)
client().async_debug_request(m_client_state.page_index, "block-pop-ups"sv, "off"sv);
if (auto const& user_agent_preset = Application::web_content_options().user_agent_preset; user_agent_preset.has_value())
client().async_debug_request(m_client_state.page_index, "spoof-user-agent"sv, *user_agents.get(*user_agent_preset));
}
void ViewImplementation::handle_web_content_process_crash(LoadErrorPage load_error_page)
{
dbgln("\033[31;1mWebContent process crashed!\033[0m Last page loaded: {}", m_url);
dbgln("Consider raising an issue at https://github.com/LadybirdBrowser/ladybird/issues/new/choose");
++m_crash_count;
constexpr size_t max_reasonable_crash_count = 5U;
if (m_crash_count >= max_reasonable_crash_count) {
dbgln("WebContent has crashed {} times in quick succession! Not restarting...", m_crash_count);
m_repeated_crash_timer->stop();
return;
}
m_repeated_crash_timer->restart();
initialize_client();
VERIFY(m_client_state.client);
// Don't keep a stale backup bitmap around.
m_backup_bitmap = nullptr;
handle_resize();
if (load_error_page == LoadErrorPage::Yes) {
StringBuilder builder;
builder.append("<html><head><title>Crashed: "sv);
builder.append(escape_html_entities(m_url.to_byte_string()));
builder.append("</title></head><body>"sv);
builder.append("<h1>Web page crashed"sv);
if (m_url.host().has_value()) {
builder.appendff(" on {}", escape_html_entities(m_url.serialized_host()));
}
builder.append("</h1>"sv);
auto escaped_url = escape_html_entities(m_url.to_byte_string());
builder.appendff("The web page <a href=\"{}\">{}</a> has crashed.<br><br>You can reload the page to try again.", escaped_url, escaped_url);
builder.append("</body></html>"sv);
load_html(builder.to_byte_string());
}
}
static ErrorOr<LexicalPath> save_screenshot(Gfx::ShareableBitmap const& bitmap)
{
if (!bitmap.is_valid())
return Error::from_string_literal("Failed to take a screenshot");
auto file = Core::DateTime::now().to_byte_string("screenshot-%Y-%m-%d-%H-%M-%S.png"sv);
auto path = TRY(Application::the().path_for_downloaded_file(file));
auto encoded = TRY(Gfx::PNGWriter::encode(*bitmap.bitmap()));
auto dump_file = TRY(Core::File::open(path.string(), Core::File::OpenMode::Write));
TRY(dump_file->write_until_depleted(encoded));
return path;
}
NonnullRefPtr<Core::Promise<LexicalPath>> ViewImplementation::take_screenshot(ScreenshotType type)
{
auto promise = Core::Promise<LexicalPath>::construct();
if (m_pending_screenshot) {
// For simplicitly, only allow taking one screenshot at a time for now. Revisit if we need
// to allow spamming screenshot requests for some reason.
promise->reject(Error::from_string_literal("A screenshot request is already in progress"));
return promise;
}
Gfx::ShareableBitmap bitmap;
switch (type) {
case ScreenshotType::Visible:
if (auto* visible_bitmap = m_client_state.has_usable_bitmap ? m_client_state.front_bitmap.bitmap.ptr() : m_backup_bitmap.ptr()) {
if (auto result = save_screenshot(visible_bitmap->to_shareable_bitmap()); result.is_error())
promise->reject(result.release_error());
else
promise->resolve(result.release_value());
}
break;
case ScreenshotType::Full:
m_pending_screenshot = promise;
client().async_take_document_screenshot(page_id());
break;
}
return promise;
}
NonnullRefPtr<Core::Promise<LexicalPath>> ViewImplementation::take_dom_node_screenshot(Web::UniqueNodeID node_id)
{
auto promise = Core::Promise<LexicalPath>::construct();
if (m_pending_screenshot) {
// For simplicitly, only allow taking one screenshot at a time for now. Revisit if we need
// to allow spamming screenshot requests for some reason.
promise->reject(Error::from_string_literal("A screenshot request is already in progress"));
return promise;
}
m_pending_screenshot = promise;
client().async_take_dom_node_screenshot(page_id(), node_id);
return promise;
}
void ViewImplementation::did_receive_screenshot(Badge<WebContentClient>, Gfx::ShareableBitmap const& screenshot)
{
VERIFY(m_pending_screenshot);
if (auto result = save_screenshot(screenshot); result.is_error())
m_pending_screenshot->reject(result.release_error());
else
m_pending_screenshot->resolve(result.release_value());
m_pending_screenshot = nullptr;
}
NonnullRefPtr<Core::Promise<String>> ViewImplementation::request_internal_page_info(PageInfoType type)
{
auto promise = Core::Promise<String>::construct();
if (m_pending_info_request) {
// For simplicitly, only allow one info request at a time for now.
promise->reject(Error::from_string_literal("A page info request is already in progress"));
return promise;
}
m_pending_info_request = promise;
client().async_request_internal_page_info(page_id(), type);
return promise;
}
void ViewImplementation::did_receive_internal_page_info(Badge<WebContentClient>, PageInfoType, String const& info)
{
VERIFY(m_pending_info_request);
m_pending_info_request->resolve(String { info });
m_pending_info_request = nullptr;
}
ErrorOr<LexicalPath> ViewImplementation::dump_gc_graph()
{
auto promise = request_internal_page_info(PageInfoType::GCGraph);
auto gc_graph_json = TRY(promise->await());
LexicalPath path { Core::StandardPaths::tempfile_directory() };
path = path.append(TRY(Core::DateTime::now().to_string("gc-graph-%Y-%m-%d-%H-%M-%S.json"sv)));
auto dump_file = TRY(Core::File::open(path.string(), Core::File::OpenMode::Write));
TRY(dump_file->write_until_depleted(gc_graph_json.bytes()));
return path;
}
void ViewImplementation::set_user_style_sheet(String source)
{
client().async_set_user_style(page_id(), move(source));
}
void ViewImplementation::use_native_user_style_sheet()
{
extern String native_stylesheet_source;
set_user_style_sheet(native_stylesheet_source);
}
void ViewImplementation::enable_inspector_prototype()
{
client().async_enable_inspector_prototype(page_id());
}
}