UI/Gtk: Add GLib event loop integration and application entry point
Add the core infrastructure for the GTK frontend: - EventLoopImplementationGtk: GLib main loop integration with Core::EventLoop, timers, notifiers, and signal handling - GObjectPtr RAII wrapper for GObject lifecycle management - Application subclass with AdwApplication and D-Bus registration - Minimal main.cpp entry point The application starts and runs the event loop but does not yet open any windows.
This commit is contained in:
parent
1bf03c21d1
commit
e8be4a88d9
7 changed files with 533 additions and 0 deletions
54
UI/Gtk/Application.cpp
Normal file
54
UI/Gtk/Application.cpp
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* Copyright (c) 2026, Johan Dahlin <jdahlin@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <UI/Gtk/Application.h>
|
||||
#include <UI/Gtk/EventLoopImplementationGtk.h>
|
||||
|
||||
namespace Ladybird {
|
||||
|
||||
Application::Application() = default;
|
||||
Application::~Application()
|
||||
{
|
||||
g_clear_object(&m_adw_application);
|
||||
}
|
||||
|
||||
NonnullOwnPtr<Core::EventLoop> Application::create_platform_event_loop()
|
||||
{
|
||||
if (!browser_options().headless_mode.has_value()) {
|
||||
Core::EventLoopManager::install(*new EventLoopManagerGtk);
|
||||
adw_init();
|
||||
m_adw_application = ADW_APPLICATION(adw_application_new("org.ladybird.Ladybird",
|
||||
static_cast<GApplicationFlags>(G_APPLICATION_DEFAULT_FLAGS | G_APPLICATION_HANDLES_OPEN)));
|
||||
GError* error = nullptr;
|
||||
g_application_register(G_APPLICATION(m_adw_application), nullptr, &error);
|
||||
if (error) {
|
||||
warnln("Failed to register GApplication: {}", error->message);
|
||||
g_error_free(error);
|
||||
}
|
||||
}
|
||||
|
||||
auto event_loop = WebView::Application::create_platform_event_loop();
|
||||
|
||||
if (!browser_options().headless_mode.has_value())
|
||||
static_cast<EventLoopImplementationGtk&>(event_loop->impl()).set_main_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 { }
|
||||
|
||||
}
|
||||
50
UI/Gtk/Application.h
Normal file
50
UI/Gtk/Application.h
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* Copyright (c) 2026, Johan Dahlin <jdahlin@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <LibWebView/Application.h>
|
||||
|
||||
#include <adwaita.h>
|
||||
|
||||
namespace Ladybird {
|
||||
|
||||
class Application : public WebView::Application {
|
||||
WEB_VIEW_APPLICATION(Application)
|
||||
|
||||
public:
|
||||
virtual ~Application() override;
|
||||
|
||||
AdwApplication* adw_application() const { return m_adw_application; }
|
||||
|
||||
private:
|
||||
explicit Application();
|
||||
|
||||
virtual NonnullOwnPtr<Core::EventLoop> create_platform_event_loop() override;
|
||||
|
||||
virtual Optional<WebView::ViewImplementation&> active_web_view() const override;
|
||||
virtual Optional<WebView::ViewImplementation&> open_blank_new_tab(Web::HTML::ActivateTab) const override;
|
||||
|
||||
virtual Optional<ByteString> ask_user_for_download_path(StringView file) const override;
|
||||
virtual void display_download_confirmation_dialog(StringView download_name, LexicalPath const& path) const override;
|
||||
virtual void display_error_dialog(StringView error_message) const override;
|
||||
|
||||
virtual Utf16String clipboard_text() const override;
|
||||
virtual Vector<Web::Clipboard::SystemClipboardRepresentation> clipboard_entries() const override;
|
||||
virtual void insert_clipboard_entry(Web::Clipboard::SystemClipboardRepresentation) override;
|
||||
|
||||
virtual bool should_capture_web_content_output() const override { return false; }
|
||||
|
||||
virtual void rebuild_bookmarks_menu() const override;
|
||||
virtual void update_bookmarks_bar_display(bool) const override;
|
||||
|
||||
virtual void on_devtools_enabled() const override;
|
||||
virtual void on_devtools_disabled() const override;
|
||||
|
||||
AdwApplication* m_adw_application { nullptr };
|
||||
};
|
||||
|
||||
}
|
||||
12
UI/Gtk/CMakeLists.txt
Normal file
12
UI/Gtk/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(GTK4 REQUIRED IMPORTED_TARGET gtk4)
|
||||
pkg_check_modules(LIBADWAITA REQUIRED IMPORTED_TARGET libadwaita-1>=1.4)
|
||||
|
||||
add_executable(ladybird main.cpp)
|
||||
|
||||
target_sources(ladybird PRIVATE
|
||||
Application.cpp
|
||||
EventLoopImplementationGtk.cpp
|
||||
)
|
||||
target_link_libraries(ladybird PRIVATE PkgConfig::GTK4 PkgConfig::LIBADWAITA)
|
||||
create_ladybird_bundle(ladybird)
|
||||
269
UI/Gtk/EventLoopImplementationGtk.cpp
Normal file
269
UI/Gtk/EventLoopImplementationGtk.cpp
Normal file
|
|
@ -0,0 +1,269 @@
|
|||
/*
|
||||
* Copyright (c) 2026, Johan Dahlin <jdahlin@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <AK/HashMap.h>
|
||||
#include <LibCore/Event.h>
|
||||
#include <LibCore/EventReceiver.h>
|
||||
#include <LibCore/Notifier.h>
|
||||
#include <LibCore/System.h>
|
||||
#include <LibCore/ThreadEventQueue.h>
|
||||
#include <LibThreading/Mutex.h>
|
||||
#include <UI/Gtk/EventLoopImplementationGtk.h>
|
||||
|
||||
#include <glib-unix.h>
|
||||
#include <glib.h>
|
||||
|
||||
namespace Ladybird {
|
||||
|
||||
static HashMap<Core::Notifier*, guint> s_notifiers;
|
||||
static Threading::Mutex s_notifiers_mutex;
|
||||
|
||||
// Signal handling for signals not supported by g_unix_signal_add
|
||||
// (which only handles SIGHUP, SIGINT, SIGTERM, SIGUSR1, SIGUSR2, SIGWINCH).
|
||||
// For unsupported signals like SIGCHLD, we use a pipe-based approach.
|
||||
|
||||
static int s_signal_pipe_fds[2] = { -1, -1 };
|
||||
|
||||
static HashMap<int, Function<void(int)>> s_pipe_signal_handlers;
|
||||
|
||||
static void pipe_signal_handler(int signal_number)
|
||||
{
|
||||
[[maybe_unused]] auto _ = ::write(s_signal_pipe_fds[1], &signal_number, sizeof(signal_number));
|
||||
}
|
||||
|
||||
static gboolean pipe_signal_callback(gint fd, [[maybe_unused]] GIOCondition condition, [[maybe_unused]] gpointer data)
|
||||
{
|
||||
int signal_number = {};
|
||||
ssize_t nread;
|
||||
do {
|
||||
errno = 0;
|
||||
nread = read(fd, &signal_number, sizeof(signal_number));
|
||||
if (nread >= 0)
|
||||
break;
|
||||
} while (errno == EINTR);
|
||||
if (nread == sizeof(signal_number)) {
|
||||
auto it = s_pipe_signal_handlers.find(signal_number);
|
||||
if (it != s_pipe_signal_handlers.end())
|
||||
it->value(signal_number);
|
||||
}
|
||||
return G_SOURCE_CONTINUE;
|
||||
}
|
||||
|
||||
static void ensure_signal_pipe()
|
||||
{
|
||||
if (s_signal_pipe_fds[0] != -1)
|
||||
return;
|
||||
auto fds = MUST(Core::System::pipe2(O_CLOEXEC));
|
||||
s_signal_pipe_fds[0] = fds[0];
|
||||
s_signal_pipe_fds[1] = fds[1];
|
||||
g_unix_fd_add(s_signal_pipe_fds[0], G_IO_IN, pipe_signal_callback, nullptr);
|
||||
}
|
||||
|
||||
static bool glib_supports_signal(int signum)
|
||||
{
|
||||
return signum == SIGHUP || signum == SIGINT || signum == SIGTERM
|
||||
|| signum == SIGUSR1 || signum == SIGUSR2 || signum == SIGWINCH;
|
||||
}
|
||||
|
||||
// Timer callback
|
||||
|
||||
struct TimerData {
|
||||
WeakPtr<Core::EventReceiver> weak_object;
|
||||
bool should_reload;
|
||||
};
|
||||
|
||||
static gboolean timer_callback(gpointer user_data)
|
||||
{
|
||||
auto* data = static_cast<TimerData*>(user_data);
|
||||
auto object = data->weak_object.strong_ref();
|
||||
if (!object)
|
||||
return G_SOURCE_REMOVE;
|
||||
Core::TimerEvent event;
|
||||
object->dispatch_event(event);
|
||||
return data->should_reload ? G_SOURCE_CONTINUE : G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
static void timer_destroy(gpointer user_data)
|
||||
{
|
||||
delete static_cast<TimerData*>(user_data);
|
||||
}
|
||||
|
||||
// Notifier callback
|
||||
|
||||
static gboolean notifier_callback([[maybe_unused]] gint fd, [[maybe_unused]] GIOCondition condition, gpointer user_data)
|
||||
{
|
||||
auto notifier = static_cast<WeakPtr<Core::EventReceiver>*>(user_data)->strong_ref();
|
||||
if (!notifier)
|
||||
return G_SOURCE_REMOVE;
|
||||
Core::NotifierActivationEvent event;
|
||||
notifier->dispatch_event(event);
|
||||
return G_SOURCE_CONTINUE;
|
||||
}
|
||||
|
||||
static void notifier_destroy(gpointer user_data)
|
||||
{
|
||||
delete static_cast<WeakPtr<Core::EventReceiver>*>(user_data);
|
||||
}
|
||||
|
||||
// EventLoopManagerGtk
|
||||
|
||||
NonnullOwnPtr<Core::EventLoopImplementation> EventLoopManagerGtk::make_implementation()
|
||||
{
|
||||
return EventLoopImplementationGtk::create();
|
||||
}
|
||||
|
||||
intptr_t EventLoopManagerGtk::register_timer(Core::EventReceiver& object, int milliseconds, bool should_reload)
|
||||
{
|
||||
auto* data = new TimerData { object.make_weak_ptr(), should_reload };
|
||||
auto source_id = g_timeout_add_full(G_PRIORITY_DEFAULT, milliseconds, timer_callback, data, timer_destroy);
|
||||
return static_cast<intptr_t>(source_id);
|
||||
}
|
||||
|
||||
void EventLoopManagerGtk::unregister_timer(intptr_t timer_id)
|
||||
{
|
||||
g_source_remove(static_cast<guint>(timer_id));
|
||||
}
|
||||
|
||||
void EventLoopManagerGtk::register_notifier(Core::Notifier& notifier)
|
||||
{
|
||||
GIOCondition condition {};
|
||||
switch (notifier.type()) {
|
||||
case Core::Notifier::Type::Read:
|
||||
condition = G_IO_IN;
|
||||
break;
|
||||
case Core::Notifier::Type::Write:
|
||||
condition = G_IO_OUT;
|
||||
break;
|
||||
default:
|
||||
VERIFY_NOT_REACHED();
|
||||
}
|
||||
|
||||
auto weak_notifier = new WeakPtr<Core::EventReceiver>(notifier.make_weak_ptr());
|
||||
auto source_id = g_unix_fd_add_full(G_PRIORITY_DEFAULT, notifier.fd(), condition, notifier_callback, weak_notifier, notifier_destroy);
|
||||
|
||||
Threading::MutexLocker locker(s_notifiers_mutex);
|
||||
s_notifiers.set(¬ifier, source_id);
|
||||
}
|
||||
|
||||
void EventLoopManagerGtk::unregister_notifier(Core::Notifier& notifier)
|
||||
{
|
||||
Threading::MutexLocker locker(s_notifiers_mutex);
|
||||
auto it = s_notifiers.find(¬ifier);
|
||||
if (it == s_notifiers.end())
|
||||
return;
|
||||
g_source_remove(it->value);
|
||||
s_notifiers.remove(it);
|
||||
}
|
||||
|
||||
void EventLoopManagerGtk::did_post_event()
|
||||
{
|
||||
if (m_idle_pending)
|
||||
return;
|
||||
m_idle_pending = true;
|
||||
g_idle_add_once(
|
||||
[](gpointer data) {
|
||||
auto& self = *static_cast<EventLoopManagerGtk*>(data);
|
||||
self.m_idle_pending = false;
|
||||
Core::ThreadEventQueue::current().process();
|
||||
},
|
||||
this);
|
||||
}
|
||||
|
||||
int EventLoopManagerGtk::register_signal(int signal_number, Function<void(int)> handler)
|
||||
{
|
||||
VERIFY(signal_number != 0);
|
||||
|
||||
if (glib_supports_signal(signal_number)) {
|
||||
struct Data {
|
||||
int signal_number;
|
||||
Function<void(int)> handler;
|
||||
};
|
||||
auto* data = new Data { signal_number, move(handler) };
|
||||
return static_cast<int>(g_unix_signal_add_full(
|
||||
G_PRIORITY_DEFAULT,
|
||||
signal_number,
|
||||
[](gpointer user_data) -> gboolean {
|
||||
auto* data = static_cast<Data*>(user_data);
|
||||
data->handler(data->signal_number);
|
||||
return G_SOURCE_CONTINUE;
|
||||
},
|
||||
data,
|
||||
[](gpointer user_data) { delete static_cast<Data*>(user_data); }));
|
||||
}
|
||||
|
||||
// For signals GLib doesn't support (e.g. SIGCHLD), use a pipe-based approach.
|
||||
ensure_signal_pipe();
|
||||
s_pipe_signal_handlers.set(signal_number, move(handler));
|
||||
::signal(signal_number, pipe_signal_handler);
|
||||
// Return negative IDs for pipe-based handlers to distinguish from GSource IDs.
|
||||
return -signal_number;
|
||||
}
|
||||
|
||||
void EventLoopManagerGtk::unregister_signal(int handler_id)
|
||||
{
|
||||
VERIFY(handler_id != 0);
|
||||
|
||||
if (handler_id > 0) {
|
||||
g_source_remove(static_cast<guint>(handler_id));
|
||||
return;
|
||||
}
|
||||
|
||||
// Negative handler_id means pipe-based handler; the signal number is -handler_id.
|
||||
int signal_number = -handler_id;
|
||||
::signal(signal_number, SIG_DFL);
|
||||
s_pipe_signal_handlers.remove(signal_number);
|
||||
}
|
||||
|
||||
// EventLoopImplementationGtk
|
||||
|
||||
EventLoopImplementationGtk::EventLoopImplementationGtk()
|
||||
: m_loop(g_main_loop_new(nullptr, FALSE))
|
||||
{
|
||||
}
|
||||
|
||||
EventLoopImplementationGtk::~EventLoopImplementationGtk()
|
||||
{
|
||||
g_clear_pointer(&m_loop, g_main_loop_unref);
|
||||
}
|
||||
|
||||
int EventLoopImplementationGtk::exec()
|
||||
{
|
||||
g_main_loop_run(m_loop);
|
||||
return m_exit_code;
|
||||
}
|
||||
|
||||
size_t EventLoopImplementationGtk::pump(PumpMode mode)
|
||||
{
|
||||
auto result = Core::ThreadEventQueue::current().process();
|
||||
auto may_block = (mode == PumpMode::WaitForEvents) ? TRUE : FALSE;
|
||||
g_main_context_iteration(g_main_loop_get_context(m_loop), may_block);
|
||||
result += Core::ThreadEventQueue::current().process();
|
||||
return result;
|
||||
}
|
||||
|
||||
void EventLoopImplementationGtk::quit(int code)
|
||||
{
|
||||
m_exit_code = code;
|
||||
m_exit_requested = true;
|
||||
if (m_loop && g_main_loop_is_running(m_loop))
|
||||
g_main_loop_quit(m_loop);
|
||||
}
|
||||
|
||||
void EventLoopImplementationGtk::wake()
|
||||
{
|
||||
g_main_context_wakeup(g_main_loop_get_context(m_loop));
|
||||
}
|
||||
|
||||
bool EventLoopImplementationGtk::was_exit_requested() const
|
||||
{
|
||||
return m_exit_requested;
|
||||
}
|
||||
|
||||
void EventLoopImplementationGtk::set_main_loop()
|
||||
{
|
||||
}
|
||||
|
||||
}
|
||||
61
UI/Gtk/EventLoopImplementationGtk.h
Normal file
61
UI/Gtk/EventLoopImplementationGtk.h
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* Copyright (c) 2026, Johan Dahlin <jdahlin@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/NonnullOwnPtr.h>
|
||||
#include <LibCore/EventLoopImplementation.h>
|
||||
|
||||
#include <glib.h>
|
||||
|
||||
namespace Ladybird {
|
||||
|
||||
class EventLoopImplementationGtk;
|
||||
|
||||
class EventLoopManagerGtk final : public Core::EventLoopManager {
|
||||
public:
|
||||
virtual NonnullOwnPtr<Core::EventLoopImplementation> make_implementation() override;
|
||||
|
||||
virtual intptr_t register_timer(Core::EventReceiver&, int milliseconds, bool should_reload) override;
|
||||
virtual void unregister_timer(intptr_t timer_id) override;
|
||||
|
||||
virtual void register_notifier(Core::Notifier&) override;
|
||||
virtual void unregister_notifier(Core::Notifier&) override;
|
||||
|
||||
virtual void did_post_event() override;
|
||||
|
||||
virtual int register_signal(int signal_number, Function<void(int)> handler) override;
|
||||
virtual void unregister_signal(int handler_id) override;
|
||||
|
||||
private:
|
||||
bool m_idle_pending { false };
|
||||
};
|
||||
|
||||
class EventLoopImplementationGtk final : public Core::EventLoopImplementation {
|
||||
public:
|
||||
static NonnullOwnPtr<EventLoopImplementationGtk> create() { return adopt_own(*new EventLoopImplementationGtk); }
|
||||
|
||||
virtual ~EventLoopImplementationGtk() override;
|
||||
|
||||
virtual int exec() override;
|
||||
virtual size_t pump(PumpMode) override;
|
||||
virtual void quit(int) override;
|
||||
virtual void wake() override;
|
||||
virtual bool was_exit_requested() const override;
|
||||
|
||||
void set_main_loop();
|
||||
|
||||
private:
|
||||
friend class EventLoopManagerGtk;
|
||||
|
||||
EventLoopImplementationGtk();
|
||||
|
||||
GMainLoop* m_loop { nullptr };
|
||||
bool m_exit_requested { false };
|
||||
int m_exit_code { 0 };
|
||||
};
|
||||
|
||||
}
|
||||
69
UI/Gtk/GLibPtr.h
Normal file
69
UI/Gtk/GLibPtr.h
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* Copyright (c) 2026, Johan Dahlin <jdahlin@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <glib-object.h>
|
||||
|
||||
namespace Ladybird {
|
||||
|
||||
// RAII wrapper for GObject-derived pointers. Calls g_object_unref on destruction.
|
||||
// Does not add a reference on construction — assumes ownership of a floating or
|
||||
// newly-created reference.
|
||||
template<typename T>
|
||||
class GObjectPtr {
|
||||
public:
|
||||
GObjectPtr() = default;
|
||||
explicit GObjectPtr(T* ptr)
|
||||
: m_ptr(ptr)
|
||||
{
|
||||
}
|
||||
|
||||
~GObjectPtr()
|
||||
{
|
||||
clear();
|
||||
}
|
||||
|
||||
GObjectPtr(GObjectPtr const&) = delete;
|
||||
GObjectPtr& operator=(GObjectPtr const&) = delete;
|
||||
|
||||
GObjectPtr(GObjectPtr&& other)
|
||||
: m_ptr(other.leak())
|
||||
{
|
||||
}
|
||||
|
||||
GObjectPtr& operator=(GObjectPtr&& other)
|
||||
{
|
||||
if (this != &other) {
|
||||
clear();
|
||||
m_ptr = other.leak();
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
T* ptr() const { return m_ptr; }
|
||||
operator T*() const { return m_ptr; }
|
||||
|
||||
T* leak()
|
||||
{
|
||||
auto* ptr = m_ptr;
|
||||
m_ptr = nullptr;
|
||||
return ptr;
|
||||
}
|
||||
|
||||
void clear()
|
||||
{
|
||||
if (m_ptr) {
|
||||
g_object_unref(m_ptr);
|
||||
m_ptr = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
T* m_ptr { nullptr };
|
||||
};
|
||||
|
||||
}
|
||||
18
UI/Gtk/main.cpp
Normal file
18
UI/Gtk/main.cpp
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* Copyright (c) 2026, Johan Dahlin <jdahlin@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <LibMain/Main.h>
|
||||
#include <LibWebView/Application.h>
|
||||
#include <UI/Gtk/Application.h>
|
||||
|
||||
ErrorOr<int> ladybird_main(Main::Arguments arguments)
|
||||
{
|
||||
AK::set_rich_debug_enabled(true);
|
||||
|
||||
auto app = TRY(Ladybird::Application::create(arguments));
|
||||
|
||||
return app->execute();
|
||||
}
|
||||
Loading…
Reference in a new issue