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:
Johan Dahlin 2026-04-08 13:29:54 +02:00 committed by Tim Flynn
parent 1bf03c21d1
commit e8be4a88d9
7 changed files with 533 additions and 0 deletions

54
UI/Gtk/Application.cpp Normal file
View 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
View 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
View 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)

View 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(&notifier, source_id);
}
void EventLoopManagerGtk::unregister_notifier(Core::Notifier& notifier)
{
Threading::MutexLocker locker(s_notifiers_mutex);
auto it = s_notifiers.find(&notifier);
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()
{
}
}

View 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
View 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
View 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();
}