mirror of
https://github.com/LadybirdBrowser/ladybird
synced 2026-04-25 17:25:08 +02:00
UI/Gtk: Add browser window, tabs, menus, and dialogs
Add the complete browser UI: - BrowserWindow: AdwHeaderBar with navigation, tab management via AdwTabView/AdwTabBar, find-in-page, fullscreen, zoom, D-Bus single-instance with open/activate handlers - Tab: WebContentView lifecycle, ViewImplementation callbacks for title, URL, favicon, cursor, tooltips, dialogs, window management - LadybirdBrowserWindow: GtkBuilder template widget with toolbar, tab bar, find bar, devtools banner, and hamburger menu - LadybirdLocationEntry: URL entry with autocomplete, domain highlighting, and security icon - Menu: GAction-based context menus and application menu with keyboard accelerators - Dialogs: JS alert/confirm/prompt (AdwAlertDialog), color picker, file picker, select dropdown, download save dialog, toast - GtkBuilder .ui resources for browser window, location entry completions, and list popovers Updates Application and main.cpp to create browser windows and handle D-Bus activation from remote instances.
This commit is contained in:
Notes:
github-actions[bot]
2026-04-17 15:19:11 +00:00
Author: https://github.com/jdahlin Commit: https://github.com/LadybirdBrowser/ladybird/commit/0a00a5d61a8 Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/8691 Reviewed-by: https://github.com/ADKaster Reviewed-by: https://github.com/christianfrey Reviewed-by: https://github.com/cqundefine Reviewed-by: https://github.com/trflynn89 ✅
@@ -4,14 +4,21 @@
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <LibURL/Parser.h>
|
||||
#include <LibWebView/URL.h>
|
||||
#include <UI/Gtk/Application.h>
|
||||
#include <UI/Gtk/BrowserWindow.h>
|
||||
#include <UI/Gtk/Dialogs.h>
|
||||
#include <UI/Gtk/EventLoopImplementationGtk.h>
|
||||
#include <UI/Gtk/Tab.h>
|
||||
#include <UI/Gtk/WebContentView.h>
|
||||
|
||||
namespace Ladybird {
|
||||
|
||||
Application::Application() = default;
|
||||
Application::~Application()
|
||||
{
|
||||
m_windows.clear();
|
||||
g_clear_object(&m_adw_application);
|
||||
}
|
||||
|
||||
@@ -28,6 +35,11 @@ NonnullOwnPtr<Core::EventLoop> Application::create_platform_event_loop()
|
||||
warnln("Failed to register GApplication: {}", error->message);
|
||||
g_error_free(error);
|
||||
}
|
||||
|
||||
if (g_application_get_is_remote(G_APPLICATION(m_adw_application)))
|
||||
forward_urls_to_remote_and_exit();
|
||||
|
||||
setup_dbus_handlers();
|
||||
}
|
||||
|
||||
auto event_loop = WebView::Application::create_platform_event_loop();
|
||||
@@ -38,17 +50,259 @@ NonnullOwnPtr<Core::EventLoop> Application::create_platform_event_loop()
|
||||
return event_loop;
|
||||
}
|
||||
|
||||
Optional<WebView::ViewImplementation&> Application::active_web_view() const { return {}; }
|
||||
Optional<WebView::ViewImplementation&> Application::open_blank_new_tab(Web::HTML::ActivateTab) const { return {}; }
|
||||
Optional<ByteString> Application::ask_user_for_download_path(StringView) const { return {}; }
|
||||
void Application::display_download_confirmation_dialog(StringView, LexicalPath const&) const { }
|
||||
void Application::display_error_dialog(StringView) const { }
|
||||
Utf16String Application::clipboard_text() const { return {}; }
|
||||
Vector<Web::Clipboard::SystemClipboardRepresentation> Application::clipboard_entries() const { return {}; }
|
||||
void Application::insert_clipboard_entry(Web::Clipboard::SystemClipboardRepresentation) { }
|
||||
void Application::rebuild_bookmarks_menu() const { }
|
||||
void Application::update_bookmarks_bar_display(bool) const { }
|
||||
void Application::on_devtools_enabled() const { }
|
||||
void Application::on_devtools_disabled() const { }
|
||||
void Application::forward_urls_to_remote_and_exit()
|
||||
{
|
||||
auto const& raw_urls = browser_options().raw_urls;
|
||||
if (!raw_urls.is_empty()) {
|
||||
Vector<GObjectPtr<GFile>> files;
|
||||
for (auto const& url : raw_urls)
|
||||
files.append(GObjectPtr<GFile> { g_file_new_for_commandline_arg(url.characters()) });
|
||||
Vector<GFile*> raw_files;
|
||||
for (auto& file : files)
|
||||
raw_files.append(file.ptr());
|
||||
g_application_open(G_APPLICATION(m_adw_application), raw_files.data(), static_cast<int>(raw_files.size()), "");
|
||||
} else {
|
||||
g_application_activate(G_APPLICATION(m_adw_application));
|
||||
}
|
||||
exit(0);
|
||||
}
|
||||
|
||||
void Application::setup_dbus_handlers()
|
||||
{
|
||||
g_signal_connect(m_adw_application, "open", G_CALLBACK(+[](GApplication*, GFile** files, int n_files, char const*, gpointer) {
|
||||
auto& app = Application::the();
|
||||
Vector<URL::URL> urls;
|
||||
for (int i = 0; i < n_files; i++) {
|
||||
g_autofree char* uri = g_file_get_uri(files[i]);
|
||||
if (uri) {
|
||||
if (auto url = URL::Parser::basic_parse(StringView { uri, strlen(uri) }); url.has_value())
|
||||
urls.append(url.release_value());
|
||||
}
|
||||
}
|
||||
app.on_open(move(urls));
|
||||
}),
|
||||
nullptr);
|
||||
|
||||
g_signal_connect(m_adw_application, "activate", G_CALLBACK(+[](GApplication*, gpointer) {
|
||||
Application::the().on_activate();
|
||||
}),
|
||||
nullptr);
|
||||
}
|
||||
|
||||
void Application::on_open(Vector<URL::URL> urls)
|
||||
{
|
||||
if (auto* window = active_window()) {
|
||||
for (size_t i = 0; i < urls.size(); i++)
|
||||
window->create_new_tab(urls[i], (i == 0) ? Web::HTML::ActivateTab::Yes : Web::HTML::ActivateTab::No);
|
||||
window->present();
|
||||
} else {
|
||||
new_window(urls);
|
||||
}
|
||||
}
|
||||
|
||||
void Application::on_activate()
|
||||
{
|
||||
if (auto* window = active_window())
|
||||
window->present();
|
||||
else
|
||||
new_window({});
|
||||
}
|
||||
|
||||
BrowserWindow& Application::new_window(Vector<URL::URL> const& initial_urls)
|
||||
{
|
||||
auto window = make<BrowserWindow>(m_adw_application, initial_urls);
|
||||
auto& window_ref = *window;
|
||||
m_active_window = &window_ref;
|
||||
|
||||
// Track active window via focus
|
||||
g_signal_connect(window_ref.gtk_window(), "notify::is-active", G_CALLBACK(+[](GObject* gtk_window, GParamSpec*, gpointer) {
|
||||
if (!gtk_window_is_active(GTK_WINDOW(gtk_window)))
|
||||
return;
|
||||
auto& app = Application::the();
|
||||
app.for_each_window([&](BrowserWindow& bw) {
|
||||
if (GTK_WINDOW(bw.gtk_window()) == GTK_WINDOW(gtk_window))
|
||||
app.set_active_window(&bw);
|
||||
});
|
||||
}),
|
||||
nullptr);
|
||||
|
||||
// Clean up when window is destroyed — defer removal to avoid mutating m_windows during iteration
|
||||
g_signal_connect(window_ref.gtk_window(), "destroy", G_CALLBACK(+[](GtkWidget* gtk_window, gpointer) {
|
||||
auto& app = Application::the();
|
||||
BrowserWindow* to_remove = nullptr;
|
||||
app.for_each_window([&](BrowserWindow& bw) {
|
||||
if (GTK_WIDGET(bw.gtk_window()) == gtk_window)
|
||||
to_remove = &bw;
|
||||
});
|
||||
if (to_remove) {
|
||||
if (app.active_window() == to_remove)
|
||||
app.set_active_window(nullptr);
|
||||
app.remove_window(*to_remove);
|
||||
bool has_windows = false;
|
||||
app.for_each_window([&](auto&) { has_windows = true; });
|
||||
if (!has_windows)
|
||||
Core::EventLoop::current().quit(0);
|
||||
}
|
||||
}),
|
||||
nullptr);
|
||||
|
||||
window_ref.present();
|
||||
m_windows.append(move(window));
|
||||
return window_ref;
|
||||
}
|
||||
|
||||
void Application::remove_window(BrowserWindow& window)
|
||||
{
|
||||
m_windows.remove_first_matching([&](auto& w) { return w.ptr() == &window; });
|
||||
if (m_active_window == &window)
|
||||
m_active_window = m_windows.is_empty() ? nullptr : m_windows.last().ptr();
|
||||
}
|
||||
|
||||
Tab* Application::active_tab() const
|
||||
{
|
||||
if (!m_active_window)
|
||||
return nullptr;
|
||||
return m_active_window->current_tab();
|
||||
}
|
||||
|
||||
Optional<WebView::ViewImplementation&> Application::active_web_view() const
|
||||
{
|
||||
if (auto* tab = active_tab())
|
||||
return static_cast<WebView::ViewImplementation&>(tab->view());
|
||||
return {};
|
||||
}
|
||||
|
||||
Optional<WebView::ViewImplementation&> Application::open_blank_new_tab(Web::HTML::ActivateTab activate_tab) const
|
||||
{
|
||||
if (!m_active_window)
|
||||
return {};
|
||||
auto& tab = m_active_window->create_new_tab(activate_tab);
|
||||
return static_cast<WebView::ViewImplementation&>(tab.view());
|
||||
}
|
||||
|
||||
Optional<ByteString> Application::ask_user_for_download_path(StringView file) const
|
||||
{
|
||||
if (!m_active_window)
|
||||
return {};
|
||||
|
||||
GObjectPtr dialog { gtk_file_dialog_new() };
|
||||
gtk_file_dialog_set_title(GTK_FILE_DIALOG(dialog.ptr()), "Save As");
|
||||
|
||||
auto const* downloads_dir = g_get_user_special_dir(G_USER_DIRECTORY_DOWNLOAD);
|
||||
if (downloads_dir) {
|
||||
GObjectPtr initial_folder { g_file_new_for_path(downloads_dir) };
|
||||
gtk_file_dialog_set_initial_folder(GTK_FILE_DIALOG(dialog.ptr()), G_FILE(initial_folder.ptr()));
|
||||
}
|
||||
gtk_file_dialog_set_initial_name(GTK_FILE_DIALOG(dialog.ptr()), ByteString(file).characters());
|
||||
|
||||
Optional<ByteString> result;
|
||||
Core::EventLoop nested_loop;
|
||||
|
||||
gtk_file_dialog_save(GTK_FILE_DIALOG(dialog.ptr()), m_active_window->gtk_window(), nullptr, +[](GObject* source, GAsyncResult* async_result, gpointer user_data) {
|
||||
auto* result_ptr = static_cast<Optional<ByteString>*>(user_data);
|
||||
GError* error = nullptr;
|
||||
GObjectPtr file { gtk_file_dialog_save_finish(GTK_FILE_DIALOG(source), async_result, &error) };
|
||||
if (file.ptr()) {
|
||||
g_autofree char* path = g_file_get_path(G_FILE(file.ptr()));
|
||||
if (path)
|
||||
*result_ptr = ByteString(path);
|
||||
}
|
||||
if (error)
|
||||
g_error_free(error);
|
||||
Core::EventLoop::current().quit(0); }, &result);
|
||||
|
||||
nested_loop.exec();
|
||||
return result;
|
||||
}
|
||||
|
||||
void Application::display_download_confirmation_dialog(StringView download_name, LexicalPath const& path) const
|
||||
{
|
||||
if (!m_active_window)
|
||||
return;
|
||||
auto message = ByteString::formatted("{} saved to {}", download_name, path.dirname());
|
||||
auto* toast = adw_toast_new(message.characters());
|
||||
adw_toast_set_timeout(toast, 5);
|
||||
m_active_window->show_toast(toast);
|
||||
}
|
||||
|
||||
void Application::display_error_dialog(StringView error_message) const
|
||||
{
|
||||
if (!m_active_window)
|
||||
return;
|
||||
Dialogs::show_error(m_active_window->gtk_window(), error_message);
|
||||
}
|
||||
|
||||
// GDK4 only provides an async clipboard API. Spin a nested event loop to read synchronously.
|
||||
static Optional<ByteString> read_clipboard_text_sync()
|
||||
{
|
||||
auto* clipboard = gdk_display_get_clipboard(gdk_display_get_default());
|
||||
|
||||
Optional<ByteString> result;
|
||||
Core::EventLoop nested_loop;
|
||||
|
||||
gdk_clipboard_read_text_async(clipboard, nullptr, [](GObject* source, GAsyncResult* async_result, gpointer user_data) {
|
||||
auto* result_ptr = static_cast<Optional<ByteString>*>(user_data);
|
||||
g_autofree char* text = gdk_clipboard_read_text_finish(GDK_CLIPBOARD(source), async_result, nullptr);
|
||||
if (text)
|
||||
*result_ptr = ByteString(text);
|
||||
Core::EventLoop::current().quit(0); }, &result);
|
||||
|
||||
nested_loop.exec();
|
||||
return result;
|
||||
}
|
||||
|
||||
Utf16String Application::clipboard_text() const
|
||||
{
|
||||
if (browser_options().headless_mode.has_value())
|
||||
return WebView::Application::clipboard_text();
|
||||
|
||||
if (auto text = read_clipboard_text_sync(); text.has_value())
|
||||
return Utf16String::from_utf8(text->view());
|
||||
return {};
|
||||
}
|
||||
|
||||
Vector<Web::Clipboard::SystemClipboardRepresentation> Application::clipboard_entries() const
|
||||
{
|
||||
if (browser_options().headless_mode.has_value())
|
||||
return WebView::Application::clipboard_entries();
|
||||
|
||||
Vector<Web::Clipboard::SystemClipboardRepresentation> representations;
|
||||
if (auto text = read_clipboard_text_sync(); text.has_value())
|
||||
representations.empend(text.release_value(), MUST(String::from_utf8("text/plain"sv)));
|
||||
return representations;
|
||||
}
|
||||
|
||||
void Application::insert_clipboard_entry(Web::Clipboard::SystemClipboardRepresentation entry)
|
||||
{
|
||||
if (browser_options().headless_mode.has_value()) {
|
||||
WebView::Application::insert_clipboard_entry(move(entry));
|
||||
return;
|
||||
}
|
||||
auto* clipboard = gdk_display_get_clipboard(gdk_display_get_default());
|
||||
if (entry.mime_type == "text/plain"sv)
|
||||
gdk_clipboard_set_text(clipboard, entry.data.characters());
|
||||
}
|
||||
|
||||
void Application::rebuild_bookmarks_menu() const
|
||||
{
|
||||
}
|
||||
|
||||
void Application::update_bookmarks_bar_display(bool) const
|
||||
{
|
||||
}
|
||||
|
||||
void Application::on_devtools_enabled() const
|
||||
{
|
||||
WebView::Application::on_devtools_enabled();
|
||||
for (auto& window : m_windows)
|
||||
window->on_devtools_enabled();
|
||||
}
|
||||
|
||||
void Application::on_devtools_disabled() const
|
||||
{
|
||||
WebView::Application::on_devtools_disabled();
|
||||
for (auto& window : m_windows)
|
||||
window->on_devtools_disabled();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -6,20 +6,41 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/Function.h>
|
||||
#include <AK/Vector.h>
|
||||
#include <LibURL/URL.h>
|
||||
#include <LibWebView/Application.h>
|
||||
|
||||
#include <adwaita.h>
|
||||
|
||||
namespace Ladybird {
|
||||
|
||||
class BrowserWindow;
|
||||
class Tab;
|
||||
|
||||
class Application : public WebView::Application {
|
||||
WEB_VIEW_APPLICATION(Application)
|
||||
|
||||
public:
|
||||
virtual ~Application() override;
|
||||
|
||||
BrowserWindow& new_window(Vector<URL::URL> const& initial_urls);
|
||||
void remove_window(BrowserWindow&);
|
||||
|
||||
BrowserWindow* active_window() const { return m_active_window; }
|
||||
void set_active_window(BrowserWindow* w) { m_active_window = w; }
|
||||
|
||||
Tab* active_tab() const;
|
||||
|
||||
AdwApplication* adw_application() const { return m_adw_application; }
|
||||
|
||||
template<typename Callback>
|
||||
void for_each_window(Callback callback)
|
||||
{
|
||||
for (auto& window : m_windows)
|
||||
callback(*window);
|
||||
}
|
||||
|
||||
private:
|
||||
explicit Application();
|
||||
|
||||
@@ -44,7 +65,14 @@ private:
|
||||
virtual void on_devtools_enabled() const override;
|
||||
virtual void on_devtools_disabled() const override;
|
||||
|
||||
void forward_urls_to_remote_and_exit();
|
||||
void setup_dbus_handlers();
|
||||
void on_open(Vector<URL::URL> urls);
|
||||
void on_activate();
|
||||
|
||||
AdwApplication* m_adw_application { nullptr };
|
||||
Vector<NonnullOwnPtr<BrowserWindow>> m_windows;
|
||||
BrowserWindow* m_active_window { nullptr };
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
501
UI/Gtk/BrowserWindow.cpp
Normal file
501
UI/Gtk/BrowserWindow.cpp
Normal file
@@ -0,0 +1,501 @@
|
||||
/*
|
||||
* Copyright (c) 2026, Johan Dahlin <jdahlin@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <AK/Format.h>
|
||||
#include <LibCore/EventLoop.h>
|
||||
#include <LibURL/Parser.h>
|
||||
#include <LibWebView/Menu.h>
|
||||
#include <LibWebView/URL.h>
|
||||
#include <UI/Gtk/Application.h>
|
||||
#include <UI/Gtk/BrowserWindow.h>
|
||||
#include <UI/Gtk/GLibPtr.h>
|
||||
#include <UI/Gtk/Menu.h>
|
||||
#include <UI/Gtk/Tab.h>
|
||||
#include <UI/Gtk/WebContentView.h>
|
||||
|
||||
namespace Ladybird {
|
||||
|
||||
class NavActionObserver final : public WebView::Action::Observer {
|
||||
public:
|
||||
NavActionObserver(GSimpleAction* gaction)
|
||||
: m_gaction(gaction)
|
||||
{
|
||||
}
|
||||
|
||||
void on_enabled_state_changed(WebView::Action& action) override
|
||||
{
|
||||
g_simple_action_set_enabled(m_gaction, action.enabled());
|
||||
}
|
||||
|
||||
private:
|
||||
GSimpleAction* m_gaction;
|
||||
};
|
||||
|
||||
void BrowserWindow::ActionBinding::detach()
|
||||
{
|
||||
if (action && observer)
|
||||
action->remove_observer(*observer);
|
||||
action = nullptr;
|
||||
observer = nullptr;
|
||||
}
|
||||
|
||||
BrowserWindow::BrowserWindow(AdwApplication* app, Vector<URL::URL> const& initial_urls)
|
||||
{
|
||||
setup_ui(app);
|
||||
setup_keyboard_shortcuts();
|
||||
|
||||
if (initial_urls.is_empty()) {
|
||||
create_new_tab(Web::HTML::ActivateTab::Yes);
|
||||
} else {
|
||||
for (size_t i = 0; i < initial_urls.size(); ++i) {
|
||||
create_new_tab(initial_urls[i], (i == 0) ? Web::HTML::ActivateTab::Yes : Web::HTML::ActivateTab::No);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BrowserWindow::~BrowserWindow()
|
||||
{
|
||||
m_back_binding.detach();
|
||||
m_forward_binding.detach();
|
||||
g_signal_handlers_disconnect_by_data(m_window, this);
|
||||
if (m_location_entry)
|
||||
g_signal_handlers_disconnect_by_data(m_location_entry, this);
|
||||
m_tabs.clear();
|
||||
}
|
||||
|
||||
void BrowserWindow::register_actions()
|
||||
{
|
||||
struct ActionData {
|
||||
BrowserWindow* window;
|
||||
void (*callback)(BrowserWindow&);
|
||||
};
|
||||
auto add_action = [&](char const* name, void (*callback)(BrowserWindow&), bool enabled = true) {
|
||||
GObjectPtr action { g_simple_action_new(name, nullptr) };
|
||||
g_simple_action_set_enabled(G_SIMPLE_ACTION(action.ptr()), enabled);
|
||||
g_signal_connect_data(action.ptr(), "activate", G_CALLBACK(+[](GSimpleAction*, GVariant*, gpointer user_data) {
|
||||
auto* data = static_cast<ActionData*>(user_data);
|
||||
data->callback(*data->window); }), new ActionData { this, callback }, +[](gpointer data, GClosure*) { delete static_cast<ActionData*>(data); }, static_cast<GConnectFlags>(0));
|
||||
g_action_map_add_action(G_ACTION_MAP(m_window), G_ACTION(action.ptr()));
|
||||
};
|
||||
|
||||
add_action("new-tab", [](BrowserWindow& self) { self.create_new_tab(Web::HTML::ActivateTab::Yes); });
|
||||
add_action("new-window", [](BrowserWindow&) { Application::the().new_window({}); });
|
||||
add_action("close-tab", [](BrowserWindow& self) { self.close_current_tab(); });
|
||||
add_action("focus-location", [](BrowserWindow& self) { ladybird_location_entry_focus_and_select_all(self.m_location_entry); });
|
||||
|
||||
add_action("go-back", [](BrowserWindow& self) {
|
||||
if (auto* tab = self.current_tab())
|
||||
tab->view().traverse_the_history_by_delta(-1); }, false);
|
||||
|
||||
add_action("go-forward", [](BrowserWindow& self) {
|
||||
if (auto* tab = self.current_tab())
|
||||
tab->view().traverse_the_history_by_delta(1); }, false);
|
||||
|
||||
add_action("zoom-in", [](BrowserWindow& self) {
|
||||
if (auto* tab = self.current_tab())
|
||||
tab->view().zoom_in(); });
|
||||
|
||||
add_action("zoom-out", [](BrowserWindow& self) {
|
||||
if (auto* tab = self.current_tab())
|
||||
tab->view().zoom_out(); });
|
||||
|
||||
add_action("zoom-reset", [](BrowserWindow& self) {
|
||||
if (auto* tab = self.current_tab())
|
||||
tab->view().reset_zoom(); }, false);
|
||||
|
||||
add_action("find", [](BrowserWindow& self) { self.show_find_bar(); });
|
||||
add_action("find-close", [](BrowserWindow& self) { self.hide_find_bar(); });
|
||||
|
||||
add_action("find-next", [](BrowserWindow& self) {
|
||||
if (auto* tab = self.current_tab())
|
||||
tab->view().find_in_page_next_match(); });
|
||||
|
||||
add_action("find-previous", [](BrowserWindow& self) {
|
||||
if (auto* tab = self.current_tab())
|
||||
tab->view().find_in_page_previous_match(); });
|
||||
|
||||
add_action("quit", [](BrowserWindow&) { Core::EventLoop::current().quit(0); });
|
||||
|
||||
add_action("fullscreen", [](BrowserWindow& self) {
|
||||
if (gtk_window_is_fullscreen(GTK_WINDOW(self.m_window)))
|
||||
gtk_window_unfullscreen(GTK_WINDOW(self.m_window));
|
||||
else
|
||||
gtk_window_fullscreen(GTK_WINDOW(self.m_window)); });
|
||||
|
||||
auto& app = WebView::Application::the();
|
||||
add_action_to_map(G_ACTION_MAP(m_window), "reload", app.reload_action());
|
||||
add_action_to_map(G_ACTION_MAP(m_window), "preferences", app.open_settings_page_action());
|
||||
add_action_to_map(G_ACTION_MAP(m_window), "about", app.open_about_page_action());
|
||||
}
|
||||
|
||||
void BrowserWindow::setup_ui(AdwApplication* app)
|
||||
{
|
||||
auto* browser_window_widget = LadybirdWidgets::create_browser_window_widget(app);
|
||||
m_window = ADW_APPLICATION_WINDOW(browser_window_widget);
|
||||
|
||||
m_tab_view = LadybirdWidgets::browser_window_tab_view(browser_window_widget);
|
||||
m_header_bar = LadybirdWidgets::browser_window_header_bar(browser_window_widget);
|
||||
m_restore_button = LadybirdWidgets::browser_window_restore_button(browser_window_widget);
|
||||
m_zoom_label = LadybirdWidgets::browser_window_zoom_label(browser_window_widget);
|
||||
m_devtools_banner = LadybirdWidgets::browser_window_devtools_banner(browser_window_widget);
|
||||
m_find_bar_revealer = LadybirdWidgets::browser_window_find_bar_revealer(browser_window_widget);
|
||||
m_find_entry = LadybirdWidgets::browser_window_find_entry(browser_window_widget);
|
||||
m_find_result_label = LadybirdWidgets::browser_window_find_result_label(browser_window_widget);
|
||||
m_toast_overlay = LadybirdWidgets::browser_window_toast_overlay(browser_window_widget);
|
||||
|
||||
// Connect find entry signals
|
||||
g_signal_connect_swapped(m_find_entry, "search-changed", G_CALLBACK(+[](BrowserWindow* self, GtkSearchEntry* entry) {
|
||||
auto* text = gtk_editable_get_text(GTK_EDITABLE(entry));
|
||||
if (auto* tab = self->current_tab()) {
|
||||
if (text && text[0] != '\0')
|
||||
tab->view().find_in_page(MUST(String::from_utf8(StringView(text, strlen(text)))));
|
||||
}
|
||||
}),
|
||||
this);
|
||||
g_signal_connect_swapped(m_find_entry, "activate", G_CALLBACK(+[](BrowserWindow* self, GtkSearchEntry*) {
|
||||
if (auto* tab = self->current_tab())
|
||||
tab->view().find_in_page_next_match();
|
||||
}),
|
||||
this);
|
||||
g_signal_connect_swapped(m_find_entry, "next-match", G_CALLBACK(+[](BrowserWindow* self, GtkSearchEntry*) {
|
||||
if (auto* tab = self->current_tab())
|
||||
tab->view().find_in_page_next_match();
|
||||
}),
|
||||
this);
|
||||
g_signal_connect_swapped(m_find_entry, "previous-match", G_CALLBACK(+[](BrowserWindow* self, GtkSearchEntry*) {
|
||||
if (auto* tab = self->current_tab())
|
||||
tab->view().find_in_page_previous_match();
|
||||
}),
|
||||
this);
|
||||
g_signal_connect_swapped(m_find_entry, "stop-search", G_CALLBACK(+[](BrowserWindow* self, GtkSearchEntry*) {
|
||||
self->hide_find_bar();
|
||||
}),
|
||||
this);
|
||||
|
||||
register_actions();
|
||||
|
||||
g_signal_connect_swapped(m_tab_view, "close-page", G_CALLBACK(+[](BrowserWindow* self, AdwTabPage* page) -> gboolean {
|
||||
self->on_tab_close_request(page);
|
||||
return GDK_EVENT_STOP;
|
||||
}),
|
||||
this);
|
||||
|
||||
g_signal_connect_swapped(m_tab_view, "notify::selected-page", G_CALLBACK(+[](BrowserWindow* self, GParamSpec*) {
|
||||
self->on_tab_switched();
|
||||
}),
|
||||
this);
|
||||
|
||||
// URL entry (centered title widget)
|
||||
m_location_entry = ladybird_location_entry_new();
|
||||
ladybird_location_entry_set_on_navigate(m_location_entry, [this](String url_string) {
|
||||
if (auto url = URL::Parser::basic_parse(url_string); url.has_value()) {
|
||||
if (auto* tab = current_tab())
|
||||
tab->navigate(url.release_value());
|
||||
if (auto* v = view())
|
||||
gtk_widget_grab_focus(GTK_WIDGET(v->gtk_widget()));
|
||||
}
|
||||
});
|
||||
|
||||
adw_header_bar_set_title_widget(m_header_bar, GTK_WIDGET(m_location_entry));
|
||||
|
||||
GObjectPtr developer_tools_submenu { g_menu_new() };
|
||||
GObjectPtr inspect_gmenu { create_application_menu(WebView::Application::the().inspect_menu(), [](WebView::Action& action) {
|
||||
return ByteString::formatted("win.inspect-{}", static_cast<int>(action.id()));
|
||||
}) };
|
||||
GObjectPtr debug_gmenu { create_application_menu(WebView::Application::the().debug_menu(), [](WebView::Action& action) {
|
||||
return ByteString::formatted("win.debug-{}", static_cast<int>(action.id()));
|
||||
}) };
|
||||
g_menu_append_section(G_MENU(developer_tools_submenu.ptr()), nullptr, G_MENU_MODEL(inspect_gmenu.ptr()));
|
||||
g_menu_append_section(G_MENU(developer_tools_submenu.ptr()), "Debug", G_MENU_MODEL(debug_gmenu.ptr()));
|
||||
append_submenu_to_section_containing_action(LadybirdWidgets::browser_window_hamburger_menu(browser_window_widget), "win.new-window", "Developer Tools", G_MENU_MODEL(developer_tools_submenu.ptr()));
|
||||
|
||||
// Listen for fullscreen state changes
|
||||
g_signal_connect_swapped(m_window, "notify::fullscreened", G_CALLBACK(+[](BrowserWindow* self, GParamSpec*) {
|
||||
gboolean fullscreen = gtk_window_is_fullscreen(GTK_WINDOW(self->m_window));
|
||||
adw_header_bar_set_show_start_title_buttons(self->m_header_bar, !fullscreen);
|
||||
adw_header_bar_set_show_end_title_buttons(self->m_header_bar, !fullscreen);
|
||||
gtk_widget_set_visible(GTK_WIDGET(self->m_restore_button), fullscreen);
|
||||
}),
|
||||
this);
|
||||
|
||||
add_menu_actions_to_map(G_ACTION_MAP(m_window), WebView::Application::the().inspect_menu(), [](WebView::Action& action) {
|
||||
return ByteString::formatted("inspect-{}", static_cast<int>(action.id()));
|
||||
});
|
||||
add_menu_actions_to_map(G_ACTION_MAP(m_window), WebView::Application::the().debug_menu(), [](WebView::Action& action) {
|
||||
return ByteString::formatted("debug-{}", static_cast<int>(action.id()));
|
||||
});
|
||||
|
||||
auto* application = gtk_window_get_application(GTK_WINDOW(m_window));
|
||||
install_action_accelerators(application, "win.reload", WebView::Application::the().reload_action());
|
||||
install_action_accelerators(application, "win.preferences", WebView::Application::the().open_settings_page_action());
|
||||
install_action_accelerators(application, "win.about", WebView::Application::the().open_about_page_action());
|
||||
install_menu_action_accelerators(application, "win.inspect", WebView::Application::the().inspect_menu());
|
||||
install_menu_action_accelerators(application, "win.debug", WebView::Application::the().debug_menu());
|
||||
|
||||
if (WebView::Application::browser_options().devtools_port.has_value())
|
||||
on_devtools_enabled();
|
||||
}
|
||||
|
||||
void BrowserWindow::setup_keyboard_shortcuts()
|
||||
{
|
||||
auto* app = gtk_window_get_application(GTK_WINDOW(m_window));
|
||||
|
||||
auto set_accels = [&](char const* action, std::initializer_list<char const*> accels) {
|
||||
Vector<char const*> list;
|
||||
list.ensure_capacity(accels.size() + 1);
|
||||
for (auto* a : accels)
|
||||
list.append(a);
|
||||
list.append(nullptr);
|
||||
gtk_application_set_accels_for_action(app, action, list.data());
|
||||
};
|
||||
|
||||
set_accels("win.new-tab", { "<Ctrl>t" });
|
||||
set_accels("win.close-tab", { "<Ctrl>w" });
|
||||
set_accels("win.focus-location", { "<Ctrl>l" });
|
||||
set_accels("win.find", { "<Ctrl>f" });
|
||||
set_accels("win.find-close", { "Escape" });
|
||||
set_accels("win.go-back", { "<Alt>Left" });
|
||||
set_accels("win.go-forward", { "<Alt>Right" });
|
||||
set_accels("win.zoom-in", { "<Ctrl>equal", "<Ctrl>plus" });
|
||||
set_accels("win.zoom-out", { "<Ctrl>minus" });
|
||||
set_accels("win.zoom-reset", { "<Ctrl>0" });
|
||||
set_accels("win.fullscreen", { "F11" });
|
||||
set_accels("win.quit", { "<Ctrl>q" });
|
||||
set_accels("win.new-window", { "<Ctrl>n" });
|
||||
}
|
||||
|
||||
void BrowserWindow::on_tab_switched()
|
||||
{
|
||||
auto* tab = current_tab();
|
||||
if (!tab)
|
||||
return;
|
||||
|
||||
auto const& url = tab->view().url();
|
||||
if (is_internal_url(url)) {
|
||||
ladybird_location_entry_set_text(m_location_entry, "");
|
||||
} else {
|
||||
auto url_str = url.serialize().to_byte_string();
|
||||
ladybird_location_entry_set_url(m_location_entry, url_str.characters());
|
||||
}
|
||||
|
||||
bind_navigation_actions(tab->view());
|
||||
update_zoom_label();
|
||||
}
|
||||
|
||||
Tab& BrowserWindow::create_new_tab(Web::HTML::ActivateTab activate_tab)
|
||||
{
|
||||
auto& new_tab_url = WebView::Application::settings().new_tab_page_url();
|
||||
auto& tab = create_new_tab(new_tab_url, activate_tab);
|
||||
return tab;
|
||||
}
|
||||
|
||||
Tab& BrowserWindow::create_new_tab(URL::URL const& url, Web::HTML::ActivateTab activate_tab)
|
||||
{
|
||||
auto tab = make<Tab>(*this, url);
|
||||
auto& tab_ref = *tab;
|
||||
|
||||
auto* page = adw_tab_view_append(m_tab_view, tab_ref.widget());
|
||||
adw_tab_page_set_title(page, "New Tab");
|
||||
tab_ref.set_tab_page(page);
|
||||
|
||||
if (activate_tab == Web::HTML::ActivateTab::Yes) {
|
||||
adw_tab_view_set_selected_page(m_tab_view, page);
|
||||
bind_navigation_actions(tab_ref.view());
|
||||
|
||||
if (is_internal_url(url)) {
|
||||
ladybird_location_entry_set_text(m_location_entry, "");
|
||||
ladybird_location_entry_focus_and_select_all(m_location_entry);
|
||||
}
|
||||
}
|
||||
|
||||
m_tabs.append(move(tab));
|
||||
return tab_ref;
|
||||
}
|
||||
|
||||
Tab& BrowserWindow::create_child_tab(Web::HTML::ActivateTab activate_tab, Tab& parent, u64 page_index)
|
||||
{
|
||||
auto tab = make<Tab>(*this, parent.view().client(), page_index);
|
||||
auto& tab_ref = *tab;
|
||||
|
||||
auto* page = adw_tab_view_append(m_tab_view, tab_ref.widget());
|
||||
adw_tab_page_set_title(page, "New Tab");
|
||||
tab_ref.set_tab_page(page);
|
||||
|
||||
if (activate_tab == Web::HTML::ActivateTab::Yes)
|
||||
adw_tab_view_set_selected_page(m_tab_view, page);
|
||||
|
||||
m_tabs.append(move(tab));
|
||||
return tab_ref;
|
||||
}
|
||||
|
||||
void BrowserWindow::close_tab(Tab& tab)
|
||||
{
|
||||
auto* page = tab.tab_page();
|
||||
if (page)
|
||||
adw_tab_view_close_page(m_tab_view, page);
|
||||
}
|
||||
|
||||
void BrowserWindow::close_current_tab()
|
||||
{
|
||||
if (auto* tab = current_tab())
|
||||
close_tab(*tab);
|
||||
}
|
||||
|
||||
void BrowserWindow::on_tab_close_request(AdwTabPage* page)
|
||||
{
|
||||
auto* child = adw_tab_page_get_child(page);
|
||||
for (auto& tab : m_tabs) {
|
||||
if (tab->widget() == child) {
|
||||
adw_tab_view_close_page_finish(m_tab_view, page, TRUE);
|
||||
m_tabs.remove_first_matching([&](auto& t) { return t.ptr() == tab.ptr(); });
|
||||
if (m_tabs.is_empty())
|
||||
gtk_window_close(GTK_WINDOW(m_window));
|
||||
return;
|
||||
}
|
||||
}
|
||||
adw_tab_view_close_page_finish(m_tab_view, page, TRUE);
|
||||
}
|
||||
|
||||
Tab* BrowserWindow::current_tab() const
|
||||
{
|
||||
auto* page = adw_tab_view_get_selected_page(m_tab_view);
|
||||
if (!page)
|
||||
return nullptr;
|
||||
|
||||
auto* child = adw_tab_page_get_child(page);
|
||||
for (auto& tab : m_tabs) {
|
||||
if (tab->widget() == child)
|
||||
return tab.ptr();
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
WebContentView* BrowserWindow::view() const
|
||||
{
|
||||
auto* tab = current_tab();
|
||||
if (!tab)
|
||||
return nullptr;
|
||||
return &tab->view();
|
||||
}
|
||||
|
||||
void BrowserWindow::present()
|
||||
{
|
||||
gtk_window_present(GTK_WINDOW(m_window));
|
||||
}
|
||||
|
||||
int BrowserWindow::tab_count() const
|
||||
{
|
||||
return adw_tab_view_get_n_pages(m_tab_view);
|
||||
}
|
||||
|
||||
void BrowserWindow::update_navigation_buttons(bool back_enabled, bool forward_enabled)
|
||||
{
|
||||
auto* back_action = G_SIMPLE_ACTION(g_action_map_lookup_action(G_ACTION_MAP(m_window), "go-back"));
|
||||
if (back_action)
|
||||
g_simple_action_set_enabled(back_action, back_enabled);
|
||||
|
||||
auto* forward_action = G_SIMPLE_ACTION(g_action_map_lookup_action(G_ACTION_MAP(m_window), "go-forward"));
|
||||
if (forward_action)
|
||||
g_simple_action_set_enabled(forward_action, forward_enabled);
|
||||
}
|
||||
|
||||
void BrowserWindow::bind_navigation_actions(WebContentView& view)
|
||||
{
|
||||
m_back_binding.detach();
|
||||
m_forward_binding.detach();
|
||||
|
||||
auto bind = [&](ActionBinding& binding, WebView::Action& action, char const* name) {
|
||||
auto* gaction = G_SIMPLE_ACTION(g_action_map_lookup_action(G_ACTION_MAP(m_window), name));
|
||||
g_simple_action_set_enabled(gaction, action.enabled());
|
||||
auto observer = make<NavActionObserver>(gaction);
|
||||
binding = { &action, observer.ptr() };
|
||||
action.add_observer(move(observer));
|
||||
};
|
||||
|
||||
bind(m_back_binding, view.navigate_back_action(), "go-back");
|
||||
bind(m_forward_binding, view.navigate_forward_action(), "go-forward");
|
||||
}
|
||||
|
||||
void BrowserWindow::update_location_entry(StringView url)
|
||||
{
|
||||
if (url.is_empty()) {
|
||||
ladybird_location_entry_set_text(m_location_entry, "");
|
||||
return;
|
||||
}
|
||||
auto byte_url = ByteString(url);
|
||||
ladybird_location_entry_set_url(m_location_entry, byte_url.characters());
|
||||
}
|
||||
|
||||
void BrowserWindow::show_find_bar()
|
||||
{
|
||||
gtk_revealer_set_reveal_child(m_find_bar_revealer, TRUE);
|
||||
gtk_widget_grab_focus(GTK_WIDGET(m_find_entry));
|
||||
}
|
||||
|
||||
void BrowserWindow::hide_find_bar()
|
||||
{
|
||||
gtk_revealer_set_reveal_child(m_find_bar_revealer, FALSE);
|
||||
if (auto* v = view())
|
||||
gtk_widget_grab_focus(GTK_WIDGET(v->gtk_widget()));
|
||||
}
|
||||
|
||||
void BrowserWindow::update_find_in_page_result(size_t current_match_index, Optional<size_t> const& total_match_count)
|
||||
{
|
||||
if (total_match_count.has_value()) {
|
||||
auto text = ByteString::formatted("{} of {} matches", current_match_index + 1, total_match_count.value());
|
||||
gtk_label_set_text(m_find_result_label, text.characters());
|
||||
} else {
|
||||
gtk_label_set_text(m_find_result_label, "No matches");
|
||||
}
|
||||
}
|
||||
|
||||
void BrowserWindow::on_devtools_enabled()
|
||||
{
|
||||
auto port = WebView::Application::browser_options().devtools_port;
|
||||
auto message = ByteString::formatted("DevTools is enabled on port {}", port.value_or(0));
|
||||
adw_banner_set_title(m_devtools_banner, message.characters());
|
||||
adw_banner_set_revealed(m_devtools_banner, TRUE);
|
||||
|
||||
g_signal_connect_swapped(m_devtools_banner, "button-clicked", G_CALLBACK(+[](BrowserWindow* self, AdwBanner*) {
|
||||
(void)WebView::Application::the().toggle_devtools_enabled();
|
||||
self->on_devtools_disabled();
|
||||
}),
|
||||
this);
|
||||
}
|
||||
|
||||
void BrowserWindow::on_devtools_disabled()
|
||||
{
|
||||
adw_banner_set_revealed(m_devtools_banner, FALSE);
|
||||
}
|
||||
|
||||
bool BrowserWindow::is_internal_url(URL::URL const& url)
|
||||
{
|
||||
return url.scheme().is_empty() || url == URL::about_blank() || url == URL::about_newtab();
|
||||
}
|
||||
|
||||
void BrowserWindow::update_zoom_label()
|
||||
{
|
||||
if (!m_zoom_label)
|
||||
return;
|
||||
auto* tab = current_tab();
|
||||
if (!tab)
|
||||
return;
|
||||
auto zoom = tab->view().zoom_level();
|
||||
auto text = ByteString::formatted("{}%", static_cast<int>(zoom * 100));
|
||||
gtk_label_set_text(m_zoom_label, text.characters());
|
||||
|
||||
auto* action = g_action_map_lookup_action(G_ACTION_MAP(m_window), "zoom-reset");
|
||||
if (action)
|
||||
g_simple_action_set_enabled(G_SIMPLE_ACTION(action), zoom != 1.0);
|
||||
}
|
||||
|
||||
void BrowserWindow::show_toast(AdwToast* toast)
|
||||
{
|
||||
if (m_toast_overlay)
|
||||
adw_toast_overlay_add_toast(m_toast_overlay, toast);
|
||||
}
|
||||
|
||||
}
|
||||
86
UI/Gtk/BrowserWindow.h
Normal file
86
UI/Gtk/BrowserWindow.h
Normal file
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright (c) 2026, Johan Dahlin <jdahlin@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/Vector.h>
|
||||
#include <LibURL/URL.h>
|
||||
#include <LibWeb/HTML/ActivateTab.h>
|
||||
#include <LibWebView/Menu.h>
|
||||
#include <UI/Gtk/Widgets/LadybirdBrowserWindow.h>
|
||||
#include <UI/Gtk/Widgets/LadybirdLocationEntry.h>
|
||||
|
||||
#include <adwaita.h>
|
||||
|
||||
namespace Ladybird {
|
||||
|
||||
class Tab;
|
||||
class WebContentView;
|
||||
|
||||
class BrowserWindow {
|
||||
public:
|
||||
BrowserWindow(AdwApplication* app, Vector<URL::URL> const& initial_urls);
|
||||
~BrowserWindow();
|
||||
|
||||
GtkWindow* gtk_window() const { return GTK_WINDOW(m_window); }
|
||||
|
||||
Tab& create_new_tab(Web::HTML::ActivateTab activate_tab);
|
||||
Tab& create_new_tab(URL::URL const& url, Web::HTML::ActivateTab activate_tab);
|
||||
Tab& create_child_tab(Web::HTML::ActivateTab activate_tab, Tab& parent, u64 page_index);
|
||||
void close_tab(Tab& tab);
|
||||
void close_current_tab();
|
||||
Tab* current_tab() const;
|
||||
WebContentView* view() const;
|
||||
void present();
|
||||
int tab_count() const;
|
||||
|
||||
void update_navigation_buttons(bool back_enabled, bool forward_enabled);
|
||||
void update_location_entry(StringView url);
|
||||
void update_zoom_label();
|
||||
void update_find_in_page_result(size_t current_match_index, Optional<size_t> const& total_match_count);
|
||||
|
||||
void show_find_bar();
|
||||
void hide_find_bar();
|
||||
|
||||
void on_devtools_enabled();
|
||||
void on_devtools_disabled();
|
||||
|
||||
void show_toast(AdwToast* toast);
|
||||
|
||||
static bool is_internal_url(URL::URL const& url);
|
||||
|
||||
private:
|
||||
void setup_ui(AdwApplication* app);
|
||||
void register_actions();
|
||||
void setup_keyboard_shortcuts();
|
||||
void on_tab_close_request(AdwTabPage* page);
|
||||
void on_tab_switched();
|
||||
void bind_navigation_actions(WebContentView& view);
|
||||
|
||||
AdwApplicationWindow* m_window { nullptr };
|
||||
LadybirdLocationEntry* m_location_entry { nullptr };
|
||||
Vector<NonnullOwnPtr<Tab>> m_tabs;
|
||||
|
||||
AdwTabView* m_tab_view { nullptr };
|
||||
AdwHeaderBar* m_header_bar { nullptr };
|
||||
GtkButton* m_restore_button { nullptr };
|
||||
GtkLabel* m_zoom_label { nullptr };
|
||||
AdwBanner* m_devtools_banner { nullptr };
|
||||
GtkRevealer* m_find_bar_revealer { nullptr };
|
||||
GtkSearchEntry* m_find_entry { nullptr };
|
||||
GtkLabel* m_find_result_label { nullptr };
|
||||
AdwToastOverlay* m_toast_overlay { nullptr };
|
||||
|
||||
struct ActionBinding {
|
||||
WebView::Action* action { nullptr };
|
||||
WebView::Action::Observer* observer { nullptr };
|
||||
void detach();
|
||||
};
|
||||
ActionBinding m_back_binding;
|
||||
ActionBinding m_forward_binding;
|
||||
};
|
||||
|
||||
}
|
||||
@@ -4,12 +4,43 @@ pkg_check_modules(LIBADWAITA REQUIRED IMPORTED_TARGET libadwaita-1>=1.4)
|
||||
|
||||
add_executable(ladybird main.cpp)
|
||||
|
||||
find_program(GLIB_COMPILE_RESOURCES NAMES glib-compile-resources REQUIRED)
|
||||
|
||||
set(GTK_UI_RESOURCES
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/Resources/browser-window.ui"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/Resources/list-popover.ui"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/Resources/location-entry.ui"
|
||||
)
|
||||
|
||||
set(GTK_GRESOURCE_XML "${CMAKE_CURRENT_SOURCE_DIR}/Resources/resources.gresource.xml")
|
||||
set(GTK_GRESOURCE_SOURCE "${CMAKE_CURRENT_BINARY_DIR}/gtk-ui-resources.cpp")
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT "${GTK_GRESOURCE_SOURCE}"
|
||||
DEPENDS "${GTK_GRESOURCE_XML}" ${GTK_UI_RESOURCES}
|
||||
COMMAND "${GLIB_COMPILE_RESOURCES}"
|
||||
"${GTK_GRESOURCE_XML}"
|
||||
--sourcedir "${CMAKE_CURRENT_SOURCE_DIR}/Resources"
|
||||
--target "${GTK_GRESOURCE_SOURCE}"
|
||||
--generate-source
|
||||
VERBATIM
|
||||
)
|
||||
add_custom_target(gtk_ui_resources DEPENDS "${GTK_GRESOURCE_SOURCE}")
|
||||
|
||||
target_sources(ladybird PRIVATE
|
||||
Application.cpp
|
||||
BrowserWindow.cpp
|
||||
Dialogs.cpp
|
||||
Events.cpp
|
||||
EventLoopImplementationGtk.cpp
|
||||
Menu.cpp
|
||||
Tab.cpp
|
||||
WebContentView.cpp
|
||||
Widgets/LadybirdBrowserWindow.cpp
|
||||
Widgets/LadybirdLocationEntry.cpp
|
||||
Widgets/LadybirdWebView.cpp
|
||||
"${GTK_GRESOURCE_SOURCE}"
|
||||
)
|
||||
target_link_libraries(ladybird PRIVATE PkgConfig::GTK4 PkgConfig::LIBADWAITA)
|
||||
add_dependencies(ladybird gtk_ui_resources)
|
||||
create_ladybird_bundle(ladybird)
|
||||
|
||||
206
UI/Gtk/Dialogs.cpp
Normal file
206
UI/Gtk/Dialogs.cpp
Normal file
@@ -0,0 +1,206 @@
|
||||
/*
|
||||
* Copyright (c) 2026, Johan Dahlin <jdahlin@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <UI/Gtk/Dialogs.h>
|
||||
#include <UI/Gtk/GLibPtr.h>
|
||||
#include <UI/Gtk/WebContentView.h>
|
||||
|
||||
#include <adwaita.h>
|
||||
|
||||
namespace Ladybird::Dialogs {
|
||||
|
||||
void show_error(GtkWindow* parent, StringView message)
|
||||
{
|
||||
auto* dialog = adw_alert_dialog_new("Error", nullptr);
|
||||
adw_alert_dialog_format_body(ADW_ALERT_DIALOG(dialog), "%.*s",
|
||||
static_cast<int>(message.length()), message.characters_without_null_termination());
|
||||
adw_alert_dialog_add_response(ADW_ALERT_DIALOG(dialog), "ok", "OK");
|
||||
adw_dialog_present(ADW_DIALOG(dialog), GTK_WIDGET(parent));
|
||||
}
|
||||
|
||||
void show_alert(GtkWindow* parent, WebContentView* view, String const& message)
|
||||
{
|
||||
auto* dialog = adw_alert_dialog_new("Alert", nullptr);
|
||||
auto msg = message.to_byte_string();
|
||||
adw_alert_dialog_set_body(ADW_ALERT_DIALOG(dialog), msg.characters());
|
||||
adw_alert_dialog_add_response(ADW_ALERT_DIALOG(dialog), "ok", "OK");
|
||||
g_signal_connect_swapped(dialog, "response", G_CALLBACK(+[](WebContentView* view, char const*) {
|
||||
view->alert_closed();
|
||||
}),
|
||||
view);
|
||||
adw_dialog_present(ADW_DIALOG(dialog), GTK_WIDGET(parent));
|
||||
}
|
||||
|
||||
void show_confirm(GtkWindow* parent, WebContentView* view, String const& message)
|
||||
{
|
||||
auto* dialog = adw_alert_dialog_new("Confirm", nullptr);
|
||||
auto msg = message.to_byte_string();
|
||||
adw_alert_dialog_set_body(ADW_ALERT_DIALOG(dialog), msg.characters());
|
||||
adw_alert_dialog_add_response(ADW_ALERT_DIALOG(dialog), "cancel", "Cancel");
|
||||
adw_alert_dialog_add_response(ADW_ALERT_DIALOG(dialog), "ok", "OK");
|
||||
adw_alert_dialog_set_response_appearance(ADW_ALERT_DIALOG(dialog), "ok", ADW_RESPONSE_SUGGESTED);
|
||||
adw_alert_dialog_set_default_response(ADW_ALERT_DIALOG(dialog), "ok");
|
||||
g_signal_connect_swapped(dialog, "response", G_CALLBACK(+[](WebContentView* view, char const* response) {
|
||||
view->confirm_closed(StringView(response, strlen(response)) == "ok"sv);
|
||||
}),
|
||||
view);
|
||||
adw_dialog_present(ADW_DIALOG(dialog), GTK_WIDGET(parent));
|
||||
}
|
||||
|
||||
void show_prompt(GtkWindow* parent, WebContentView* view, String const& message, String const& default_value)
|
||||
{
|
||||
auto* dialog = adw_alert_dialog_new("Prompt", nullptr);
|
||||
auto msg = message.to_byte_string();
|
||||
adw_alert_dialog_set_body(ADW_ALERT_DIALOG(dialog), msg.characters());
|
||||
adw_alert_dialog_add_response(ADW_ALERT_DIALOG(dialog), "cancel", "Cancel");
|
||||
adw_alert_dialog_add_response(ADW_ALERT_DIALOG(dialog), "ok", "OK");
|
||||
adw_alert_dialog_set_response_appearance(ADW_ALERT_DIALOG(dialog), "ok", ADW_RESPONSE_SUGGESTED);
|
||||
adw_alert_dialog_set_default_response(ADW_ALERT_DIALOG(dialog), "ok");
|
||||
|
||||
auto* entry = gtk_entry_new();
|
||||
auto def = default_value.to_byte_string();
|
||||
gtk_editable_set_text(GTK_EDITABLE(entry), def.characters());
|
||||
adw_alert_dialog_set_extra_child(ADW_ALERT_DIALOG(dialog), entry);
|
||||
|
||||
struct PromptData {
|
||||
WebContentView* view;
|
||||
GtkEntry* entry;
|
||||
};
|
||||
auto* data = new PromptData { view, GTK_ENTRY(entry) };
|
||||
|
||||
g_signal_connect(dialog, "response", G_CALLBACK(+[](AdwAlertDialog*, char const* response, gpointer user_data) {
|
||||
auto* data = static_cast<PromptData*>(user_data);
|
||||
if (StringView(response, strlen(response)) == "ok"sv) {
|
||||
auto* text = gtk_editable_get_text(GTK_EDITABLE(data->entry));
|
||||
data->view->prompt_closed(MUST(String::from_utf8(StringView(text, strlen(text)))));
|
||||
} else {
|
||||
data->view->prompt_closed({});
|
||||
}
|
||||
delete data;
|
||||
}),
|
||||
data);
|
||||
adw_dialog_present(ADW_DIALOG(dialog), GTK_WIDGET(parent));
|
||||
}
|
||||
|
||||
void show_color_picker(GtkWindow* parent, WebContentView* view, Color current_color)
|
||||
{
|
||||
GObjectPtr dialog { gtk_color_dialog_new() };
|
||||
auto rgba = GdkRGBA {
|
||||
static_cast<float>(current_color.red()) / 255.0f,
|
||||
static_cast<float>(current_color.green()) / 255.0f,
|
||||
static_cast<float>(current_color.blue()) / 255.0f,
|
||||
static_cast<float>(current_color.alpha()) / 255.0f
|
||||
};
|
||||
|
||||
gtk_color_dialog_choose_rgba(GTK_COLOR_DIALOG(dialog.ptr()), parent, &rgba, nullptr, +[](GObject* source, GAsyncResult* result, gpointer user_data) {
|
||||
auto* view = static_cast<WebContentView*>(user_data);
|
||||
GError* error = nullptr;
|
||||
auto* color = gtk_color_dialog_choose_rgba_finish(GTK_COLOR_DIALOG(source), result, &error);
|
||||
if (error) {
|
||||
view->color_picker_update({}, Web::HTML::ColorPickerUpdateState::Closed);
|
||||
g_error_free(error);
|
||||
return;
|
||||
}
|
||||
auto picked = Color(
|
||||
static_cast<u8>(color->red * 255),
|
||||
static_cast<u8>(color->green * 255),
|
||||
static_cast<u8>(color->blue * 255),
|
||||
static_cast<u8>(color->alpha * 255));
|
||||
view->color_picker_update(picked, Web::HTML::ColorPickerUpdateState::Closed);
|
||||
gdk_rgba_free(color); }, view);
|
||||
}
|
||||
|
||||
void show_file_picker(GtkWindow* parent, WebContentView* view, Web::HTML::FileFilter const& accepted_file_types, Web::HTML::AllowMultipleFiles allow_multiple)
|
||||
{
|
||||
GObjectPtr dialog { gtk_file_dialog_new() };
|
||||
if (allow_multiple == Web::HTML::AllowMultipleFiles::Yes)
|
||||
gtk_file_dialog_set_title(GTK_FILE_DIALOG(dialog.ptr()), "Select Files");
|
||||
else
|
||||
gtk_file_dialog_set_title(GTK_FILE_DIALOG(dialog.ptr()), "Select File");
|
||||
|
||||
// Build file filters from accepted types
|
||||
GObjectPtr filters { g_list_store_new(GTK_TYPE_FILE_FILTER) };
|
||||
if (!accepted_file_types.filters.is_empty()) {
|
||||
GObjectPtr filter { gtk_file_filter_new() };
|
||||
gtk_file_filter_set_name(GTK_FILE_FILTER(filter.ptr()), "Accepted files");
|
||||
for (auto const& filter_type : accepted_file_types.filters) {
|
||||
filter_type.visit(
|
||||
[&](Web::HTML::FileFilter::Extension const& ext) {
|
||||
auto pattern = ByteString::formatted("*.{}", ext.value);
|
||||
gtk_file_filter_add_pattern(GTK_FILE_FILTER(filter.ptr()), pattern.characters());
|
||||
},
|
||||
[&](Web::HTML::FileFilter::MimeType const& mime) {
|
||||
auto mime_str = mime.value.to_byte_string();
|
||||
gtk_file_filter_add_mime_type(GTK_FILE_FILTER(filter.ptr()), mime_str.characters());
|
||||
},
|
||||
[&](Web::HTML::FileFilter::FileType const& file_type) {
|
||||
switch (file_type) {
|
||||
case Web::HTML::FileFilter::FileType::Audio:
|
||||
gtk_file_filter_add_mime_type(GTK_FILE_FILTER(filter.ptr()), "audio/*");
|
||||
break;
|
||||
case Web::HTML::FileFilter::FileType::Image:
|
||||
gtk_file_filter_add_mime_type(GTK_FILE_FILTER(filter.ptr()), "image/*");
|
||||
break;
|
||||
case Web::HTML::FileFilter::FileType::Video:
|
||||
gtk_file_filter_add_mime_type(GTK_FILE_FILTER(filter.ptr()), "video/*");
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
g_list_store_append(G_LIST_STORE(filters.ptr()), filter.ptr());
|
||||
}
|
||||
GObjectPtr all_filter { gtk_file_filter_new() };
|
||||
gtk_file_filter_set_name(GTK_FILE_FILTER(all_filter.ptr()), "All files");
|
||||
gtk_file_filter_add_pattern(GTK_FILE_FILTER(all_filter.ptr()), "*");
|
||||
g_list_store_append(G_LIST_STORE(filters.ptr()), all_filter.ptr());
|
||||
gtk_file_dialog_set_filters(GTK_FILE_DIALOG(dialog.ptr()), G_LIST_MODEL(filters.ptr()));
|
||||
|
||||
if (allow_multiple == Web::HTML::AllowMultipleFiles::Yes) {
|
||||
gtk_file_dialog_open_multiple(GTK_FILE_DIALOG(dialog.ptr()), parent, nullptr, +[](GObject* source, GAsyncResult* result, gpointer user_data) {
|
||||
auto* view = static_cast<WebContentView*>(user_data);
|
||||
GError* error = nullptr;
|
||||
auto* file_list = gtk_file_dialog_open_multiple_finish(GTK_FILE_DIALOG(source), result, &error);
|
||||
if (error) {
|
||||
view->file_picker_closed({});
|
||||
g_error_free(error);
|
||||
return;
|
||||
}
|
||||
GObjectPtr owned_file_list { file_list };
|
||||
Vector<Web::HTML::SelectedFile> selected;
|
||||
auto n = g_list_model_get_n_items(G_LIST_MODEL(file_list));
|
||||
for (guint i = 0; i < n; i++) {
|
||||
GObjectPtr file { g_list_model_get_item(G_LIST_MODEL(file_list), i) };
|
||||
g_autofree char* path = g_file_get_path(G_FILE(file.ptr()));
|
||||
if (path) {
|
||||
auto selected_file = Web::HTML::SelectedFile::from_file_path(ByteString(path));
|
||||
if (!selected_file.is_error())
|
||||
selected.append(selected_file.release_value());
|
||||
}
|
||||
}
|
||||
view->file_picker_closed(move(selected)); }, view);
|
||||
} else {
|
||||
gtk_file_dialog_open(GTK_FILE_DIALOG(dialog.ptr()), parent, nullptr, +[](GObject* source, GAsyncResult* result, gpointer user_data) {
|
||||
auto* view = static_cast<WebContentView*>(user_data);
|
||||
GError* error = nullptr;
|
||||
auto* file = gtk_file_dialog_open_finish(GTK_FILE_DIALOG(source), result, &error);
|
||||
if (error) {
|
||||
view->file_picker_closed({});
|
||||
g_error_free(error);
|
||||
return;
|
||||
}
|
||||
GObjectPtr owned_file { file };
|
||||
Vector<Web::HTML::SelectedFile> selected;
|
||||
g_autofree char* path = g_file_get_path(file);
|
||||
if (path) {
|
||||
auto selected_file = Web::HTML::SelectedFile::from_file_path(ByteString(path));
|
||||
if (!selected_file.is_error())
|
||||
selected.append(selected_file.release_value());
|
||||
}
|
||||
view->file_picker_closed(move(selected)); }, view);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
31
UI/Gtk/Dialogs.h
Normal file
31
UI/Gtk/Dialogs.h
Normal file
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright (c) 2026, Johan Dahlin <jdahlin@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/String.h>
|
||||
#include <LibGfx/Color.h>
|
||||
#include <LibWeb/HTML/FileFilter.h>
|
||||
#include <LibWeb/HTML/SelectedFile.h>
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
namespace Ladybird {
|
||||
|
||||
class WebContentView;
|
||||
|
||||
namespace Dialogs {
|
||||
|
||||
void show_error(GtkWindow* parent, StringView message);
|
||||
void show_alert(GtkWindow* parent, WebContentView* view, String const& message);
|
||||
void show_confirm(GtkWindow* parent, WebContentView* view, String const& message);
|
||||
void show_prompt(GtkWindow* parent, WebContentView* view, String const& message, String const& default_value);
|
||||
void show_color_picker(GtkWindow* parent, WebContentView* view, Color current_color);
|
||||
void show_file_picker(GtkWindow* parent, WebContentView* view, Web::HTML::FileFilter const& accepted_file_types, Web::HTML::AllowMultipleFiles allow_multiple);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
489
UI/Gtk/Menu.cpp
Normal file
489
UI/Gtk/Menu.cpp
Normal file
@@ -0,0 +1,489 @@
|
||||
/*
|
||||
* Copyright (c) 2026, Johan Dahlin <jdahlin@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <UI/Gtk/GLibPtr.h>
|
||||
#include <UI/Gtk/Menu.h>
|
||||
#include <UI/Gtk/WebContentView.h>
|
||||
|
||||
namespace Ladybird {
|
||||
|
||||
class ActionObserver final : public WebView::Action::Observer {
|
||||
public:
|
||||
explicit ActionObserver(GSimpleAction* gaction)
|
||||
: m_gaction(GObjectPtr<GSimpleAction> { G_SIMPLE_ACTION(g_object_ref(gaction)) })
|
||||
{
|
||||
}
|
||||
|
||||
void on_enabled_state_changed(WebView::Action& action) override
|
||||
{
|
||||
if (m_gaction.ptr())
|
||||
g_simple_action_set_enabled(m_gaction, action.enabled());
|
||||
}
|
||||
|
||||
void on_checked_state_changed(WebView::Action& action) override
|
||||
{
|
||||
if (m_gaction.ptr() && g_action_get_state_type(G_ACTION(m_gaction.ptr())))
|
||||
g_simple_action_set_state(m_gaction, g_variant_new_boolean(action.checked()));
|
||||
}
|
||||
|
||||
private:
|
||||
GObjectPtr<GSimpleAction> m_gaction;
|
||||
};
|
||||
|
||||
static void set_menu_item_icon_name(GMenuItem* item, char const* icon_name)
|
||||
{
|
||||
if (!icon_name)
|
||||
return;
|
||||
|
||||
GObjectPtr icon { g_themed_icon_new(icon_name) };
|
||||
g_menu_item_set_icon(item, G_ICON(icon.ptr()));
|
||||
}
|
||||
|
||||
static void set_menu_item_accel(GMenuItem* item, char const* accel)
|
||||
{
|
||||
if (!accel)
|
||||
return;
|
||||
g_menu_item_set_attribute(item, "accel", "s", accel);
|
||||
}
|
||||
|
||||
static char const* primary_accelerator_for_action(WebView::ActionID id)
|
||||
{
|
||||
switch (id) {
|
||||
case WebView::ActionID::NavigateBack:
|
||||
return "<Alt>Left";
|
||||
case WebView::ActionID::NavigateForward:
|
||||
return "<Alt>Right";
|
||||
case WebView::ActionID::Reload:
|
||||
return "<Ctrl>r";
|
||||
case WebView::ActionID::CopySelection:
|
||||
return "<Ctrl>c";
|
||||
case WebView::ActionID::Paste:
|
||||
return "<Ctrl>v";
|
||||
case WebView::ActionID::SelectAll:
|
||||
return "<Ctrl>a";
|
||||
case WebView::ActionID::ToggleBookmark:
|
||||
return "<Ctrl>d";
|
||||
case WebView::ActionID::ToggleBookmarksBar:
|
||||
return "<Ctrl><Shift>b";
|
||||
case WebView::ActionID::OpenProcessesPage:
|
||||
return "<Ctrl><Shift>m";
|
||||
case WebView::ActionID::OpenSettingsPage:
|
||||
return "<Ctrl>comma";
|
||||
case WebView::ActionID::ToggleDevTools:
|
||||
return "<Ctrl><Shift>i";
|
||||
case WebView::ActionID::ViewSource:
|
||||
return "<Ctrl>u";
|
||||
case WebView::ActionID::ZoomIn:
|
||||
return "<Ctrl>equal";
|
||||
case WebView::ActionID::ZoomOut:
|
||||
return "<Ctrl>minus";
|
||||
case WebView::ActionID::ResetZoom:
|
||||
case WebView::ActionID::ResetZoomViaToolbar:
|
||||
return "<Ctrl>0";
|
||||
case WebView::ActionID::CollectGarbage:
|
||||
return "<Ctrl><Shift>g";
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
static void initialize_native_control(WebView::Action& action, GSimpleAction* gaction, GMenuItem* menu_item)
|
||||
{
|
||||
if (gaction)
|
||||
g_simple_action_set_enabled(gaction, action.enabled());
|
||||
|
||||
auto set_icon = [&](char const* icon_name) {
|
||||
if (menu_item)
|
||||
set_menu_item_icon_name(menu_item, icon_name);
|
||||
};
|
||||
auto set_accel = [&](char const* accel) {
|
||||
if (menu_item)
|
||||
set_menu_item_accel(menu_item, accel);
|
||||
};
|
||||
|
||||
switch (action.id()) {
|
||||
case WebView::ActionID::NavigateBack:
|
||||
set_icon("go-previous-symbolic");
|
||||
set_accel("<Alt>Left");
|
||||
break;
|
||||
case WebView::ActionID::NavigateForward:
|
||||
set_icon("go-next-symbolic");
|
||||
set_accel("<Alt>Right");
|
||||
break;
|
||||
case WebView::ActionID::Reload:
|
||||
set_icon("view-refresh-symbolic");
|
||||
set_accel("<Ctrl>r");
|
||||
break;
|
||||
|
||||
case WebView::ActionID::CopySelection:
|
||||
set_icon("edit-copy-symbolic");
|
||||
set_accel("<Ctrl>c");
|
||||
break;
|
||||
case WebView::ActionID::Paste:
|
||||
set_icon("edit-paste-symbolic");
|
||||
set_accel("<Ctrl>v");
|
||||
break;
|
||||
case WebView::ActionID::SelectAll:
|
||||
set_icon("edit-select-all-symbolic");
|
||||
set_accel("<Ctrl>a");
|
||||
break;
|
||||
|
||||
case WebView::ActionID::SearchSelectedText:
|
||||
set_icon("edit-find-symbolic");
|
||||
break;
|
||||
|
||||
case WebView::ActionID::TakeVisibleScreenshot:
|
||||
case WebView::ActionID::TakeFullScreenshot:
|
||||
set_icon("image-x-generic-symbolic");
|
||||
break;
|
||||
|
||||
case WebView::ActionID::ToggleBookmark:
|
||||
case WebView::ActionID::ToggleBookmarkViaToolbar:
|
||||
set_icon(action.engaged() ? "starred-symbolic" : "non-starred-symbolic");
|
||||
set_accel("<Ctrl>d");
|
||||
break;
|
||||
case WebView::ActionID::ToggleBookmarksBar:
|
||||
set_icon("user-bookmarks-symbolic");
|
||||
set_accel("<Ctrl><Shift>b");
|
||||
break;
|
||||
case WebView::ActionID::BookmarkItem:
|
||||
set_icon("globe-symbolic");
|
||||
break;
|
||||
|
||||
case WebView::ActionID::OpenAboutPage:
|
||||
set_icon("help-about-symbolic");
|
||||
break;
|
||||
case WebView::ActionID::OpenProcessesPage:
|
||||
set_icon("utilities-system-monitor-symbolic");
|
||||
set_accel("<Ctrl><Shift>m");
|
||||
break;
|
||||
case WebView::ActionID::OpenSettingsPage:
|
||||
set_icon("emblem-system-symbolic");
|
||||
set_accel("<Ctrl>comma");
|
||||
break;
|
||||
case WebView::ActionID::ToggleDevTools:
|
||||
case WebView::ActionID::DumpDOMTree:
|
||||
set_icon("applications-engineering-symbolic");
|
||||
set_accel("<Ctrl><Shift>i");
|
||||
break;
|
||||
case WebView::ActionID::ViewSource:
|
||||
set_icon("text-html-symbolic");
|
||||
set_accel("<Ctrl>u");
|
||||
break;
|
||||
|
||||
case WebView::ActionID::OpenInNewTab:
|
||||
set_icon("tab-new-symbolic");
|
||||
break;
|
||||
case WebView::ActionID::CopyURL:
|
||||
set_icon("edit-copy-symbolic");
|
||||
break;
|
||||
|
||||
case WebView::ActionID::OpenImage:
|
||||
set_icon("image-x-generic-symbolic");
|
||||
break;
|
||||
case WebView::ActionID::SaveImage:
|
||||
set_icon("download-symbolic");
|
||||
break;
|
||||
case WebView::ActionID::CopyImage:
|
||||
set_icon("edit-copy-symbolic");
|
||||
break;
|
||||
|
||||
case WebView::ActionID::OpenAudio:
|
||||
set_icon("audio-x-generic-symbolic");
|
||||
break;
|
||||
case WebView::ActionID::OpenVideo:
|
||||
set_icon("video-x-generic-symbolic");
|
||||
break;
|
||||
case WebView::ActionID::PlayMedia:
|
||||
set_icon("media-playback-start-symbolic");
|
||||
break;
|
||||
case WebView::ActionID::PauseMedia:
|
||||
set_icon("media-playback-pause-symbolic");
|
||||
break;
|
||||
case WebView::ActionID::MuteMedia:
|
||||
set_icon("audio-volume-muted-symbolic");
|
||||
break;
|
||||
case WebView::ActionID::UnmuteMedia:
|
||||
set_icon("audio-volume-high-symbolic");
|
||||
break;
|
||||
case WebView::ActionID::ShowControls:
|
||||
set_icon("view-visible-symbolic");
|
||||
break;
|
||||
case WebView::ActionID::HideControls:
|
||||
set_icon("view-hidden-symbolic");
|
||||
break;
|
||||
case WebView::ActionID::ToggleMediaLoopState:
|
||||
set_icon("view-refresh-symbolic");
|
||||
break;
|
||||
case WebView::ActionID::EnterFullscreen:
|
||||
case WebView::ActionID::ExitFullscreen:
|
||||
set_icon("view-fullscreen-symbolic");
|
||||
break;
|
||||
|
||||
case WebView::ActionID::ZoomIn:
|
||||
set_icon("zoom-in-symbolic");
|
||||
set_accel("<Ctrl>equal");
|
||||
break;
|
||||
case WebView::ActionID::ZoomOut:
|
||||
set_icon("zoom-out-symbolic");
|
||||
set_accel("<Ctrl>minus");
|
||||
break;
|
||||
case WebView::ActionID::ResetZoom:
|
||||
case WebView::ActionID::ResetZoomViaToolbar:
|
||||
set_icon("zoom-original-symbolic");
|
||||
set_accel("<Ctrl>0");
|
||||
break;
|
||||
|
||||
case WebView::ActionID::DumpSessionHistoryTree:
|
||||
set_icon("document-open-recent-symbolic");
|
||||
break;
|
||||
case WebView::ActionID::DumpLayoutTree:
|
||||
case WebView::ActionID::DumpPaintTree:
|
||||
case WebView::ActionID::DumpDisplayList:
|
||||
set_icon("view-list-symbolic");
|
||||
break;
|
||||
case WebView::ActionID::DumpStackingContextTree:
|
||||
set_icon("view-grid-symbolic");
|
||||
break;
|
||||
case WebView::ActionID::DumpStyleSheets:
|
||||
case WebView::ActionID::DumpStyles:
|
||||
set_icon("text-x-css-symbolic");
|
||||
break;
|
||||
case WebView::ActionID::DumpCSSErrors:
|
||||
set_icon("dialog-error-symbolic");
|
||||
break;
|
||||
case WebView::ActionID::DumpCookies:
|
||||
set_icon("preferences-web-browser-cookies-symbolic");
|
||||
break;
|
||||
case WebView::ActionID::DumpLocalStorage:
|
||||
set_icon("drive-harddisk-symbolic");
|
||||
break;
|
||||
case WebView::ActionID::ShowLineBoxBorders:
|
||||
set_icon("view-grid-symbolic");
|
||||
break;
|
||||
case WebView::ActionID::CollectGarbage:
|
||||
set_icon("user-trash-symbolic");
|
||||
set_accel("<Ctrl><Shift>g");
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void add_items_to_menu(GMenu& menu, ReadonlySpan<WebView::Menu::MenuItem> menu_items, Function<ByteString(WebView::Action&)> const& detailed_action_name_for_action)
|
||||
{
|
||||
GObjectPtr current_section { g_menu_new() };
|
||||
bool section_has_items = false;
|
||||
|
||||
auto flush_section = [&] {
|
||||
if (!section_has_items)
|
||||
return;
|
||||
g_menu_append_section(&menu, nullptr, G_MENU_MODEL(current_section.ptr()));
|
||||
current_section = GObjectPtr<GMenu> { g_menu_new() };
|
||||
section_has_items = false;
|
||||
};
|
||||
|
||||
for (auto& menu_item : menu_items) {
|
||||
menu_item.visit(
|
||||
[&](NonnullRefPtr<WebView::Action> const& action) {
|
||||
if (!action->visible())
|
||||
return;
|
||||
|
||||
auto label = action->text().to_byte_string();
|
||||
auto detailed_action_name = detailed_action_name_for_action(*action);
|
||||
GObjectPtr gitem { g_menu_item_new(label.characters(), detailed_action_name.characters()) };
|
||||
initialize_native_control(*action, nullptr, G_MENU_ITEM(gitem.ptr()));
|
||||
g_menu_append_item(G_MENU(current_section.ptr()), G_MENU_ITEM(gitem.ptr()));
|
||||
section_has_items = true;
|
||||
},
|
||||
[&](NonnullRefPtr<WebView::Menu> const& submenu) {
|
||||
GObjectPtr submenu_model { create_application_menu(*submenu, detailed_action_name_for_action) };
|
||||
auto title = submenu->title().to_byte_string();
|
||||
GObjectPtr gitem { g_menu_item_new_submenu(title.characters(), G_MENU_MODEL(submenu_model.ptr())) };
|
||||
|
||||
if (submenu->render_group_icon())
|
||||
set_menu_item_icon_name(G_MENU_ITEM(gitem.ptr()), "folder-symbolic");
|
||||
|
||||
g_menu_append_item(G_MENU(current_section.ptr()), G_MENU_ITEM(gitem.ptr()));
|
||||
section_has_items = true;
|
||||
},
|
||||
[&](WebView::Separator) {
|
||||
flush_section();
|
||||
});
|
||||
}
|
||||
|
||||
flush_section();
|
||||
}
|
||||
|
||||
class ContextMenu final {
|
||||
public:
|
||||
ContextMenu(GtkWidget& parent, WebView::Menu& source)
|
||||
: m_popover(GTK_POPOVER(gtk_popover_menu_new_from_model(nullptr)))
|
||||
{
|
||||
gtk_widget_set_parent(GTK_WIDGET(m_popover), &parent);
|
||||
m_action_group = GObjectPtr<GSimpleActionGroup> { g_simple_action_group_new() };
|
||||
gtk_widget_insert_action_group(GTK_WIDGET(m_popover), "context", G_ACTION_GROUP(m_action_group.ptr()));
|
||||
|
||||
size_t action_index = 0;
|
||||
source.for_each_action([&](WebView::Action& action) {
|
||||
auto action_name = ByteString::formatted("item-{}", action_index++);
|
||||
m_action_names.set(&action, action_name);
|
||||
add_action_to_map(G_ACTION_MAP(m_action_group.ptr()), action_name.characters(), action);
|
||||
});
|
||||
}
|
||||
|
||||
~ContextMenu()
|
||||
{
|
||||
gtk_widget_unparent(GTK_WIDGET(m_popover));
|
||||
}
|
||||
|
||||
void popup(WebContentView& view, WebView::Menu& source, Gfx::IntPoint position)
|
||||
{
|
||||
GObjectPtr menu_model { create_application_menu(source, [&](WebView::Action& action) {
|
||||
return ByteString::formatted("context.{}", m_action_names.get(&action).value());
|
||||
}) };
|
||||
gtk_popover_menu_set_menu_model(GTK_POPOVER_MENU(m_popover), G_MENU_MODEL(menu_model.ptr()));
|
||||
|
||||
auto device_pixel_ratio = view.device_pixel_ratio();
|
||||
GdkRectangle rect = {
|
||||
static_cast<int>(position.x() / device_pixel_ratio),
|
||||
static_cast<int>(position.y() / device_pixel_ratio),
|
||||
1, 1
|
||||
};
|
||||
gtk_popover_set_pointing_to(m_popover, &rect);
|
||||
gtk_popover_popup(m_popover);
|
||||
}
|
||||
|
||||
private:
|
||||
GtkPopover* m_popover { nullptr };
|
||||
GObjectPtr<GSimpleActionGroup> m_action_group;
|
||||
HashMap<WebView::Action const*, ByteString> m_action_names;
|
||||
};
|
||||
|
||||
static GSimpleAction* create_application_action(char const* action_name, WebView::Action& action, bool observe_state)
|
||||
{
|
||||
GSimpleAction* gaction = action.is_checkable()
|
||||
? g_simple_action_new_stateful(action_name, nullptr, g_variant_new_boolean(action.checked()))
|
||||
: g_simple_action_new(action_name, nullptr);
|
||||
|
||||
auto* weak_action = new WeakPtr<WebView::Action>(action.make_weak_ptr());
|
||||
g_signal_connect_data(gaction, "activate", G_CALLBACK(+[](GSimpleAction*, GVariant*, gpointer user_data) {
|
||||
auto* weak = static_cast<WeakPtr<WebView::Action>*>(user_data);
|
||||
if (auto action = weak->strong_ref()) {
|
||||
if (action->is_checkable())
|
||||
action->set_checked(!action->checked());
|
||||
action->activate();
|
||||
} }), weak_action, +[](gpointer data, GClosure*) { delete static_cast<WeakPtr<WebView::Action>*>(data); }, static_cast<GConnectFlags>(0));
|
||||
|
||||
initialize_native_control(action, gaction, nullptr);
|
||||
if (observe_state)
|
||||
action.add_observer(make<ActionObserver>(gaction));
|
||||
return gaction;
|
||||
}
|
||||
|
||||
void add_action_to_map(GActionMap* action_map, char const* action_name, WebView::Action& action, bool observe_state)
|
||||
{
|
||||
GObjectPtr gaction { create_application_action(action_name, action, observe_state) };
|
||||
g_action_map_add_action(action_map, G_ACTION(gaction.ptr()));
|
||||
}
|
||||
|
||||
GMenu* create_application_menu(WebView::Menu& menu, Function<ByteString(WebView::Action&)> const& detailed_action_name_for_action)
|
||||
{
|
||||
auto* gmenu = g_menu_new();
|
||||
add_items_to_menu(*gmenu, menu.items(), detailed_action_name_for_action);
|
||||
return gmenu;
|
||||
}
|
||||
|
||||
void create_context_menu(GtkWidget& parent, WebContentView& view, WebView::Menu& menu)
|
||||
{
|
||||
auto context_menu = make<ContextMenu>(parent, menu);
|
||||
menu.on_activation = [context_menu = move(context_menu), &view, weak_menu = menu.make_weak_ptr()](Gfx::IntPoint position) {
|
||||
if (auto strong_menu = weak_menu.strong_ref())
|
||||
context_menu->popup(view, *strong_menu, position);
|
||||
};
|
||||
}
|
||||
|
||||
void add_menu_actions_to_map(GActionMap* action_map, WebView::Menu& menu, Function<ByteString(WebView::Action&)> const& action_name_for_action)
|
||||
{
|
||||
menu.for_each_action([&](WebView::Action& action) {
|
||||
auto action_name = action_name_for_action(action);
|
||||
add_action_to_map(action_map, action_name.characters(), action);
|
||||
});
|
||||
}
|
||||
|
||||
void install_action_accelerators(GtkApplication* application, char const* detailed_action_name, WebView::Action const& action)
|
||||
{
|
||||
switch (action.id()) {
|
||||
case WebView::ActionID::Reload: {
|
||||
static constexpr char const* accels[] = { "<Ctrl>r", "F5", nullptr };
|
||||
gtk_application_set_accels_for_action(application, detailed_action_name, accels);
|
||||
break;
|
||||
}
|
||||
case WebView::ActionID::ToggleDevTools: {
|
||||
static constexpr char const* accels[] = { "<Ctrl><Shift>i", "<Ctrl><Shift>c", "F12", nullptr };
|
||||
gtk_application_set_accels_for_action(application, detailed_action_name, accels);
|
||||
break;
|
||||
}
|
||||
case WebView::ActionID::ZoomIn: {
|
||||
static constexpr char const* accels[] = { "<Ctrl>equal", "<Ctrl>plus", nullptr };
|
||||
gtk_application_set_accels_for_action(application, detailed_action_name, accels);
|
||||
break;
|
||||
}
|
||||
case WebView::ActionID::NavigateBack:
|
||||
case WebView::ActionID::NavigateForward:
|
||||
case WebView::ActionID::ToggleBookmark:
|
||||
case WebView::ActionID::ToggleBookmarksBar:
|
||||
case WebView::ActionID::OpenProcessesPage:
|
||||
case WebView::ActionID::OpenSettingsPage:
|
||||
case WebView::ActionID::ViewSource:
|
||||
case WebView::ActionID::ZoomOut:
|
||||
case WebView::ActionID::ResetZoom:
|
||||
case WebView::ActionID::ResetZoomViaToolbar:
|
||||
case WebView::ActionID::CollectGarbage: {
|
||||
auto const* accel = primary_accelerator_for_action(action.id());
|
||||
if (!accel)
|
||||
return;
|
||||
char const* accels[] = { accel, nullptr };
|
||||
gtk_application_set_accels_for_action(application, detailed_action_name, accels);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void install_menu_action_accelerators(GtkApplication* application, char const* prefix, WebView::Menu& menu)
|
||||
{
|
||||
menu.for_each_action([&](WebView::Action& action) {
|
||||
auto detailed_action_name = ByteString::formatted("{}-{}", prefix, static_cast<int>(action.id()));
|
||||
install_action_accelerators(application, detailed_action_name.characters(), action);
|
||||
});
|
||||
}
|
||||
|
||||
void append_submenu_to_section_containing_action(GMenu* menu, char const* detailed_action_name, char const* submenu_label, GMenuModel* submenu_model)
|
||||
{
|
||||
int n_items = g_menu_model_get_n_items(G_MENU_MODEL(menu));
|
||||
for (int i = 0; i < n_items; ++i) {
|
||||
GObjectPtr section { g_menu_model_get_item_link(G_MENU_MODEL(menu), i, G_MENU_LINK_SECTION) };
|
||||
if (!section.ptr())
|
||||
continue;
|
||||
|
||||
int section_items = g_menu_model_get_n_items(G_MENU_MODEL(section.ptr()));
|
||||
for (int j = 0; j < section_items; ++j) {
|
||||
g_autofree char* action = nullptr;
|
||||
g_menu_model_get_item_attribute(G_MENU_MODEL(section.ptr()), j, G_MENU_ATTRIBUTE_ACTION, "s", &action);
|
||||
if (action && g_strcmp0(action, detailed_action_name) == 0) {
|
||||
g_menu_append_submenu(G_MENU(section.ptr()), submenu_label, submenu_model);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
g_menu_append_submenu(menu, submenu_label, submenu_model);
|
||||
}
|
||||
|
||||
}
|
||||
27
UI/Gtk/Menu.h
Normal file
27
UI/Gtk/Menu.h
Normal file
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright (c) 2026, Johan Dahlin <jdahlin@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/Function.h>
|
||||
#include <LibWebView/Menu.h>
|
||||
|
||||
#include <gio/gio.h>
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
namespace Ladybird {
|
||||
|
||||
class WebContentView;
|
||||
|
||||
void add_action_to_map(GActionMap* action_map, char const* action_name, WebView::Action& action, bool observe_state = true);
|
||||
GMenu* create_application_menu(WebView::Menu& menu, Function<ByteString(WebView::Action&)> const& detailed_action_name_for_action);
|
||||
void create_context_menu(GtkWidget& parent, WebContentView& view, WebView::Menu& menu);
|
||||
void add_menu_actions_to_map(GActionMap* action_map, WebView::Menu& menu, Function<ByteString(WebView::Action&)> const& action_name_for_action);
|
||||
void install_action_accelerators(GtkApplication* application, char const* detailed_action_name, WebView::Action const& action);
|
||||
void install_menu_action_accelerators(GtkApplication* application, char const* prefix, WebView::Menu& menu);
|
||||
void append_submenu_to_section_containing_action(GMenu* menu, char const* detailed_action_name, char const* submenu_label, GMenuModel* submenu_model);
|
||||
|
||||
}
|
||||
252
UI/Gtk/Resources/browser-window.ui
Normal file
252
UI/Gtk/Resources/browser-window.ui
Normal file
@@ -0,0 +1,252 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<requires lib="gtk" version="4.0"/>
|
||||
<requires lib="libadwaita" version="1.4"/>
|
||||
|
||||
<template class="LadybirdBrowserWindow" parent="AdwApplicationWindow">
|
||||
<property name="title">Ladybird</property>
|
||||
<property name="default-width">1024</property>
|
||||
<property name="default-height">768</property>
|
||||
<property name="content">
|
||||
<object class="AdwToolbarView" id="toolbar_view">
|
||||
<!-- Top bar: Header -->
|
||||
<child type="top">
|
||||
<object class="AdwHeaderBar" id="header_bar">
|
||||
<child type="start">
|
||||
<object class="GtkBox" id="nav_box">
|
||||
<style><class name="linked"/></style>
|
||||
<child>
|
||||
<object class="GtkButton" id="back_button">
|
||||
<property name="action-name">win.go-back</property>
|
||||
<property name="icon-name">go-previous-symbolic</property>
|
||||
<property name="tooltip-text">Back (Alt+Left)</property>
|
||||
<style><class name="flat"/></style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="forward_button">
|
||||
<property name="action-name">win.go-forward</property>
|
||||
<property name="icon-name">go-next-symbolic</property>
|
||||
<property name="tooltip-text">Forward (Alt+Right)</property>
|
||||
<style><class name="flat"/></style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="reload_button">
|
||||
<property name="action-name">win.reload</property>
|
||||
<property name="icon-name">view-refresh-symbolic</property>
|
||||
<property name="tooltip-text">Reload (Ctrl+R)</property>
|
||||
<style><class name="flat"/></style>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child type="start">
|
||||
<object class="GtkButton" id="new_tab_button">
|
||||
<property name="action-name">win.new-tab</property>
|
||||
<property name="icon-name">tab-new-symbolic</property>
|
||||
<property name="tooltip-text">New Tab (Ctrl+T)</property>
|
||||
<style><class name="flat"/></style>
|
||||
</object>
|
||||
</child>
|
||||
<child type="end">
|
||||
<object class="GtkButton" id="restore_button">
|
||||
<property name="icon-name">view-restore-symbolic</property>
|
||||
<property name="tooltip-text">Exit Fullscreen</property>
|
||||
<property name="action-name">win.fullscreen</property>
|
||||
<property name="visible">false</property>
|
||||
</object>
|
||||
</child>
|
||||
<child type="end">
|
||||
<object class="GtkMenuButton" id="menu_button">
|
||||
<property name="icon-name">open-menu-symbolic</property>
|
||||
<property name="tooltip-text">Menu</property>
|
||||
<property name="popover">hamburger_popover</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
|
||||
<!-- Top bar: Find bar -->
|
||||
<child type="top">
|
||||
<object class="GtkRevealer" id="find_bar_revealer">
|
||||
<property name="transition-type">slide-down</property>
|
||||
<property name="reveal-child">false</property>
|
||||
<property name="child">
|
||||
<object class="GtkBox" id="find_bar">
|
||||
<property name="orientation">horizontal</property>
|
||||
<property name="spacing">4</property>
|
||||
<property name="margin-start">8</property>
|
||||
<property name="margin-end">8</property>
|
||||
<property name="margin-top">4</property>
|
||||
<property name="margin-bottom">4</property>
|
||||
<style><class name="toolbar"/></style>
|
||||
<child>
|
||||
<object class="GtkSearchEntry" id="find_entry">
|
||||
<property name="hexpand">true</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="find_result_label">
|
||||
<style><class name="dim-label"/></style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="find_prev_button">
|
||||
<property name="action-name">win.find-previous</property>
|
||||
<property name="icon-name">go-up-symbolic</property>
|
||||
<property name="tooltip-text">Previous Match (Shift+Enter)</property>
|
||||
<style><class name="flat"/></style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="find_next_button">
|
||||
<property name="action-name">win.find-next</property>
|
||||
<property name="icon-name">go-down-symbolic</property>
|
||||
<property name="tooltip-text">Next Match (Enter)</property>
|
||||
<style><class name="flat"/></style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="find_close_button">
|
||||
<property name="action-name">win.find-close</property>
|
||||
<property name="icon-name">window-close-symbolic</property>
|
||||
<property name="tooltip-text">Close (Escape)</property>
|
||||
<style><class name="flat"/></style>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
|
||||
<!-- Top bar: Tab bar -->
|
||||
<child type="top">
|
||||
<object class="AdwTabBar" id="tab_bar">
|
||||
<property name="autohide">true</property>
|
||||
<property name="view">tab_view</property>
|
||||
</object>
|
||||
</child>
|
||||
|
||||
<!-- Content: Toast overlay wrapping tab view -->
|
||||
<property name="content">
|
||||
<object class="AdwToastOverlay" id="toast_overlay">
|
||||
<property name="child">
|
||||
<object class="AdwTabView" id="tab_view">
|
||||
<property name="vexpand">true</property>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</property>
|
||||
|
||||
<!-- Bottom bar: DevTools banner -->
|
||||
<child type="bottom">
|
||||
<object class="AdwBanner" id="devtools_banner">
|
||||
<property name="revealed">false</property>
|
||||
<property name="button-label">Disable</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</property>
|
||||
</template>
|
||||
|
||||
<!-- Hamburger menu model -->
|
||||
<menu id="hamburger_menu">
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="custom">zoom</attribute>
|
||||
</item>
|
||||
</section>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="custom">tools</attribute>
|
||||
</item>
|
||||
</section>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label">New Tab</attribute>
|
||||
<attribute name="action">win.new-tab</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label">New Window</attribute>
|
||||
<attribute name="action">win.new-window</attribute>
|
||||
</item>
|
||||
</section>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label">Preferences</attribute>
|
||||
<attribute name="action">win.preferences</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label">About Ladybird</attribute>
|
||||
<attribute name="action">win.about</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label">Quit</attribute>
|
||||
<attribute name="action">win.quit</attribute>
|
||||
</item>
|
||||
</section>
|
||||
</menu>
|
||||
|
||||
<!-- Hamburger popover with custom widget slots -->
|
||||
<object class="GtkPopoverMenu" id="hamburger_popover">
|
||||
<property name="menu-model">hamburger_menu</property>
|
||||
<child type="zoom">
|
||||
<object class="GtkBox" id="zoom_box">
|
||||
<property name="orientation">horizontal</property>
|
||||
<property name="hexpand">true</property>
|
||||
<property name="homogeneous">true</property>
|
||||
<style><class name="linked"/></style>
|
||||
<child>
|
||||
<object class="GtkButton" id="zoom_out_button">
|
||||
<property name="action-name">win.zoom-out</property>
|
||||
<property name="icon-name">zoom-out-symbolic</property>
|
||||
<property name="tooltip-text">Zoom Out</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="zoom_reset_button">
|
||||
<property name="action-name">win.zoom-reset</property>
|
||||
<property name="tooltip-text">Reset Zoom</property>
|
||||
<property name="child">
|
||||
<object class="GtkLabel" id="zoom_label">
|
||||
<property name="label">100%</property>
|
||||
<property name="width-chars">5</property>
|
||||
<style><class name="numeric"/></style>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="zoom_in_button">
|
||||
<property name="action-name">win.zoom-in</property>
|
||||
<property name="icon-name">zoom-in-symbolic</property>
|
||||
<property name="tooltip-text">Zoom In</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child type="tools">
|
||||
<object class="GtkBox" id="tools_box">
|
||||
<property name="orientation">horizontal</property>
|
||||
<property name="hexpand">true</property>
|
||||
<property name="homogeneous">true</property>
|
||||
<style><class name="linked"/></style>
|
||||
<child>
|
||||
<object class="GtkButton" id="find_menu_button">
|
||||
<property name="action-name">win.find</property>
|
||||
<property name="icon-name">system-search-symbolic</property>
|
||||
<property name="tooltip-text">Find in Page</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="fullscreen_menu_button">
|
||||
<property name="action-name">win.fullscreen</property>
|
||||
<property name="icon-name">view-fullscreen-symbolic</property>
|
||||
<property name="tooltip-text">Fullscreen</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
||||
21
UI/Gtk/Resources/list-popover.ui
Normal file
21
UI/Gtk/Resources/list-popover.ui
Normal file
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<requires lib="gtk" version="4.0"/>
|
||||
|
||||
<object class="GtkPopover" id="popover">
|
||||
<property name="has-arrow">false</property>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="scrolled_window">
|
||||
<property name="hscrollbar-policy">never</property>
|
||||
<property name="vscrollbar-policy">automatic</property>
|
||||
<property name="max-content-height">300</property>
|
||||
<property name="propagate-natural-height">true</property>
|
||||
<property name="child">
|
||||
<object class="GtkListBox" id="list_box">
|
||||
<style><class name="boxed-list"/></style>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
||||
24
UI/Gtk/Resources/location-entry.ui
Normal file
24
UI/Gtk/Resources/location-entry.ui
Normal file
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<requires lib="gtk" version="4.0"/>
|
||||
|
||||
<object class="GtkPopover" id="completion_popover">
|
||||
<property name="autohide">false</property>
|
||||
<property name="has-arrow">false</property>
|
||||
<property name="position">bottom</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="child">
|
||||
<object class="GtkScrolledWindow">
|
||||
<property name="hscrollbar-policy">never</property>
|
||||
<property name="vscrollbar-policy">automatic</property>
|
||||
<property name="max-content-height">300</property>
|
||||
<property name="propagate-natural-height">true</property>
|
||||
<property name="child">
|
||||
<object class="GtkListBox" id="completion_list_box">
|
||||
<property name="selection-mode">browse</property>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</interface>
|
||||
8
UI/Gtk/Resources/resources.gresource.xml
Normal file
8
UI/Gtk/Resources/resources.gresource.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<gresources>
|
||||
<gresource prefix="/org/ladybird/Ladybird/gtk">
|
||||
<file preprocess="xml-stripblanks">browser-window.ui</file>
|
||||
<file preprocess="xml-stripblanks">list-popover.ui</file>
|
||||
<file preprocess="xml-stripblanks">location-entry.ui</file>
|
||||
</gresource>
|
||||
</gresources>
|
||||
347
UI/Gtk/Tab.cpp
Normal file
347
UI/Gtk/Tab.cpp
Normal file
@@ -0,0 +1,347 @@
|
||||
/*
|
||||
* Copyright (c) 2026, Johan Dahlin <jdahlin@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <LibGfx/Bitmap.h>
|
||||
#include <LibGfx/Cursor.h>
|
||||
#include <LibURL/URL.h>
|
||||
#include <LibWeb/HTML/SelectItem.h>
|
||||
#include <LibWebView/Menu.h>
|
||||
#include <LibWebView/URL.h>
|
||||
#include <UI/Gtk/BrowserWindow.h>
|
||||
#include <UI/Gtk/Dialogs.h>
|
||||
#include <UI/Gtk/Events.h>
|
||||
#include <UI/Gtk/GLibPtr.h>
|
||||
#include <UI/Gtk/Menu.h>
|
||||
#include <UI/Gtk/Tab.h>
|
||||
#include <UI/Gtk/WebContentView.h>
|
||||
#include <UI/Gtk/Widgets/Builder.h>
|
||||
|
||||
namespace Ladybird {
|
||||
|
||||
struct ListPopoverShell {
|
||||
GtkPopover* popover { nullptr };
|
||||
GtkListBox* list_box { nullptr };
|
||||
};
|
||||
|
||||
static ListPopoverShell create_list_popover_shell(GtkWidget* parent)
|
||||
{
|
||||
GObjectPtr builder { gtk_builder_new_from_resource("/org/ladybird/Ladybird/gtk/list-popover.ui") };
|
||||
auto* popover = LadybirdWidgets::get_builder_object<GtkPopover>(builder, "popover");
|
||||
auto* list_box = LadybirdWidgets::get_builder_object<GtkListBox>(builder, "list_box");
|
||||
gtk_widget_set_parent(GTK_WIDGET(popover), parent);
|
||||
return { popover, list_box };
|
||||
}
|
||||
|
||||
static void append_select_option_row(GtkListBox* list_box, char const* label, bool selected, bool disabled, unsigned id, int margin_start)
|
||||
{
|
||||
auto* row = gtk_list_box_row_new();
|
||||
auto* box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
|
||||
gtk_widget_set_margin_start(box, margin_start);
|
||||
gtk_widget_set_margin_end(box, 8);
|
||||
gtk_widget_set_margin_top(box, 4);
|
||||
gtk_widget_set_margin_bottom(box, 4);
|
||||
|
||||
if (selected) {
|
||||
auto* check = gtk_image_new_from_icon_name("object-select-symbolic");
|
||||
gtk_box_append(GTK_BOX(box), check);
|
||||
} else {
|
||||
auto* spacer = gtk_image_new_from_icon_name("object-select-symbolic");
|
||||
gtk_widget_set_opacity(spacer, 0);
|
||||
gtk_box_append(GTK_BOX(box), spacer);
|
||||
}
|
||||
|
||||
auto* label_widget = gtk_label_new(label);
|
||||
gtk_label_set_xalign(GTK_LABEL(label_widget), 0.0);
|
||||
gtk_box_append(GTK_BOX(box), label_widget);
|
||||
|
||||
gtk_list_box_row_set_child(GTK_LIST_BOX_ROW(row), box);
|
||||
gtk_widget_set_sensitive(row, !disabled);
|
||||
g_object_set_data(G_OBJECT(row), "item-id", GUINT_TO_POINTER(id));
|
||||
gtk_list_box_append(GTK_LIST_BOX(list_box), row);
|
||||
}
|
||||
|
||||
Tab::Tab(BrowserWindow& window, URL::URL url)
|
||||
: Tab(window, nullptr, 0)
|
||||
{
|
||||
m_initial_url = move(url);
|
||||
if (!m_initial_url.scheme().is_empty())
|
||||
navigate(m_initial_url);
|
||||
}
|
||||
|
||||
Tab::Tab(BrowserWindow& window, WebView::WebContentClient& parent_client, u64 page_index)
|
||||
: Tab(window, &parent_client, page_index)
|
||||
{
|
||||
}
|
||||
|
||||
Tab::Tab(BrowserWindow& window, RefPtr<WebView::WebContentClient> parent_client, size_t page_index)
|
||||
: m_window(window)
|
||||
{
|
||||
m_web_view = ladybird_web_view_new();
|
||||
gtk_widget_set_vexpand(GTK_WIDGET(m_web_view), TRUE);
|
||||
gtk_widget_set_hexpand(GTK_WIDGET(m_web_view), TRUE);
|
||||
m_view = adopt_own(*new WebContentView(m_web_view, parent_client, page_index));
|
||||
|
||||
setup_callbacks();
|
||||
}
|
||||
|
||||
Tab::~Tab() = default;
|
||||
|
||||
void Tab::setup_callbacks()
|
||||
{
|
||||
auto* root = GTK_WIDGET(m_web_view);
|
||||
|
||||
m_view->on_title_change = [this](auto const& title) {
|
||||
if (m_tab_page) {
|
||||
auto utf8 = title.to_utf8();
|
||||
auto byte_str = ByteString(utf8.bytes_as_string_view());
|
||||
adw_tab_page_set_title(m_tab_page, byte_str.characters());
|
||||
}
|
||||
};
|
||||
|
||||
m_view->on_url_change = [this](auto const& url) {
|
||||
if (m_window.current_tab() != this)
|
||||
return;
|
||||
if (BrowserWindow::is_internal_url(url)) {
|
||||
m_window.update_location_entry(""sv);
|
||||
return;
|
||||
}
|
||||
auto url_string = url.serialize();
|
||||
m_window.update_location_entry(url_string.bytes_as_string_view());
|
||||
};
|
||||
|
||||
m_view->on_load_start = [this](auto const&, bool) {
|
||||
if (m_tab_page)
|
||||
adw_tab_page_set_loading(m_tab_page, TRUE);
|
||||
};
|
||||
|
||||
m_view->on_load_finish = [this](auto const&) {
|
||||
if (m_tab_page)
|
||||
adw_tab_page_set_loading(m_tab_page, FALSE);
|
||||
};
|
||||
|
||||
m_view->on_cursor_change = [root](auto const& cursor) {
|
||||
auto cursor_name = cursor.visit(
|
||||
[](Gfx::StandardCursor standard_cursor) -> StringView {
|
||||
return standard_cursor_to_css_name(standard_cursor);
|
||||
},
|
||||
[](Gfx::ImageCursor const&) -> StringView {
|
||||
return "default"sv;
|
||||
});
|
||||
auto cursor_name_str = ByteString(cursor_name);
|
||||
GObjectPtr gdk_cursor { gdk_cursor_new_from_name(cursor_name_str.characters(), nullptr) };
|
||||
gtk_widget_set_cursor(root, GDK_CURSOR(gdk_cursor.ptr()));
|
||||
};
|
||||
|
||||
m_view->on_enter_tooltip_area = [root](auto const& tooltip) {
|
||||
auto text = ByteString(tooltip);
|
||||
gtk_widget_set_tooltip_text(root, text.characters());
|
||||
};
|
||||
|
||||
m_view->on_leave_tooltip_area = [root]() {
|
||||
gtk_widget_set_tooltip_text(root, nullptr);
|
||||
};
|
||||
|
||||
m_view->on_link_hover = [root](auto const& url) {
|
||||
auto url_string = url.serialize();
|
||||
auto byte_string = ByteString(url_string.bytes_as_string_view());
|
||||
gtk_widget_set_tooltip_text(root, byte_string.characters());
|
||||
};
|
||||
|
||||
m_view->on_link_unhover = [root]() {
|
||||
gtk_widget_set_tooltip_text(root, nullptr);
|
||||
};
|
||||
|
||||
m_view->on_new_web_view = [this](auto activate_tab, auto, auto page_index) -> String {
|
||||
if (page_index.has_value()) {
|
||||
auto& new_tab = m_window.create_child_tab(activate_tab, *this, page_index.value());
|
||||
return new_tab.view().handle();
|
||||
}
|
||||
auto& new_tab = m_window.create_new_tab(activate_tab);
|
||||
return new_tab.view().handle();
|
||||
};
|
||||
|
||||
m_view->on_activate_tab = [root]() {
|
||||
gtk_widget_grab_focus(root);
|
||||
};
|
||||
|
||||
m_view->on_close = [this]() {
|
||||
m_window.close_tab(*this);
|
||||
};
|
||||
|
||||
m_view->on_zoom_level_changed = [this]() {
|
||||
m_window.update_zoom_label();
|
||||
};
|
||||
|
||||
// Dialogs
|
||||
m_view->on_request_alert = [this](auto const& message) {
|
||||
Dialogs::show_alert(m_window.gtk_window(), m_view.ptr(), message);
|
||||
};
|
||||
|
||||
m_view->on_request_confirm = [this](auto const& message) {
|
||||
Dialogs::show_confirm(m_window.gtk_window(), m_view.ptr(), message);
|
||||
};
|
||||
|
||||
m_view->on_request_prompt = [this](auto const& message, auto const& default_value) {
|
||||
Dialogs::show_prompt(m_window.gtk_window(), m_view.ptr(), message, default_value);
|
||||
};
|
||||
|
||||
m_view->on_request_color_picker = [this](Color current_color) {
|
||||
Dialogs::show_color_picker(m_window.gtk_window(), m_view.ptr(), current_color);
|
||||
};
|
||||
|
||||
m_view->on_request_file_picker = [this](auto const& accepted_file_types, auto allow_multiple) {
|
||||
Dialogs::show_file_picker(m_window.gtk_window(), m_view.ptr(), accepted_file_types, allow_multiple);
|
||||
};
|
||||
|
||||
m_view->on_request_select_dropdown = [this](Gfx::IntPoint content_position, i32 minimum_width, Vector<Web::HTML::SelectItem> items) {
|
||||
show_select_dropdown(content_position, minimum_width, move(items));
|
||||
};
|
||||
|
||||
m_view->on_find_in_page = [this](auto current_match_index, auto const& total_match_count) {
|
||||
m_window.update_find_in_page_result(current_match_index, total_match_count);
|
||||
};
|
||||
|
||||
m_view->on_fullscreen_window = [this]() {
|
||||
gtk_window_fullscreen(m_window.gtk_window());
|
||||
};
|
||||
|
||||
m_view->on_exit_fullscreen_window = [this]() {
|
||||
gtk_window_unfullscreen(m_window.gtk_window());
|
||||
};
|
||||
|
||||
m_view->on_restore_window = [this]() {
|
||||
gtk_window_unmaximize(m_window.gtk_window());
|
||||
gtk_window_unfullscreen(m_window.gtk_window());
|
||||
};
|
||||
|
||||
m_view->on_maximize_window = [this]() {
|
||||
gtk_window_maximize(m_window.gtk_window());
|
||||
};
|
||||
|
||||
m_view->on_minimize_window = [this]() {
|
||||
gtk_window_minimize(m_window.gtk_window());
|
||||
};
|
||||
|
||||
m_view->on_reposition_window = [](auto) {
|
||||
// GTK4 removed window repositioning APIs. On Wayland, clients cannot
|
||||
// position their own top-level windows. On other backends, GTK4 chose
|
||||
// not to expose it for portability.
|
||||
};
|
||||
|
||||
m_view->on_resize_window = [this](auto size) {
|
||||
gtk_window_set_default_size(m_window.gtk_window(), size.width(), size.height());
|
||||
};
|
||||
|
||||
m_view->on_favicon_change = [this](auto const& bitmap) {
|
||||
if (!m_tab_page)
|
||||
return;
|
||||
g_autoptr(GBytes) bytes = g_bytes_new(bitmap.scanline_u8(0), bitmap.size_in_bytes());
|
||||
GObjectPtr texture { gdk_memory_texture_new(bitmap.width(), bitmap.height(), GDK_MEMORY_B8G8R8A8_PREMULTIPLIED, bytes, bitmap.pitch()) };
|
||||
adw_tab_page_set_icon(m_tab_page, G_ICON(texture.ptr()));
|
||||
};
|
||||
|
||||
m_view->on_audio_play_state_changed = [this](auto play_state) {
|
||||
if (m_tab_page) {
|
||||
adw_tab_page_set_indicator_icon(m_tab_page,
|
||||
play_state == Web::HTML::AudioPlayState::Playing
|
||||
? g_themed_icon_new("audio-volume-high-symbolic")
|
||||
: nullptr);
|
||||
}
|
||||
};
|
||||
|
||||
// FIXME: Support non-modal JS dialogs (on_request_set_prompt_text,
|
||||
// on_request_accept_dialog, on_request_dismiss_dialog) for WebDriver support.
|
||||
|
||||
// Context menus
|
||||
create_context_menu(*root, *m_view, m_view->page_context_menu());
|
||||
create_context_menu(*root, *m_view, m_view->link_context_menu());
|
||||
create_context_menu(*root, *m_view, m_view->image_context_menu());
|
||||
create_context_menu(*root, *m_view, m_view->media_context_menu());
|
||||
}
|
||||
|
||||
void Tab::navigate(URL::URL const& url)
|
||||
{
|
||||
m_view->load(url);
|
||||
}
|
||||
|
||||
void Tab::show_select_dropdown(Gfx::IntPoint content_position, i32 minimum_width, Vector<Web::HTML::SelectItem> items)
|
||||
{
|
||||
auto* root = GTK_WIDGET(m_web_view);
|
||||
auto shell = create_list_popover_shell(root);
|
||||
auto* popover = shell.popover;
|
||||
auto* list_box = shell.list_box;
|
||||
gtk_widget_set_size_request(GTK_WIDGET(popover), minimum_width, -1);
|
||||
|
||||
auto device_pixel_ratio = m_view->device_pixel_ratio();
|
||||
GdkRectangle rect = {
|
||||
static_cast<int>(content_position.x() / device_pixel_ratio),
|
||||
static_cast<int>(content_position.y() / device_pixel_ratio),
|
||||
1, 1
|
||||
};
|
||||
gtk_popover_set_pointing_to(GTK_POPOVER(popover), &rect);
|
||||
gtk_list_box_set_selection_mode(GTK_LIST_BOX(list_box), GTK_SELECTION_NONE);
|
||||
|
||||
for (auto const& item : items) {
|
||||
item.visit(
|
||||
[&](Web::HTML::SelectItemOption const& option) {
|
||||
append_select_option_row(list_box, option.label.to_byte_string().characters(), option.selected, option.disabled, option.id, 8);
|
||||
},
|
||||
[&](Web::HTML::SelectItemOptionGroup const& group) {
|
||||
auto* header = gtk_label_new(group.label.to_byte_string().characters());
|
||||
gtk_label_set_xalign(GTK_LABEL(header), 0.0);
|
||||
gtk_widget_add_css_class(header, "heading");
|
||||
gtk_widget_set_margin_start(header, 8);
|
||||
gtk_widget_set_margin_top(header, 6);
|
||||
gtk_widget_set_margin_bottom(header, 2);
|
||||
auto* header_row = gtk_list_box_row_new();
|
||||
gtk_list_box_row_set_child(GTK_LIST_BOX_ROW(header_row), header);
|
||||
gtk_list_box_row_set_activatable(GTK_LIST_BOX_ROW(header_row), FALSE);
|
||||
gtk_list_box_row_set_selectable(GTK_LIST_BOX_ROW(header_row), FALSE);
|
||||
gtk_list_box_append(GTK_LIST_BOX(list_box), header_row);
|
||||
|
||||
for (auto const& option : group.items) {
|
||||
append_select_option_row(list_box, option.label.to_byte_string().characters(), option.selected, option.disabled, option.id, 16);
|
||||
}
|
||||
},
|
||||
[&](Web::HTML::SelectItemSeparator const&) {
|
||||
auto* sep_row = gtk_list_box_row_new();
|
||||
gtk_list_box_row_set_child(GTK_LIST_BOX_ROW(sep_row), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL));
|
||||
gtk_list_box_row_set_activatable(GTK_LIST_BOX_ROW(sep_row), FALSE);
|
||||
gtk_list_box_row_set_selectable(GTK_LIST_BOX_ROW(sep_row), FALSE);
|
||||
gtk_list_box_append(GTK_LIST_BOX(list_box), sep_row);
|
||||
},
|
||||
[&](auto const&) {});
|
||||
}
|
||||
|
||||
struct DropdownState {
|
||||
WebContentView* view;
|
||||
GtkPopover* popover;
|
||||
bool selected { false };
|
||||
};
|
||||
auto* dropdown_state = new DropdownState { m_view.ptr(), GTK_POPOVER(popover) };
|
||||
|
||||
g_signal_connect(list_box, "row-activated", G_CALLBACK(+[](GtkListBox*, GtkListBoxRow* row, gpointer user_data) {
|
||||
auto* state = static_cast<DropdownState*>(user_data);
|
||||
auto item_id = GPOINTER_TO_UINT(g_object_get_data(G_OBJECT(row), "item-id"));
|
||||
state->selected = true;
|
||||
state->view->select_dropdown_closed(item_id);
|
||||
gtk_popover_popdown(state->popover);
|
||||
}),
|
||||
dropdown_state);
|
||||
|
||||
g_signal_connect(popover, "closed", G_CALLBACK(+[](GtkPopover* popover, gpointer user_data) {
|
||||
auto* state = static_cast<DropdownState*>(user_data);
|
||||
if (!state->selected)
|
||||
state->view->select_dropdown_closed({});
|
||||
delete state;
|
||||
gtk_widget_unparent(GTK_WIDGET(popover));
|
||||
}),
|
||||
dropdown_state);
|
||||
|
||||
gtk_popover_popup(GTK_POPOVER(popover));
|
||||
}
|
||||
|
||||
}
|
||||
55
UI/Gtk/Tab.h
Normal file
55
UI/Gtk/Tab.h
Normal file
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright (c) 2026, Johan Dahlin <jdahlin@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/NonnullOwnPtr.h>
|
||||
#include <AK/OwnPtr.h>
|
||||
#include <AK/RefPtr.h>
|
||||
#include <LibGfx/Point.h>
|
||||
#include <LibURL/URL.h>
|
||||
#include <LibWeb/HTML/ActivateTab.h>
|
||||
#include <LibWeb/HTML/SelectItem.h>
|
||||
#include <LibWebView/Forward.h>
|
||||
#include <UI/Gtk/Widgets/LadybirdWebView.h>
|
||||
|
||||
#include <adwaita.h>
|
||||
|
||||
namespace Ladybird {
|
||||
|
||||
class BrowserWindow;
|
||||
class WebContentView;
|
||||
|
||||
class Tab {
|
||||
public:
|
||||
Tab(BrowserWindow& window, URL::URL url = {});
|
||||
Tab(BrowserWindow& window, WebView::WebContentClient& parent_client, u64 page_index);
|
||||
~Tab();
|
||||
|
||||
GtkWidget* widget() const { return GTK_WIDGET(m_web_view); }
|
||||
WebContentView& view() { return *m_view; }
|
||||
WebContentView const& view() const { return *m_view; }
|
||||
|
||||
void navigate(URL::URL const& url);
|
||||
|
||||
AdwTabPage* tab_page() const { return m_tab_page; }
|
||||
void set_tab_page(AdwTabPage* page) { m_tab_page = page; }
|
||||
|
||||
private:
|
||||
Tab(BrowserWindow& window, RefPtr<WebView::WebContentClient> parent_client, size_t page_index);
|
||||
void setup_callbacks();
|
||||
void show_select_dropdown(Gfx::IntPoint content_position, i32 minimum_width, Vector<Web::HTML::SelectItem> items);
|
||||
|
||||
BrowserWindow& m_window;
|
||||
OwnPtr<WebContentView> m_view;
|
||||
|
||||
LadybirdWebView* m_web_view { nullptr };
|
||||
AdwTabPage* m_tab_page { nullptr };
|
||||
|
||||
URL::URL m_initial_url;
|
||||
};
|
||||
|
||||
}
|
||||
88
UI/Gtk/Widgets/LadybirdBrowserWindow.cpp
Normal file
88
UI/Gtk/Widgets/LadybirdBrowserWindow.cpp
Normal file
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* Copyright (c) 2026, Johan Dahlin <jdahlin@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <AK/Assertions.h>
|
||||
#include <UI/Gtk/Widgets/LadybirdBrowserWindow.h>
|
||||
|
||||
struct LadybirdBrowserWindow {
|
||||
AdwApplicationWindow parent_instance;
|
||||
AdwToolbarView* toolbar_view { nullptr };
|
||||
AdwHeaderBar* header_bar { nullptr };
|
||||
GtkButton* back_button { nullptr };
|
||||
GtkButton* forward_button { nullptr };
|
||||
GtkButton* reload_button { nullptr };
|
||||
GtkButton* restore_button { nullptr };
|
||||
GtkMenuButton* menu_button { nullptr };
|
||||
AdwTabView* tab_view { nullptr };
|
||||
AdwTabBar* tab_bar { nullptr };
|
||||
AdwToastOverlay* toast_overlay { nullptr };
|
||||
GtkRevealer* find_bar_revealer { nullptr };
|
||||
GtkSearchEntry* find_entry { nullptr };
|
||||
GtkLabel* find_result_label { nullptr };
|
||||
GtkButton* zoom_reset_button { nullptr };
|
||||
GtkLabel* zoom_label { nullptr };
|
||||
AdwBanner* devtools_banner { nullptr };
|
||||
GMenu* hamburger_menu { nullptr };
|
||||
};
|
||||
|
||||
struct LadybirdBrowserWindowClass {
|
||||
AdwApplicationWindowClass parent_class;
|
||||
};
|
||||
|
||||
GType ladybird_browser_window_get_type(void);
|
||||
G_DEFINE_FINAL_TYPE(LadybirdBrowserWindow, ladybird_browser_window, ADW_TYPE_APPLICATION_WINDOW)
|
||||
|
||||
static void ladybird_browser_window_class_init(LadybirdBrowserWindowClass* klass)
|
||||
{
|
||||
auto* widget_class = GTK_WIDGET_CLASS(klass);
|
||||
gtk_widget_class_set_template_from_resource(widget_class, "/org/ladybird/Ladybird/gtk/browser-window.ui");
|
||||
|
||||
gtk_widget_class_bind_template_child(widget_class, LadybirdBrowserWindow, toolbar_view);
|
||||
gtk_widget_class_bind_template_child(widget_class, LadybirdBrowserWindow, header_bar);
|
||||
gtk_widget_class_bind_template_child(widget_class, LadybirdBrowserWindow, back_button);
|
||||
gtk_widget_class_bind_template_child(widget_class, LadybirdBrowserWindow, forward_button);
|
||||
gtk_widget_class_bind_template_child(widget_class, LadybirdBrowserWindow, reload_button);
|
||||
gtk_widget_class_bind_template_child(widget_class, LadybirdBrowserWindow, restore_button);
|
||||
gtk_widget_class_bind_template_child(widget_class, LadybirdBrowserWindow, menu_button);
|
||||
gtk_widget_class_bind_template_child(widget_class, LadybirdBrowserWindow, tab_view);
|
||||
gtk_widget_class_bind_template_child(widget_class, LadybirdBrowserWindow, tab_bar);
|
||||
gtk_widget_class_bind_template_child(widget_class, LadybirdBrowserWindow, toast_overlay);
|
||||
gtk_widget_class_bind_template_child(widget_class, LadybirdBrowserWindow, find_bar_revealer);
|
||||
gtk_widget_class_bind_template_child(widget_class, LadybirdBrowserWindow, find_entry);
|
||||
gtk_widget_class_bind_template_child(widget_class, LadybirdBrowserWindow, find_result_label);
|
||||
gtk_widget_class_bind_template_child(widget_class, LadybirdBrowserWindow, zoom_reset_button);
|
||||
gtk_widget_class_bind_template_child(widget_class, LadybirdBrowserWindow, zoom_label);
|
||||
gtk_widget_class_bind_template_child(widget_class, LadybirdBrowserWindow, devtools_banner);
|
||||
gtk_widget_class_bind_template_child(widget_class, LadybirdBrowserWindow, hamburger_menu);
|
||||
}
|
||||
|
||||
static void ladybird_browser_window_init(LadybirdBrowserWindow* self)
|
||||
{
|
||||
gtk_widget_init_template(GTK_WIDGET(self));
|
||||
}
|
||||
|
||||
namespace LadybirdWidgets {
|
||||
|
||||
LadybirdBrowserWindow* create_browser_window_widget(AdwApplication* app)
|
||||
{
|
||||
return reinterpret_cast<LadybirdBrowserWindow*>(g_object_new(ladybird_browser_window_get_type(),
|
||||
"application", app,
|
||||
nullptr));
|
||||
}
|
||||
|
||||
AdwHeaderBar* browser_window_header_bar(LadybirdBrowserWindow* window) { return window->header_bar; }
|
||||
AdwTabView* browser_window_tab_view(LadybirdBrowserWindow* window) { return window->tab_view; }
|
||||
GtkButton* browser_window_restore_button(LadybirdBrowserWindow* window) { return window->restore_button; }
|
||||
GtkLabel* browser_window_zoom_label(LadybirdBrowserWindow* window) { return window->zoom_label; }
|
||||
AdwBanner* browser_window_devtools_banner(LadybirdBrowserWindow* window) { return window->devtools_banner; }
|
||||
GtkRevealer* browser_window_find_bar_revealer(LadybirdBrowserWindow* window) { return window->find_bar_revealer; }
|
||||
GtkSearchEntry* browser_window_find_entry(LadybirdBrowserWindow* window) { return window->find_entry; }
|
||||
GtkLabel* browser_window_find_result_label(LadybirdBrowserWindow* window) { return window->find_result_label; }
|
||||
GMenu* browser_window_hamburger_menu(LadybirdBrowserWindow* window) { return window->hamburger_menu; }
|
||||
AdwToastOverlay* browser_window_toast_overlay(LadybirdBrowserWindow* window) { return window->toast_overlay; }
|
||||
GtkMenuButton* browser_window_menu_button(LadybirdBrowserWindow* window) { return window->menu_button; }
|
||||
|
||||
}
|
||||
29
UI/Gtk/Widgets/LadybirdBrowserWindow.h
Normal file
29
UI/Gtk/Widgets/LadybirdBrowserWindow.h
Normal file
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright (c) 2026, Johan Dahlin <jdahlin@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <adwaita.h>
|
||||
|
||||
struct LadybirdBrowserWindow;
|
||||
|
||||
namespace LadybirdWidgets {
|
||||
|
||||
LadybirdBrowserWindow* create_browser_window_widget(AdwApplication* app);
|
||||
|
||||
AdwHeaderBar* browser_window_header_bar(LadybirdBrowserWindow*);
|
||||
AdwTabView* browser_window_tab_view(LadybirdBrowserWindow*);
|
||||
GtkButton* browser_window_restore_button(LadybirdBrowserWindow*);
|
||||
GtkLabel* browser_window_zoom_label(LadybirdBrowserWindow*);
|
||||
AdwBanner* browser_window_devtools_banner(LadybirdBrowserWindow*);
|
||||
GtkRevealer* browser_window_find_bar_revealer(LadybirdBrowserWindow*);
|
||||
GtkSearchEntry* browser_window_find_entry(LadybirdBrowserWindow*);
|
||||
GtkLabel* browser_window_find_result_label(LadybirdBrowserWindow*);
|
||||
GMenu* browser_window_hamburger_menu(LadybirdBrowserWindow*);
|
||||
AdwToastOverlay* browser_window_toast_overlay(LadybirdBrowserWindow*);
|
||||
GtkMenuButton* browser_window_menu_button(LadybirdBrowserWindow*);
|
||||
|
||||
}
|
||||
368
UI/Gtk/Widgets/LadybirdLocationEntry.cpp
Normal file
368
UI/Gtk/Widgets/LadybirdLocationEntry.cpp
Normal file
@@ -0,0 +1,368 @@
|
||||
/*
|
||||
* Copyright (c) 2026, Johan Dahlin <jdahlin@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <AK/NonnullOwnPtr.h>
|
||||
#include <AK/OwnPtr.h>
|
||||
#include <AK/String.h>
|
||||
#include <AK/Vector.h>
|
||||
#include <LibWebView/Application.h>
|
||||
#include <LibWebView/Autocomplete.h>
|
||||
#include <LibWebView/URL.h>
|
||||
#include <UI/Gtk/GLibPtr.h>
|
||||
#include <UI/Gtk/Widgets/Builder.h>
|
||||
#include <UI/Gtk/Widgets/LadybirdLocationEntry.h>
|
||||
|
||||
struct LocationEntryState {
|
||||
NonnullOwnPtr<WebView::Autocomplete> autocomplete;
|
||||
Vector<WebView::AutocompleteSuggestion> suggestions;
|
||||
int selected_index { -1 };
|
||||
String user_text;
|
||||
bool is_focused { false };
|
||||
bool updating_text { false };
|
||||
Function<void(String)> on_navigate;
|
||||
};
|
||||
|
||||
#define LADYBIRD_LOCATION_ENTRY(obj) (reinterpret_cast<LadybirdLocationEntry*>(obj))
|
||||
#define LADYBIRD_TYPE_LOCATION_ENTRY (ladybird_location_entry_get_type())
|
||||
|
||||
struct LadybirdLocationEntry {
|
||||
GtkEntry parent_instance;
|
||||
|
||||
GtkPopover* popover { nullptr };
|
||||
GtkListBox* list_box { nullptr };
|
||||
// GObject allocates this struct with g_malloc0, which zero-fills without
|
||||
// calling C++ constructors. OwnPtr is safe here because zero-initialized
|
||||
// OwnPtr is equivalent to nullptr (empty state).
|
||||
OwnPtr<LocationEntryState> state;
|
||||
};
|
||||
|
||||
struct LadybirdLocationEntryClass {
|
||||
GtkEntryClass parent_class;
|
||||
};
|
||||
|
||||
G_DEFINE_FINAL_TYPE(LadybirdLocationEntry, ladybird_location_entry, GTK_TYPE_ENTRY)
|
||||
|
||||
static void ladybird_location_entry_update_display_attributes(LadybirdLocationEntry* self);
|
||||
static void ladybird_location_entry_show_completions(LadybirdLocationEntry* self);
|
||||
static void ladybird_location_entry_hide_completions(LadybirdLocationEntry* self);
|
||||
static void ladybird_location_entry_navigate(LadybirdLocationEntry* self);
|
||||
static void ladybird_location_entry_move_selection(LadybirdLocationEntry* self, int delta);
|
||||
static void ladybird_location_entry_apply_selected_suggestion(LadybirdLocationEntry* self);
|
||||
|
||||
static void set_entry_text_suppressed(LadybirdLocationEntry* self, char const* text, bool move_cursor_to_end = false)
|
||||
{
|
||||
self->state->updating_text = true;
|
||||
gtk_editable_set_text(GTK_EDITABLE(self), text);
|
||||
if (move_cursor_to_end)
|
||||
gtk_editable_set_position(GTK_EDITABLE(self), -1);
|
||||
self->state->updating_text = false;
|
||||
}
|
||||
|
||||
static void ladybird_location_entry_finalize(GObject* object)
|
||||
{
|
||||
auto* self = LADYBIRD_LOCATION_ENTRY(object);
|
||||
if (self->popover) {
|
||||
gtk_popover_popdown(self->popover);
|
||||
gtk_widget_unparent(GTK_WIDGET(self->popover));
|
||||
self->popover = nullptr;
|
||||
}
|
||||
self->state.clear();
|
||||
G_OBJECT_CLASS(ladybird_location_entry_parent_class)->finalize(object);
|
||||
}
|
||||
|
||||
static void ladybird_location_entry_class_init(LadybirdLocationEntryClass* klass)
|
||||
{
|
||||
auto* object_class = G_OBJECT_CLASS(klass);
|
||||
object_class->finalize = ladybird_location_entry_finalize;
|
||||
}
|
||||
|
||||
static void ladybird_location_entry_init(LadybirdLocationEntry* self)
|
||||
{
|
||||
self->state = adopt_own(*new LocationEntryState { .autocomplete = make<WebView::Autocomplete>(), .suggestions = {}, .user_text = {}, .on_navigate = {} });
|
||||
|
||||
gtk_widget_set_hexpand(GTK_WIDGET(self), TRUE);
|
||||
|
||||
if (auto const& search_engine = WebView::Application::settings().search_engine(); search_engine.has_value()) {
|
||||
auto placeholder = ByteString::formatted("Search with {} or enter URL", search_engine->name);
|
||||
gtk_entry_set_placeholder_text(GTK_ENTRY(self), placeholder.characters());
|
||||
} else {
|
||||
gtk_entry_set_placeholder_text(GTK_ENTRY(self), "Enter URL or search...");
|
||||
}
|
||||
|
||||
// Load completion popover from resource
|
||||
Ladybird::GObjectPtr builder { gtk_builder_new_from_resource("/org/ladybird/Ladybird/gtk/location-entry.ui") };
|
||||
self->popover = LadybirdWidgets::get_builder_object<GtkPopover>(builder, "completion_popover");
|
||||
self->list_box = LadybirdWidgets::get_builder_object<GtkListBox>(builder, "completion_list_box");
|
||||
gtk_widget_set_parent(GTK_WIDGET(self->popover), GTK_WIDGET(self));
|
||||
|
||||
// Clicking a suggestion navigates to it
|
||||
g_signal_connect_swapped(self->list_box, "row-activated", G_CALLBACK(+[](LadybirdLocationEntry* self, GtkListBoxRow* row) {
|
||||
auto index = gtk_list_box_row_get_index(row);
|
||||
if (index >= 0 && static_cast<size_t>(index) < self->state->suggestions.size()) {
|
||||
set_entry_text_suppressed(self, self->state->suggestions[index].text.to_byte_string().characters());
|
||||
ladybird_location_entry_hide_completions(self);
|
||||
ladybird_location_entry_navigate(self);
|
||||
}
|
||||
}),
|
||||
self);
|
||||
|
||||
// Autocomplete results callback
|
||||
self->state->autocomplete->on_autocomplete_query_complete = [self](auto suggestions, auto) {
|
||||
if (suggestions.is_empty() || !self->state->is_focused) {
|
||||
ladybird_location_entry_hide_completions(self);
|
||||
return;
|
||||
}
|
||||
self->state->suggestions = move(suggestions);
|
||||
self->state->selected_index = -1;
|
||||
ladybird_location_entry_show_completions(self);
|
||||
};
|
||||
|
||||
// Text changed -> query autocomplete
|
||||
g_signal_connect_swapped(self, "changed", G_CALLBACK(+[](LadybirdLocationEntry* self, GtkEditable*) {
|
||||
if (!self->state->is_focused || self->state->updating_text)
|
||||
return;
|
||||
auto* text = gtk_editable_get_text(GTK_EDITABLE(self));
|
||||
if (!text || text[0] == '\0') {
|
||||
ladybird_location_entry_hide_completions(self);
|
||||
return;
|
||||
}
|
||||
self->state->user_text = MUST(String::from_utf8(StringView { text, strlen(text) }));
|
||||
self->state->autocomplete->query_autocomplete_engine(self->state->user_text);
|
||||
}),
|
||||
self);
|
||||
|
||||
// Enter navigates
|
||||
g_signal_connect_swapped(self, "activate", G_CALLBACK(+[](LadybirdLocationEntry* self, GtkEntry*) {
|
||||
ladybird_location_entry_hide_completions(self);
|
||||
ladybird_location_entry_navigate(self);
|
||||
}),
|
||||
self);
|
||||
|
||||
// Key controller for Up/Down/Escape
|
||||
auto* key_controller = gtk_event_controller_key_new();
|
||||
g_signal_connect_swapped(key_controller, "key-pressed", G_CALLBACK(+[](LadybirdLocationEntry* self, guint keyval, guint, GdkModifierType) -> gboolean {
|
||||
if (!gtk_widget_get_visible(GTK_WIDGET(self->popover)))
|
||||
return GDK_EVENT_PROPAGATE;
|
||||
|
||||
switch (keyval) {
|
||||
case GDK_KEY_Down:
|
||||
ladybird_location_entry_move_selection(self, 1);
|
||||
return GDK_EVENT_STOP;
|
||||
case GDK_KEY_Up:
|
||||
ladybird_location_entry_move_selection(self, -1);
|
||||
return GDK_EVENT_STOP;
|
||||
case GDK_KEY_Escape:
|
||||
ladybird_location_entry_hide_completions(self);
|
||||
if (!self->state->user_text.is_empty())
|
||||
set_entry_text_suppressed(self, self->state->user_text.to_byte_string().characters());
|
||||
return GDK_EVENT_STOP;
|
||||
default:
|
||||
return GDK_EVENT_PROPAGATE;
|
||||
}
|
||||
}),
|
||||
self);
|
||||
gtk_widget_add_controller(GTK_WIDGET(self), GTK_EVENT_CONTROLLER(key_controller));
|
||||
|
||||
// Focus tracking
|
||||
auto* focus_controller = gtk_event_controller_focus_new();
|
||||
g_signal_connect_swapped(focus_controller, "enter", G_CALLBACK(+[](LadybirdLocationEntry* self, GtkEventControllerFocus*) {
|
||||
self->state->is_focused = true;
|
||||
gtk_entry_set_attributes(GTK_ENTRY(self), nullptr);
|
||||
}),
|
||||
self);
|
||||
g_signal_connect_swapped(focus_controller, "leave", G_CALLBACK(+[](LadybirdLocationEntry* self, GtkEventControllerFocus*) {
|
||||
self->state->is_focused = false;
|
||||
ladybird_location_entry_hide_completions(self);
|
||||
ladybird_location_entry_update_display_attributes(self);
|
||||
}),
|
||||
self);
|
||||
gtk_widget_add_controller(GTK_WIDGET(self), GTK_EVENT_CONTROLLER(focus_controller));
|
||||
}
|
||||
|
||||
// Public API
|
||||
|
||||
LadybirdLocationEntry* ladybird_location_entry_new(void)
|
||||
{
|
||||
return LADYBIRD_LOCATION_ENTRY(g_object_new(LADYBIRD_TYPE_LOCATION_ENTRY, nullptr));
|
||||
}
|
||||
|
||||
void ladybird_location_entry_set_url(LadybirdLocationEntry* self, char const* url)
|
||||
{
|
||||
set_entry_text_suppressed(self, url ? url : "");
|
||||
|
||||
// Extract scheme for security icon
|
||||
if (url) {
|
||||
auto sv = StringView(url, strlen(url));
|
||||
auto colon = sv.find(':');
|
||||
if (colon.has_value()) {
|
||||
auto scheme = sv.substring_view(0, *colon);
|
||||
auto scheme_bs = ByteString(scheme);
|
||||
ladybird_location_entry_set_security_icon(self, scheme_bs.characters());
|
||||
} else {
|
||||
ladybird_location_entry_set_security_icon(self, nullptr);
|
||||
}
|
||||
} else {
|
||||
ladybird_location_entry_set_security_icon(self, nullptr);
|
||||
}
|
||||
|
||||
if (!self->state->is_focused)
|
||||
ladybird_location_entry_update_display_attributes(self);
|
||||
}
|
||||
|
||||
void ladybird_location_entry_set_text(LadybirdLocationEntry* self, char const* text)
|
||||
{
|
||||
set_entry_text_suppressed(self, text ? text : "");
|
||||
gtk_entry_set_attributes(GTK_ENTRY(self), nullptr);
|
||||
ladybird_location_entry_set_security_icon(self, nullptr);
|
||||
}
|
||||
|
||||
void ladybird_location_entry_set_security_icon(LadybirdLocationEntry* self, char const* scheme)
|
||||
{
|
||||
if (!scheme) {
|
||||
gtk_entry_set_icon_from_icon_name(GTK_ENTRY(self), GTK_ENTRY_ICON_PRIMARY, nullptr);
|
||||
return;
|
||||
}
|
||||
|
||||
auto sv = StringView(scheme, strlen(scheme));
|
||||
if (sv == "https"sv) {
|
||||
gtk_entry_set_icon_from_icon_name(GTK_ENTRY(self), GTK_ENTRY_ICON_PRIMARY, "channel-secure-symbolic");
|
||||
gtk_entry_set_icon_tooltip_text(GTK_ENTRY(self), GTK_ENTRY_ICON_PRIMARY, "Secure connection");
|
||||
} else if (sv == "file"sv || sv == "resource"sv || sv == "about"sv || sv == "data"sv) {
|
||||
gtk_entry_set_icon_from_icon_name(GTK_ENTRY(self), GTK_ENTRY_ICON_PRIMARY, nullptr);
|
||||
} else {
|
||||
gtk_entry_set_icon_from_icon_name(GTK_ENTRY(self), GTK_ENTRY_ICON_PRIMARY, "channel-insecure-symbolic");
|
||||
gtk_entry_set_icon_tooltip_text(GTK_ENTRY(self), GTK_ENTRY_ICON_PRIMARY, "Insecure connection");
|
||||
}
|
||||
}
|
||||
|
||||
void ladybird_location_entry_focus_and_select_all(LadybirdLocationEntry* self)
|
||||
{
|
||||
gtk_widget_grab_focus(GTK_WIDGET(self));
|
||||
gtk_editable_select_region(GTK_EDITABLE(self), 0, -1);
|
||||
}
|
||||
|
||||
void ladybird_location_entry_set_on_navigate(LadybirdLocationEntry* self, Function<void(String)> callback)
|
||||
{
|
||||
self->state->on_navigate = move(callback);
|
||||
}
|
||||
|
||||
// Internal helpers
|
||||
|
||||
static void ladybird_location_entry_update_display_attributes(LadybirdLocationEntry* self)
|
||||
{
|
||||
auto* text = gtk_editable_get_text(GTK_EDITABLE(self));
|
||||
if (!text || text[0] == '\0') {
|
||||
gtk_entry_set_attributes(GTK_ENTRY(self), nullptr);
|
||||
return;
|
||||
}
|
||||
|
||||
auto url_str = StringView(text, strlen(text));
|
||||
auto url_parts = WebView::break_url_into_parts(url_str);
|
||||
|
||||
auto* attrs = pango_attr_list_new();
|
||||
|
||||
if (url_parts.has_value()) {
|
||||
auto* dim = pango_attr_foreground_alpha_new(40000);
|
||||
pango_attr_list_insert(attrs, dim);
|
||||
|
||||
auto highlight_start = url_parts->scheme_and_subdomain.length();
|
||||
auto highlight_end = highlight_start + url_parts->effective_tld_plus_one.length();
|
||||
|
||||
if (highlight_start < highlight_end) {
|
||||
auto* domain_alpha = pango_attr_foreground_alpha_new(65535);
|
||||
domain_alpha->start_index = highlight_start;
|
||||
domain_alpha->end_index = highlight_end;
|
||||
pango_attr_list_insert(attrs, domain_alpha);
|
||||
|
||||
auto* semi_bold = pango_attr_weight_new(PANGO_WEIGHT_MEDIUM);
|
||||
semi_bold->start_index = highlight_start;
|
||||
semi_bold->end_index = highlight_end;
|
||||
pango_attr_list_insert(attrs, semi_bold);
|
||||
}
|
||||
}
|
||||
|
||||
gtk_entry_set_attributes(GTK_ENTRY(self), attrs);
|
||||
pango_attr_list_unref(attrs);
|
||||
}
|
||||
|
||||
static void ladybird_location_entry_navigate(LadybirdLocationEntry* self)
|
||||
{
|
||||
auto* text = gtk_editable_get_text(GTK_EDITABLE(self));
|
||||
if (!text || text[0] == '\0')
|
||||
return;
|
||||
auto query = MUST(String::from_utf8(StringView { text, strlen(text) }));
|
||||
if (auto url = WebView::sanitize_url(query, WebView::Application::settings().search_engine()); url.has_value()) {
|
||||
if (self->state->on_navigate)
|
||||
self->state->on_navigate(url->serialize());
|
||||
}
|
||||
}
|
||||
|
||||
static void ladybird_location_entry_show_completions(LadybirdLocationEntry* self)
|
||||
{
|
||||
GtkWidget* child;
|
||||
while ((child = gtk_widget_get_first_child(GTK_WIDGET(self->list_box))) != nullptr)
|
||||
gtk_list_box_remove(self->list_box, child);
|
||||
|
||||
for (auto const& suggestion : self->state->suggestions) {
|
||||
auto byte_str = suggestion.text.to_byte_string();
|
||||
auto* label = gtk_label_new(byte_str.characters());
|
||||
gtk_label_set_xalign(GTK_LABEL(label), 0.0);
|
||||
gtk_label_set_ellipsize(GTK_LABEL(label), PANGO_ELLIPSIZE_END);
|
||||
gtk_widget_set_margin_start(label, 8);
|
||||
gtk_widget_set_margin_end(label, 8);
|
||||
gtk_widget_set_margin_top(label, 4);
|
||||
gtk_widget_set_margin_bottom(label, 4);
|
||||
gtk_list_box_append(self->list_box, label);
|
||||
}
|
||||
|
||||
gtk_list_box_unselect_all(self->list_box);
|
||||
|
||||
auto entry_width = gtk_widget_get_width(GTK_WIDGET(self));
|
||||
if (entry_width > 0)
|
||||
gtk_widget_set_size_request(GTK_WIDGET(self->popover), entry_width, -1);
|
||||
|
||||
gtk_popover_popup(self->popover);
|
||||
}
|
||||
|
||||
static void ladybird_location_entry_hide_completions(LadybirdLocationEntry* self)
|
||||
{
|
||||
self->state->suggestions.clear();
|
||||
self->state->selected_index = -1;
|
||||
gtk_popover_popdown(self->popover);
|
||||
}
|
||||
|
||||
static void ladybird_location_entry_move_selection(LadybirdLocationEntry* self, int delta)
|
||||
{
|
||||
auto& state = *self->state;
|
||||
if (state.suggestions.is_empty())
|
||||
return;
|
||||
|
||||
auto new_index = state.selected_index + delta;
|
||||
if (new_index < -1)
|
||||
new_index = static_cast<int>(state.suggestions.size()) - 1;
|
||||
if (new_index >= static_cast<int>(state.suggestions.size()))
|
||||
new_index = -1;
|
||||
|
||||
state.selected_index = new_index;
|
||||
|
||||
if (state.selected_index >= 0) {
|
||||
auto* row = gtk_list_box_get_row_at_index(self->list_box, state.selected_index);
|
||||
gtk_list_box_select_row(self->list_box, row);
|
||||
ladybird_location_entry_apply_selected_suggestion(self);
|
||||
} else {
|
||||
gtk_list_box_unselect_all(self->list_box);
|
||||
set_entry_text_suppressed(self, state.user_text.to_byte_string().characters(), true);
|
||||
}
|
||||
}
|
||||
|
||||
static void ladybird_location_entry_apply_selected_suggestion(LadybirdLocationEntry* self)
|
||||
{
|
||||
auto& state = *self->state;
|
||||
if (state.selected_index < 0 || static_cast<size_t>(state.selected_index) >= state.suggestions.size())
|
||||
return;
|
||||
|
||||
set_entry_text_suppressed(self, state.suggestions[state.selected_index].text.to_byte_string().characters(), true);
|
||||
}
|
||||
21
UI/Gtk/Widgets/LadybirdLocationEntry.h
Normal file
21
UI/Gtk/Widgets/LadybirdLocationEntry.h
Normal file
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright (c) 2026, Johan Dahlin <jdahlin@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/Function.h>
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
struct LadybirdLocationEntry;
|
||||
|
||||
GType ladybird_location_entry_get_type(void);
|
||||
LadybirdLocationEntry* ladybird_location_entry_new(void);
|
||||
|
||||
void ladybird_location_entry_set_url(LadybirdLocationEntry* self, char const* url);
|
||||
void ladybird_location_entry_set_text(LadybirdLocationEntry* self, char const* text);
|
||||
void ladybird_location_entry_set_security_icon(LadybirdLocationEntry* self, char const* scheme);
|
||||
void ladybird_location_entry_focus_and_select_all(LadybirdLocationEntry* self);
|
||||
void ladybird_location_entry_set_on_navigate(LadybirdLocationEntry* self, Function<void(String)> callback);
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <LibMain/Main.h>
|
||||
#include <LibWebView/Application.h>
|
||||
#include <UI/Gtk/Application.h>
|
||||
#include <UI/Gtk/BrowserWindow.h>
|
||||
|
||||
ErrorOr<int> ladybird_main(Main::Arguments arguments)
|
||||
{
|
||||
@@ -14,5 +15,12 @@ ErrorOr<int> ladybird_main(Main::Arguments arguments)
|
||||
|
||||
auto app = TRY(Ladybird::Application::create(arguments));
|
||||
|
||||
if (auto const& browser_options = Ladybird::Application::browser_options(); !browser_options.headless_mode.has_value()) {
|
||||
// Single-instance is handled via D-Bus in Application::create_platform_event_loop().
|
||||
// If this is a remote instance, it already forwarded URLs and exited.
|
||||
auto& window = app->new_window(browser_options.urls);
|
||||
(void)window;
|
||||
}
|
||||
|
||||
return app->execute();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user