1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-05-31 12:28:12 +00:00

Ladybird+LibWebView+WebContent: Make the screenshot IPCs async

These IPCs are different than other IPCs in that we can't just set up a
callback function to be invoked when WebContent sends us the screenshot
data. There are multiple places that would set that callback, and they
would step on each other's toes.

Instead, the screenshot APIs on ViewImplementation now return a Promise
which callers can interact with to receive the screenshot (or an error).
This commit is contained in:
Timothy Flynn 2023-12-31 09:51:02 -05:00 committed by Andreas Kling
parent 93db790974
commit d8fa226a8f
13 changed files with 121 additions and 47 deletions

View file

@ -791,14 +791,16 @@ static void copy_data_to_clipboard(StringView data, NSPasteboardType pasteboard_
- (void)takeVisibleScreenshot:(id)sender - (void)takeVisibleScreenshot:(id)sender
{ {
auto result = m_web_view_bridge->take_screenshot(WebView::ViewImplementation::ScreenshotType::Visible); m_web_view_bridge->take_screenshot(WebView::ViewImplementation::ScreenshotType::Visible)->when_rejected([](auto const& error) {
(void)result; // FIXME: Display an error if this failed. (void)error; // FIXME: Display the error.
});
} }
- (void)takeFullScreenshot:(id)sender - (void)takeFullScreenshot:(id)sender
{ {
auto result = m_web_view_bridge->take_screenshot(WebView::ViewImplementation::ScreenshotType::Full); m_web_view_bridge->take_screenshot(WebView::ViewImplementation::ScreenshotType::Full)->when_rejected([](auto const& error) {
(void)result; // FIXME: Display an error if this failed. (void)error; // FIXME: Display the error.
});
} }
- (void)openLink:(id)sender - (void)openLink:(id)sender

View file

@ -310,19 +310,19 @@ Tab::Tab(BrowserWindow* window, WebContentOptions const& web_content_options, St
auto* take_visible_screenshot_action = new QAction("Take &Visible Screenshot", this); auto* take_visible_screenshot_action = new QAction("Take &Visible Screenshot", this);
take_visible_screenshot_action->setIcon(load_icon_from_uri("resource://icons/16x16/filetype-image.png"sv)); take_visible_screenshot_action->setIcon(load_icon_from_uri("resource://icons/16x16/filetype-image.png"sv));
QObject::connect(take_visible_screenshot_action, &QAction::triggered, this, [this]() { QObject::connect(take_visible_screenshot_action, &QAction::triggered, this, [this]() {
if (auto result = view().take_screenshot(WebView::ViewImplementation::ScreenshotType::Visible); result.is_error()) { view().take_screenshot(WebView::ViewImplementation::ScreenshotType::Visible)->when_rejected([this](auto const& error) {
auto error = MUST(String::formatted("{}", result.error())); auto error_message = MUST(String::formatted("{}", error));
QMessageBox::warning(this, "Ladybird", qstring_from_ak_string(error)); QMessageBox::warning(this, "Ladybird", qstring_from_ak_string(error_message));
} });
}); });
auto* take_full_screenshot_action = new QAction("Take &Full Screenshot", this); auto* take_full_screenshot_action = new QAction("Take &Full Screenshot", this);
take_full_screenshot_action->setIcon(load_icon_from_uri("resource://icons/16x16/filetype-image.png"sv)); take_full_screenshot_action->setIcon(load_icon_from_uri("resource://icons/16x16/filetype-image.png"sv));
QObject::connect(take_full_screenshot_action, &QAction::triggered, this, [this]() { QObject::connect(take_full_screenshot_action, &QAction::triggered, this, [this]() {
if (auto result = view().take_screenshot(WebView::ViewImplementation::ScreenshotType::Full); result.is_error()) { view().take_screenshot(WebView::ViewImplementation::ScreenshotType::Full)->when_rejected([this](auto const& error) {
auto error = MUST(String::formatted("{}", result.error())); auto error_message = MUST(String::formatted("{}", error));
QMessageBox::warning(this, "Ladybird", qstring_from_ak_string(error)); QMessageBox::warning(this, "Ladybird", qstring_from_ak_string(error_message));
} });
}); });
m_page_context_menu = new QMenu("Context menu", this); m_page_context_menu = new QMenu("Context menu", this);

View file

@ -657,20 +657,20 @@ Tab::Tab(BrowserWindow& window)
auto take_visible_screenshot_action = GUI::Action::create( auto take_visible_screenshot_action = GUI::Action::create(
"Take &Visible Screenshot"sv, g_icon_bag.filetype_image, [this](auto&) { "Take &Visible Screenshot"sv, g_icon_bag.filetype_image, [this](auto&) {
if (auto result = view().take_screenshot(WebView::ViewImplementation::ScreenshotType::Visible); result.is_error()) { view().take_screenshot(WebView::ViewImplementation::ScreenshotType::Visible)->when_rejected([this](auto const& error) {
auto error = String::formatted("{}", result.error()).release_value_but_fixme_should_propagate_errors(); auto error_message = MUST(String::formatted("{}", error));
GUI::MessageBox::show_error(&this->window(), error); GUI::MessageBox::show_error(&this->window(), error_message);
} });
}, },
this); this);
take_visible_screenshot_action->set_status_tip("Save a screenshot of the visible portion of the current tab to the Downloads directory"_string); take_visible_screenshot_action->set_status_tip("Save a screenshot of the visible portion of the current tab to the Downloads directory"_string);
auto take_full_screenshot_action = GUI::Action::create( auto take_full_screenshot_action = GUI::Action::create(
"Take &Full Screenshot"sv, g_icon_bag.filetype_image, [this](auto&) { "Take &Full Screenshot"sv, g_icon_bag.filetype_image, [this](auto&) {
if (auto result = view().take_screenshot(WebView::ViewImplementation::ScreenshotType::Full); result.is_error()) { view().take_screenshot(WebView::ViewImplementation::ScreenshotType::Full)->when_rejected([this](auto const& error) {
auto error = String::formatted("{}", result.error()).release_value_but_fixme_should_propagate_errors(); auto error_message = MUST(String::formatted("{}", error));
GUI::MessageBox::show_error(&this->window(), error); GUI::MessageBox::show_error(&this->window(), error_message);
} });
}, },
this); this);
take_full_screenshot_action->set_status_tip("Save a screenshot of the entirety of the current tab to the Downloads directory"_string); take_full_screenshot_action->set_status_tip("Save a screenshot of the entirety of the current tab to the Downloads directory"_string);

View file

@ -254,10 +254,13 @@ void InspectorClient::context_menu_screenshot_dom_node()
{ {
VERIFY(m_context_menu_data.has_value()); VERIFY(m_context_menu_data.has_value());
if (auto result = m_content_web_view.take_dom_node_screenshot(m_context_menu_data->dom_node_id); result.is_error()) m_content_web_view.take_dom_node_screenshot(m_context_menu_data->dom_node_id)
append_console_warning(MUST(String::formatted("Warning: {}", result.error()))); ->when_resolved([this](auto const& path) {
else append_console_message(MUST(String::formatted("Screenshot saved to: {}", path)));
append_console_message(MUST(String::formatted("Screenshot saved to: {}", result.value()))); })
.when_rejected([this](auto const& error) {
append_console_warning(MUST(String::formatted("Warning: {}", error)));
});
m_context_menu_data.clear(); m_context_menu_data.clear();
} }

View file

@ -370,27 +370,65 @@ static ErrorOr<LexicalPath> save_screenshot(Gfx::ShareableBitmap const& bitmap)
return path; return path;
} }
ErrorOr<LexicalPath> ViewImplementation::take_screenshot(ScreenshotType type) 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; Gfx::ShareableBitmap bitmap;
switch (type) { switch (type) {
case ScreenshotType::Visible: 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* visible_bitmap = m_client_state.has_usable_bitmap ? m_client_state.front_bitmap.bitmap.ptr() : m_backup_bitmap.ptr()) {
bitmap = visible_bitmap->to_shareable_bitmap(); 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; break;
case ScreenshotType::Full: case ScreenshotType::Full:
bitmap = client().take_document_screenshot(); m_pending_screenshot = promise;
client().async_take_document_screenshot();
break; break;
} }
return save_screenshot(bitmap); return promise;
} }
ErrorOr<LexicalPath> ViewImplementation::take_dom_node_screenshot(i32 node_id) NonnullRefPtr<Core::Promise<LexicalPath>> ViewImplementation::take_dom_node_screenshot(i32 node_id)
{ {
auto bitmap = client().take_dom_node_screenshot(node_id); auto promise = Core::Promise<LexicalPath>::construct();
return save_screenshot(bitmap);
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(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;
} }
ErrorOr<LexicalPath> ViewImplementation::dump_gc_graph() ErrorOr<LexicalPath> ViewImplementation::dump_gc_graph()

View file

@ -11,6 +11,7 @@
#include <AK/Function.h> #include <AK/Function.h>
#include <AK/LexicalPath.h> #include <AK/LexicalPath.h>
#include <AK/String.h> #include <AK/String.h>
#include <LibCore/Promise.h>
#include <LibGfx/Forward.h> #include <LibGfx/Forward.h>
#include <LibGfx/StandardCursor.h> #include <LibGfx/StandardCursor.h>
#include <LibWeb/Forward.h> #include <LibWeb/Forward.h>
@ -95,8 +96,9 @@ public:
Visible, Visible,
Full, Full,
}; };
ErrorOr<LexicalPath> take_screenshot(ScreenshotType); NonnullRefPtr<Core::Promise<LexicalPath>> take_screenshot(ScreenshotType);
ErrorOr<LexicalPath> take_dom_node_screenshot(i32); NonnullRefPtr<Core::Promise<LexicalPath>> take_dom_node_screenshot(i32);
virtual void did_receive_screenshot(Badge<WebContentClient>, Gfx::ShareableBitmap const&);
ErrorOr<LexicalPath> dump_gc_graph(); ErrorOr<LexicalPath> dump_gc_graph();
@ -229,6 +231,8 @@ protected:
size_t m_crash_count = 0; size_t m_crash_count = 0;
RefPtr<Core::Timer> m_repeated_crash_timer; RefPtr<Core::Timer> m_repeated_crash_timer;
RefPtr<Core::Promise<LexicalPath>> m_pending_screenshot;
}; };
} }

View file

@ -231,6 +231,11 @@ void WebContentClient::did_get_dom_node_html(String const& html)
m_view.on_received_dom_node_html(html); m_view.on_received_dom_node_html(html);
} }
void WebContentClient::did_take_screenshot(Gfx::ShareableBitmap const& screenshot)
{
m_view.did_receive_screenshot({}, screenshot);
}
void WebContentClient::did_output_js_console_message(i32 message_index) void WebContentClient::did_output_js_console_message(i32 message_index)
{ {
if (m_view.on_received_console_message) if (m_view.on_received_console_message)

View file

@ -58,6 +58,7 @@ private:
virtual void did_get_hovered_node_id(i32 node_id) override; virtual void did_get_hovered_node_id(i32 node_id) override;
virtual void did_finish_editing_dom_node(Optional<i32> const& node_id) override; virtual void did_finish_editing_dom_node(Optional<i32> const& node_id) override;
virtual void did_get_dom_node_html(String const& html) override; virtual void did_get_dom_node_html(String const& html) override;
virtual void did_take_screenshot(Gfx::ShareableBitmap const& screenshot) override;
virtual void did_output_js_console_message(i32 message_index) override; virtual void did_output_js_console_message(i32 message_index) override;
virtual void did_get_js_console_messages(i32 start_index, Vector<ByteString> const& message_types, Vector<ByteString> const& messages) override; virtual void did_get_js_console_messages(i32 start_index, Vector<ByteString> const& message_types, Vector<ByteString> const& messages) override;
virtual void did_change_favicon(Gfx::ShareableBitmap const&) override; virtual void did_change_favicon(Gfx::ShareableBitmap const&) override;

View file

@ -854,11 +854,13 @@ void ConnectionFromClient::js_console_request_messages(i32 start_index)
m_top_level_document_console_client->send_messages(start_index); m_top_level_document_console_client->send_messages(start_index);
} }
Messages::WebContentServer::TakeDocumentScreenshotResponse ConnectionFromClient::take_document_screenshot() void ConnectionFromClient::take_document_screenshot()
{ {
auto* document = page().page().top_level_browsing_context().active_document(); auto* document = page().page().top_level_browsing_context().active_document();
if (!document || !document->document_element()) if (!document || !document->document_element()) {
return Gfx::ShareableBitmap {}; async_did_take_screenshot({});
return;
}
auto const& content_size = page().content_size(); auto const& content_size = page().content_size();
Web::DevicePixelRect rect { { 0, 0 }, content_size }; Web::DevicePixelRect rect { { 0, 0 }, content_size };
@ -866,21 +868,23 @@ Messages::WebContentServer::TakeDocumentScreenshotResponse ConnectionFromClient:
auto bitmap = Gfx::Bitmap::create(Gfx::BitmapFormat::BGRA8888, rect.size().to_type<int>()).release_value_but_fixme_should_propagate_errors(); auto bitmap = Gfx::Bitmap::create(Gfx::BitmapFormat::BGRA8888, rect.size().to_type<int>()).release_value_but_fixme_should_propagate_errors();
page().paint(rect, *bitmap); page().paint(rect, *bitmap);
return bitmap->to_shareable_bitmap(); async_did_take_screenshot(bitmap->to_shareable_bitmap());
} }
Messages::WebContentServer::TakeDomNodeScreenshotResponse ConnectionFromClient::take_dom_node_screenshot(i32 node_id) void ConnectionFromClient::take_dom_node_screenshot(i32 node_id)
{ {
auto* dom_node = Web::DOM::Node::from_unique_id(node_id); auto* dom_node = Web::DOM::Node::from_unique_id(node_id);
if (!dom_node || !dom_node->paintable_box()) if (!dom_node || !dom_node->paintable_box()) {
return Gfx::ShareableBitmap {}; async_did_take_screenshot({});
return;
}
auto rect = page().page().enclosing_device_rect(dom_node->paintable_box()->absolute_border_box_rect()); auto rect = page().page().enclosing_device_rect(dom_node->paintable_box()->absolute_border_box_rect());
auto bitmap = Gfx::Bitmap::create(Gfx::BitmapFormat::BGRA8888, rect.size().to_type<int>()).release_value_but_fixme_should_propagate_errors(); auto bitmap = Gfx::Bitmap::create(Gfx::BitmapFormat::BGRA8888, rect.size().to_type<int>()).release_value_but_fixme_should_propagate_errors();
page().paint(rect, *bitmap, { .paint_overlay = Web::PaintOptions::PaintOverlay::No }); page().paint(rect, *bitmap, { .paint_overlay = Web::PaintOptions::PaintOverlay::No });
return bitmap->to_shareable_bitmap(); async_did_take_screenshot(bitmap->to_shareable_bitmap());
} }
Messages::WebContentServer::DumpGcGraphResponse ConnectionFromClient::dump_gc_graph() Messages::WebContentServer::DumpGcGraphResponse ConnectionFromClient::dump_gc_graph()

View file

@ -121,8 +121,8 @@ private:
virtual void enable_inspector_prototype() override; virtual void enable_inspector_prototype() override;
virtual Messages::WebContentServer::TakeDocumentScreenshotResponse take_document_screenshot() override; virtual void take_document_screenshot() override;
virtual Messages::WebContentServer::TakeDomNodeScreenshotResponse take_dom_node_screenshot(i32 node_id) override; virtual void take_dom_node_screenshot(i32 node_id) override;
virtual Messages::WebContentServer::DumpGcGraphResponse dump_gc_graph() override; virtual Messages::WebContentServer::DumpGcGraphResponse dump_gc_graph() override;

View file

@ -48,6 +48,8 @@ endpoint WebContentClient
did_finish_editing_dom_node(Optional<i32> node_id) =| did_finish_editing_dom_node(Optional<i32> node_id) =|
did_get_dom_node_html(String html) =| did_get_dom_node_html(String html) =|
did_take_screenshot(Gfx::ShareableBitmap screenshot) =|
did_change_favicon(Gfx::ShareableBitmap favicon) =| did_change_favicon(Gfx::ShareableBitmap favicon) =|
did_request_all_cookies(URL url) => (Vector<Web::Cookie::Cookie> cookies) did_request_all_cookies(URL url) => (Vector<Web::Cookie::Cookie> cookies)
did_request_named_cookie(URL url, ByteString name) => (Optional<Web::Cookie::Cookie> cookie) did_request_named_cookie(URL url, ByteString name) => (Optional<Web::Cookie::Cookie> cookie)

View file

@ -54,8 +54,8 @@ endpoint WebContentServer
remove_dom_node(i32 node_id) =| remove_dom_node(i32 node_id) =|
get_dom_node_html(i32 node_id) =| get_dom_node_html(i32 node_id) =|
take_document_screenshot() => (Gfx::ShareableBitmap data) take_document_screenshot() =|
take_dom_node_screenshot(i32 node_id) => (Gfx::ShareableBitmap data) take_dom_node_screenshot(i32 node_id) =|
dump_gc_graph() => (String json) dump_gc_graph() => (String json)

View file

@ -96,7 +96,21 @@ public:
RefPtr<Gfx::Bitmap> take_screenshot() RefPtr<Gfx::Bitmap> take_screenshot()
{ {
return client().take_document_screenshot().bitmap(); VERIFY(!m_pending_screenshot);
m_pending_screenshot = Core::Promise<RefPtr<Gfx::Bitmap>>::construct();
client().async_take_document_screenshot();
auto screenshot = MUST(m_pending_screenshot->await());
m_pending_screenshot = nullptr;
return screenshot;
}
virtual void did_receive_screenshot(Badge<WebView::WebContentClient>, Gfx::ShareableBitmap const& screenshot) override
{
VERIFY(m_pending_screenshot);
m_pending_screenshot->resolve(screenshot.bitmap());
} }
ErrorOr<String> dump_layout_tree() ErrorOr<String> dump_layout_tree()
@ -145,6 +159,7 @@ private:
private: private:
Gfx::IntRect m_viewport_rect; Gfx::IntRect m_viewport_rect;
RefPtr<Core::Promise<RefPtr<Gfx::Bitmap>>> m_pending_screenshot;
}; };
static ErrorOr<NonnullRefPtr<Core::Timer>> load_page_for_screenshot_and_exit(Core::EventLoop& event_loop, HeadlessWebContentView& view, int screenshot_timeout) static ErrorOr<NonnullRefPtr<Core::Timer>> load_page_for_screenshot_and_exit(Core::EventLoop& event_loop, HeadlessWebContentView& view, int screenshot_timeout)