LibWebView+UI: Add an Application method to open a URL in a new tab

This lets us avoid each UI needing to handle link clicks directly, and
lets actions stored in LibWebView avoid awkwardly going through the link
click callbacks to open URLs.
This commit is contained in:
Timothy Flynn 2025-09-13 09:08:24 -04:00 committed by Tim Flynn
parent c2365653a5
commit ce331cbcd5
Notes: github-actions[bot] 2025-09-18 11:28:58 +00:00
20 changed files with 62 additions and 170 deletions

View file

@ -278,6 +278,12 @@ ErrorOr<void> Application::initialize(Main::Arguments const& arguments)
return {};
}
void Application::open_url_in_new_tab(URL::URL const& url, Web::HTML::ActivateTab activate_tab) const
{
if (auto view = open_blank_new_tab(activate_tab); view.has_value())
view->load(url);
}
static ErrorOr<NonnullRefPtr<WebContentClient>> create_web_content_client(Optional<ViewImplementation&> view)
{
auto request_server_socket = TRY(connect_new_request_server_client());

View file

@ -21,6 +21,7 @@
#include <LibWeb/CSS/PreferredColorScheme.h>
#include <LibWeb/CSS/PreferredContrast.h>
#include <LibWeb/CSS/PreferredMotion.h>
#include <LibWeb/HTML/ActivateTab.h>
#include <LibWebView/Forward.h>
#include <LibWebView/Options.h>
#include <LibWebView/Process.h>
@ -56,7 +57,10 @@ public:
static ProcessManager& process_manager() { return *the().m_process_manager; }
ErrorOr<NonnullRefPtr<WebContentClient>> launch_web_content_process(ViewImplementation&);
virtual Optional<ViewImplementation&> active_web_view() const { return {}; }
virtual Optional<ViewImplementation&> open_blank_new_tab(Web::HTML::ActivateTab) const { return {}; }
void open_url_in_new_tab(URL::URL const&, Web::HTML::ActivateTab) const;
void add_child_process(Process&&);

View file

@ -158,11 +158,6 @@ 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());
@ -834,8 +829,7 @@ void ViewImplementation::initialize_context_menus()
auto url = URL::Parser::basic_parse(url_string);
VERIFY(url.has_value());
if (on_link_click)
on_link_click(*url, "_blank"sv, 0);
Application::the().open_url_in_new_tab(*url, Web::HTML::ActivateTab::Yes);
});
m_search_selected_text_action->set_visible(false);
@ -861,16 +855,14 @@ void ViewImplementation::initialize_context_menus()
});
m_open_in_new_tab_action = Action::create("Open in New Tab"sv, ActionID::OpenInNewTab, [this]() {
if (on_link_click)
on_link_click(m_context_menu_url, {}, Web::UIEvents::Mod_PlatformCtrl);
Application::the().open_url_in_new_tab(m_context_menu_url, Web::HTML::ActivateTab::No);
});
m_copy_url_action = Action::create("Copy URL"sv, ActionID::CopyURL, [this]() {
insert_text_into_clipboard(url_text_to_copy(m_context_menu_url));
});
m_open_image_action = Action::create("Open Image"sv, ActionID::OpenImage, [this]() {
if (on_link_click)
on_link_click(m_context_menu_url, {}, {});
load(m_context_menu_url);
});
m_copy_image_action = Action::create("Copy Image"sv, ActionID::CopyImage, [this]() {
if (!m_image_context_menu_bitmap.has_value())
@ -889,12 +881,10 @@ void ViewImplementation::initialize_context_menus()
});
m_open_audio_action = Action::create("Open Audio"sv, ActionID::OpenAudio, [this]() {
if (on_link_click)
on_link_click(m_context_menu_url, {}, {});
load(m_context_menu_url);
});
m_open_video_action = Action::create("Open Video"sv, ActionID::OpenVideo, [this]() {
if (on_link_click)
on_link_click(m_context_menu_url, {}, {});
load(m_context_menu_url);
});
m_media_play_action = Action::create("Play"sv, ActionID::PlayMedia, [this]() {
client().async_toggle_media_play_state(page_id());

View file

@ -63,7 +63,6 @@ public:
void load(URL::URL const&);
void load_html(StringView);
void load_empty_document();
void reload();
void traverse_the_history_by_delta(int delta);
@ -172,8 +171,6 @@ public:
Function<void()> on_close;
Function<void(URL::URL const&)> on_link_hover;
Function<void()> on_link_unhover;
Function<void(URL::URL const&, ByteString const& target, unsigned modifiers)> on_link_click;
Function<void(URL::URL const&, ByteString const& target, unsigned modifiers)> on_link_middle_click;
Function<void(Utf16String const&)> on_title_change;
Function<void(URL::URL const&)> on_url_change;
Function<void(URL::URL const&, bool)> on_load_start;
@ -191,7 +188,6 @@ public:
Function<void(String const& message)> on_request_set_prompt_text;
Function<void()> on_request_accept_dialog;
Function<void()> on_request_dismiss_dialog;
Function<void(URL::URL const&, URL::URL const&, String const&)> on_received_source;
Function<void(JsonObject)> on_received_dom_tree;
Function<void(DOMNodeProperties)> on_received_dom_node_properties;
Function<void(JsonObject)> on_received_accessibility_tree;

View file

@ -8,6 +8,7 @@
#include <LibWebView/Application.h>
#include <LibWebView/CookieJar.h>
#include <LibWebView/HelperProcess.h>
#include <LibWebView/SourceHighlighter.h>
#include <LibWebView/ViewImplementation.h>
#include <LibWebView/WebContentClient.h>
#include <LibWebView/WebUI.h>
@ -244,18 +245,17 @@ void WebContentClient::did_unhover_link(u64 page_id)
void WebContentClient::did_click_link(u64 page_id, URL::URL url, ByteString target, unsigned modifiers)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_link_click)
view->on_link_click(url, target, modifiers);
}
if (modifiers == Web::UIEvents::Mod_PlatformCtrl)
Application::the().open_url_in_new_tab(url, Web::HTML::ActivateTab::No);
else if (target == "_blank"sv)
Application::the().open_url_in_new_tab(url, Web::HTML::ActivateTab::Yes);
else if (auto view = view_for_page_id(page_id); view.has_value())
view->load(url);
}
void WebContentClient::did_middle_click_link(u64 page_id, URL::URL url, ByteString target, unsigned modifiers)
void WebContentClient::did_middle_click_link(u64, URL::URL url, ByteString, unsigned)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_link_middle_click)
view->on_link_middle_click(url, target, modifiers);
}
Application::the().open_url_in_new_tab(url, Web::HTML::ActivateTab::No);
}
void WebContentClient::did_request_context_menu(u64 page_id, Gfx::IntPoint content_position)
@ -282,11 +282,11 @@ void WebContentClient::did_request_media_context_menu(u64 page_id, Gfx::IntPoint
view->did_request_media_context_menu({}, content_position, move(menu));
}
void WebContentClient::did_get_source(u64 page_id, URL::URL url, URL::URL base_url, String source)
void WebContentClient::did_get_source(u64, URL::URL url, URL::URL base_url, String source)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_received_source)
view->on_received_source(url, base_url, source);
if (auto view = Application::the().open_blank_new_tab(Web::HTML::ActivateTab::Yes); view.has_value()) {
auto html = highlight_source(url, base_url, source, Syntax::Language::HTML, WebView::HighlightOutputMode::FullDocument);
view->load_html(html);
}
}

View file

@ -21,6 +21,7 @@ private:
virtual NonnullOwnPtr<Core::EventLoop> create_platform_event_loop() override;
virtual Optional<WebView::ViewImplementation&> active_web_view() const override;
virtual Optional<WebView::ViewImplementation&> open_blank_new_tab(Web::HTML::ActivateTab) const override;
virtual Optional<ByteString> ask_user_for_download_folder() const override;
virtual void display_download_confirmation_dialog(StringView download_name, LexicalPath const& path) const override;

View file

@ -13,6 +13,7 @@
#import <Application/ApplicationDelegate.h>
#import <Interface/LadybirdWebView.h>
#import <Interface/Tab.h>
#import <Interface/TabController.h>
#if !__has_feature(objc_arc)
# error "This project requires ARC"
@ -41,6 +42,16 @@ Optional<WebView::ViewImplementation&> Application::active_web_view() const
return {};
}
Optional<WebView::ViewImplementation&> Application::open_blank_new_tab(Web::HTML::ActivateTab activate_tab) const
{
ApplicationDelegate* delegate = [NSApp delegate];
auto* controller = [delegate createNewTab:activate_tab fromTab:[delegate activeTab]];
auto* tab = (Tab*)[controller window];
return [[tab web_view] view];
}
Optional<ByteString> Application::ask_user_for_download_folder() const
{
auto* panel = [NSOpenPanel openPanel];

View file

@ -20,12 +20,10 @@
- (nullable instancetype)init;
- (nonnull TabController*)createNewTab:(Optional<URL::URL> const&)url
fromTab:(nullable Tab*)tab
activateTab:(Web::HTML::ActivateTab)activate_tab;
- (nonnull TabController*)createNewTab:(Web::HTML::ActivateTab)activate_tab
fromTab:(nullable Tab*)tab;
- (nonnull TabController*)createNewTab:(StringView)html
url:(URL::URL const&)url
- (nonnull TabController*)createNewTab:(Optional<URL::URL> const&)url
fromTab:(nullable Tab*)tab
activateTab:(Web::HTML::ActivateTab)activate_tab;

View file

@ -67,6 +67,17 @@
#pragma mark - Public methods
- (nonnull TabController*)createNewTab:(Web::HTML::ActivateTab)activate_tab
fromTab:(nullable Tab*)tab
{
auto* controller = [[TabController alloc] init];
[self initializeTabController:controller
activateTab:activate_tab
fromTab:tab];
return controller;
}
- (TabController*)createNewTab:(Optional<URL::URL> const&)url
fromTab:(Tab*)tab
activateTab:(Web::HTML::ActivateTab)activate_tab
@ -80,17 +91,6 @@
return controller;
}
- (nonnull TabController*)createNewTab:(StringView)html
url:(URL::URL const&)url
fromTab:(nullable Tab*)tab
activateTab:(Web::HTML::ActivateTab)activate_tab
{
auto* controller = [self createNewTab:activate_tab fromTab:tab];
[controller loadHTML:html url:url];
return controller;
}
- (nonnull TabController*)createChildTab:(Optional<URL::URL> const&)url
fromTab:(nonnull Tab*)tab
activateTab:(Web::HTML::ActivateTab)activate_tab
@ -164,17 +164,6 @@
[controller focusLocationToolbarItem];
}
- (nonnull TabController*)createNewTab:(Web::HTML::ActivateTab)activate_tab
fromTab:(nullable Tab*)tab
{
auto* controller = [[TabController alloc] init];
[self initializeTabController:controller
activateTab:activate_tab
fromTab:tab];
return controller;
}
- (nonnull TabController*)createChildTab:(Web::HTML::ActivateTab)activate_tab
fromTab:(nonnull Tab*)tab
pageIndex:(u64)page_index

View file

@ -21,15 +21,10 @@
- (String const&)onCreateNewTab:(Optional<URL::URL> const&)url
activateTab:(Web::HTML::ActivateTab)activate_tab;
- (String const&)onCreateNewTab:(StringView)html
url:(URL::URL const&)url
activateTab:(Web::HTML::ActivateTab)activate_tab;
- (String const&)onCreateChildTab:(Optional<URL::URL> const&)url
activateTab:(Web::HTML::ActivateTab)activate_tab
pageIndex:(u64)page_index;
- (void)loadURL:(URL::URL const&)url;
- (void)onLoadStart:(URL::URL const&)url isRedirect:(BOOL)is_redirect;
- (void)onLoadFinish:(URL::URL const&)url;
@ -51,7 +46,6 @@
pageIndex:(u64)page_index;
- (void)loadURL:(URL::URL const&)url;
- (void)loadHTML:(StringView)html;
- (WebView::ViewImplementation&)view;
- (String const&)handle;

View file

@ -8,7 +8,6 @@
#include <Interface/LadybirdWebViewBridge.h>
#include <LibURL/URL.h>
#include <LibWeb/HTML/SelectedFile.h>
#include <LibWebView/SourceHighlighter.h>
#import <Application/ApplicationDelegate.h>
#import <Interface/Event.h>
@ -136,11 +135,6 @@ struct HideCursor {
m_web_view_bridge->load(url);
}
- (void)loadHTML:(StringView)html
{
m_web_view_bridge->load_html(html);
}
- (WebView::ViewImplementation&)view
{
return *m_web_view_bridge;
@ -349,7 +343,7 @@ static void copy_data_to_clipboard(StringView data, NSPasteboardType pasteboard_
}
if (auto urls = Ladybird::drag_event_url_list(event); !urls.is_empty()) {
[self.observer loadURL:urls[0]];
[self loadURL:urls[0]];
for (size_t i = 1; i < urls.size(); ++i) {
[self.observer onCreateNewTab:urls[i] activateTab:Web::HTML::ActivateTab::No];
@ -513,28 +507,6 @@ static void copy_data_to_clipboard(StringView data, NSPasteboardType pasteboard_
[self.status_label setHidden:YES];
};
m_web_view_bridge->on_link_click = [weak_self](auto const& url, auto const& target, unsigned modifiers) {
LadybirdWebView* self = weak_self;
if (self == nil) {
return;
}
if (modifiers == Web::UIEvents::KeyModifier::Mod_Super) {
[self.observer onCreateNewTab:url activateTab:Web::HTML::ActivateTab::No];
} else if (target == "_blank"sv) {
[self.observer onCreateNewTab:url activateTab:Web::HTML::ActivateTab::Yes];
} else {
[self.observer loadURL:url];
}
};
m_web_view_bridge->on_link_middle_click = [weak_self](auto url, auto, unsigned) {
LadybirdWebView* self = weak_self;
if (self == nil) {
return;
}
[self.observer onCreateNewTab:url activateTab:Web::HTML::ActivateTab::No];
};
m_web_view_bridge->on_request_alert = [weak_self](auto const& message) {
LadybirdWebView* self = weak_self;
if (self == nil) {
@ -848,18 +820,6 @@ static void copy_data_to_clipboard(StringView data, NSPasteboardType pasteboard_
m_web_view_bridge->did_update_window_rect();
};
m_web_view_bridge->on_received_source = [weak_self](auto const& url, auto const& base_url, auto const& source) {
LadybirdWebView* self = weak_self;
if (self == nil) {
return;
}
auto html = WebView::highlight_source(url, base_url, source, Syntax::Language::HTML, WebView::HighlightOutputMode::FullDocument);
[self.observer onCreateNewTab:html
url:url
activateTab:Web::HTML::ActivateTab::Yes];
};
m_web_view_bridge->on_theme_color_change = [weak_self](auto color) {
LadybirdWebView* self = weak_self;
if (self == nil) {

View file

@ -231,21 +231,6 @@ static constexpr CGFloat const WINDOW_HEIGHT = 800;
return [[tab web_view] handle];
}
- (String const&)onCreateNewTab:(StringView)html
url:(URL::URL const&)url
activateTab:(Web::HTML::ActivateTab)activate_tab
{
auto* delegate = (ApplicationDelegate*)[NSApp delegate];
auto* controller = [delegate createNewTab:html
url:url
fromTab:self
activateTab:activate_tab];
auto* tab = (Tab*)[controller window];
return [[tab web_view] handle];
}
- (String const&)onCreateChildTab:(Optional<URL::URL> const&)url
activateTab:(Web::HTML::ActivateTab)activate_tab
pageIndex:(u64)page_index
@ -261,11 +246,6 @@ static constexpr CGFloat const WINDOW_HEIGHT = 800;
return [[tab web_view] handle];
}
- (void)loadURL:(URL::URL const&)url
{
[[self tabController] loadURL:url];
}
- (void)onLoadStart:(URL::URL const&)url isRedirect:(BOOL)is_redirect
{
self.title = Ladybird::string_to_ns_string(url.serialize());

View file

@ -20,7 +20,6 @@
pageIndex:(u64)page_index;
- (void)loadURL:(URL::URL const&)url;
- (void)loadHTML:(StringView)html url:(URL::URL const&)url;
- (void)onLoadStart:(URL::URL const&)url isRedirect:(BOOL)isRedirect;

View file

@ -135,11 +135,6 @@ static NSString* const TOOLBAR_TAB_OVERVIEW_IDENTIFIER = @"ToolbarTabOverviewIde
[[self tab].web_view loadURL:url];
}
- (void)loadHTML:(StringView)html url:(URL::URL const&)url
{
[[self tab].web_view loadHTML:html];
}
- (void)onLoadStart:(URL::URL const&)url isRedirect:(BOOL)isRedirect
{
[self setLocationFieldText:url.serialize()];

View file

@ -97,6 +97,12 @@ Optional<WebView::ViewImplementation&> Application::active_web_view() const
return {};
}
Optional<WebView::ViewImplementation&> Application::open_blank_new_tab(Web::HTML::ActivateTab activate_tab) const
{
auto& tab = active_window().create_new_tab(activate_tab);
return tab.view();
}
Optional<ByteString> Application::ask_user_for_download_folder() const
{
auto path = QFileDialog::getExistingDirectory(nullptr, "Select download directory", QDir::homePath());

View file

@ -37,6 +37,7 @@ private:
virtual NonnullOwnPtr<Core::EventLoop> create_platform_event_loop() override;
virtual Optional<WebView::ViewImplementation&> active_web_view() const override;
virtual Optional<WebView::ViewImplementation&> open_blank_new_tab(Web::HTML::ActivateTab) const override;
virtual Optional<ByteString> ask_user_for_download_folder() const override;
virtual void display_download_confirmation_dialog(StringView download_name, LexicalPath const& path) const override;

View file

@ -350,13 +350,6 @@ Tab& BrowserWindow::new_tab_from_url(URL::URL const& url, Web::HTML::ActivateTab
return tab;
}
Tab& BrowserWindow::new_tab_from_content(StringView html, Web::HTML::ActivateTab activate_tab)
{
auto& tab = create_new_tab(activate_tab);
tab.load_html(html);
return tab;
}
Tab& BrowserWindow::new_child_tab(Web::HTML::ActivateTab activate_tab, Tab& parent, Optional<u64> page_index)
{
return create_new_tab(activate_tab, parent, page_index);
@ -423,27 +416,6 @@ void BrowserWindow::initialize_tab(Tab* tab)
return new_tab.view().handle();
};
tab->view().on_tab_open_request = [this](auto url, auto activate_tab) {
auto& tab = new_tab_from_url(url, activate_tab);
return tab.view().handle();
};
tab->view().on_link_click = [this](auto url, auto target, unsigned modifiers) {
// TODO: maybe activate tabs according to some configuration, this is just normal current browser behavior
if (modifiers == Web::UIEvents::Mod_Ctrl) {
m_current_tab->view().on_tab_open_request(url, Web::HTML::ActivateTab::No);
} else if (target == "_blank") {
m_current_tab->view().on_tab_open_request(url, Web::HTML::ActivateTab::Yes);
} else {
m_current_tab->view().load(url);
}
};
tab->view().on_link_middle_click = [this](auto url, auto target, unsigned modifiers) {
m_current_tab->view().on_link_click(url, target, Web::UIEvents::Mod_Ctrl);
(void)modifiers;
};
m_tabs_container->setTabIcon(m_tabs_container->indexOf(tab), tab->favicon());
create_close_button_for_tab(tab);
}

View file

@ -58,7 +58,6 @@ public slots:
void tab_favicon_changed(int index, QIcon const& icon);
void tab_audio_play_state_changed(int index, Web::HTML::AudioPlayState);
Tab& new_tab_from_url(URL::URL const&, Web::HTML::ActivateTab);
Tab& new_tab_from_content(StringView html, Web::HTML::ActivateTab);
Tab& new_child_tab(Web::HTML::ActivateTab, Tab& parent, Optional<u64> page_index);
void activate_tab(int index);
void close_tab(int index);

View file

@ -9,7 +9,6 @@
#include <LibURL/URL.h>
#include <LibWeb/HTML/SelectedFile.h>
#include <LibWebView/Application.h>
#include <LibWebView/SourceHighlighter.h>
#include <UI/Qt/BrowserWindow.h>
#include <UI/Qt/Icon.h>
#include <UI/Qt/Menu.h>
@ -329,11 +328,6 @@ Tab::Tab(BrowserWindow* window, RefPtr<WebView::WebContentClient> parent_client,
QObject::connect(focus_location_editor_action, &QAction::triggered, this, &Tab::focus_location_editor);
view().on_received_source = [this](auto const& url, auto const& base_url, auto const& source) {
auto html = WebView::highlight_source(url, base_url, source, Syntax::Language::HTML, WebView::HighlightOutputMode::FullDocument);
m_window->new_tab_from_content(html, Web::HTML::ActivateTab::Yes);
};
view().on_restore_window = [this]() {
m_window->showNormal();
};

View file

@ -15,7 +15,6 @@
#include <LibGfx/Rect.h>
#include <LibURL/URL.h>
#include <LibWeb/Forward.h>
#include <LibWeb/HTML/ActivateTab.h>
#include <LibWebView/ViewImplementation.h>
#include <QMenu>
@ -40,8 +39,6 @@ public:
WebContentView(QWidget* window, RefPtr<WebView::WebContentClient> parent_client = nullptr, size_t page_index = 0, WebContentViewInitialState initial_state = {});
virtual ~WebContentView() override;
Function<String(URL::URL const&, Web::HTML::ActivateTab)> on_tab_open_request;
virtual void paintEvent(QPaintEvent*) override;
virtual void resizeEvent(QResizeEvent*) override;
virtual void leaveEvent(QEvent* event) override;