UI/Gtk: Add input events and WebContentView

Add the web content rendering surface and input handling:
- Events: GDK keyboard, mouse, and scroll event
  translation to Web::KeyEvent/MouseEvent
- WebContentView: ViewImplementation backed by
  GdkMemoryTextureBuilder with incremental update hints,
  enqueue_native_event helpers for mouse/key events
- LadybirdWebView: Custom GtkWidget with focus, key,
  click, motion, and scroll controllers
- Builder.h: Typed GtkBuilder object lookup helper
This commit is contained in:
Johan Dahlin 2026-04-08 13:30:32 +02:00 committed by Tim Flynn
parent e8be4a88d9
commit 2573aff090
8 changed files with 1047 additions and 0 deletions

View file

@ -6,7 +6,10 @@ add_executable(ladybird main.cpp)
target_sources(ladybird PRIVATE
Application.cpp
Events.cpp
EventLoopImplementationGtk.cpp
WebContentView.cpp
Widgets/LadybirdWebView.cpp
)
target_link_libraries(ladybird PRIVATE PkgConfig::GTK4 PkgConfig::LIBADWAITA)
create_ladybird_bundle(ladybird)

326
UI/Gtk/Events.cpp Normal file
View file

@ -0,0 +1,326 @@
/*
* Copyright (c) 2026, Johan Dahlin <jdahlin@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <UI/Gtk/Events.h>
namespace Ladybird {
// GDK doesn't define constants for extended mouse buttons
static constexpr guint GDK_BUTTON_BACKWARD = 8;
static constexpr guint GDK_BUTTON_FORWARD = 9;
Web::UIEvents::MouseButton gdk_buttons_to_web(GdkModifierType state)
{
unsigned buttons = Web::UIEvents::MouseButton::None;
if (state & GDK_BUTTON1_MASK)
buttons |= Web::UIEvents::MouseButton::Primary;
if (state & GDK_BUTTON2_MASK)
buttons |= Web::UIEvents::MouseButton::Middle;
if (state & GDK_BUTTON3_MASK)
buttons |= Web::UIEvents::MouseButton::Secondary;
if (state & GDK_BUTTON4_MASK)
buttons |= Web::UIEvents::MouseButton::Backward;
if (state & GDK_BUTTON5_MASK)
buttons |= Web::UIEvents::MouseButton::Forward;
return static_cast<Web::UIEvents::MouseButton>(buttons);
}
Web::UIEvents::MouseButton gdk_button_to_web(guint button)
{
switch (button) {
case GDK_BUTTON_PRIMARY:
return Web::UIEvents::MouseButton::Primary;
case GDK_BUTTON_MIDDLE:
return Web::UIEvents::MouseButton::Middle;
case GDK_BUTTON_SECONDARY:
return Web::UIEvents::MouseButton::Secondary;
case GDK_BUTTON_BACKWARD:
return Web::UIEvents::MouseButton::Backward;
case GDK_BUTTON_FORWARD:
return Web::UIEvents::MouseButton::Forward;
default:
return Web::UIEvents::MouseButton::Primary;
}
}
Web::UIEvents::KeyModifier gdk_modifier_to_web(GdkModifierType state)
{
unsigned modifiers = Web::UIEvents::KeyModifier::Mod_None;
if (state & GDK_SHIFT_MASK)
modifiers |= Web::UIEvents::KeyModifier::Mod_Shift;
if (state & GDK_CONTROL_MASK)
modifiers |= Web::UIEvents::KeyModifier::Mod_Ctrl;
if (state & GDK_ALT_MASK)
modifiers |= Web::UIEvents::KeyModifier::Mod_Alt;
if (state & GDK_SUPER_MASK)
modifiers |= Web::UIEvents::KeyModifier::Mod_Super;
return static_cast<Web::UIEvents::KeyModifier>(modifiers);
}
Web::UIEvents::KeyCode gdk_keyval_to_web(guint keyval)
{
switch (keyval) {
case GDK_KEY_BackSpace:
return Web::UIEvents::KeyCode::Key_Backspace;
case GDK_KEY_Tab:
case GDK_KEY_ISO_Left_Tab:
return Web::UIEvents::KeyCode::Key_Tab;
case GDK_KEY_Return:
case GDK_KEY_KP_Enter:
return Web::UIEvents::KeyCode::Key_Return;
case GDK_KEY_Escape:
return Web::UIEvents::KeyCode::Key_Escape;
case GDK_KEY_space:
return Web::UIEvents::KeyCode::Key_Space;
case GDK_KEY_Page_Up:
return Web::UIEvents::KeyCode::Key_PageUp;
case GDK_KEY_Page_Down:
return Web::UIEvents::KeyCode::Key_PageDown;
case GDK_KEY_End:
return Web::UIEvents::KeyCode::Key_End;
case GDK_KEY_Home:
return Web::UIEvents::KeyCode::Key_Home;
case GDK_KEY_Left:
return Web::UIEvents::KeyCode::Key_Left;
case GDK_KEY_Up:
return Web::UIEvents::KeyCode::Key_Up;
case GDK_KEY_Right:
return Web::UIEvents::KeyCode::Key_Right;
case GDK_KEY_Down:
return Web::UIEvents::KeyCode::Key_Down;
case GDK_KEY_Delete:
return Web::UIEvents::KeyCode::Key_Delete;
case GDK_KEY_Insert:
return Web::UIEvents::KeyCode::Key_Insert;
case GDK_KEY_Shift_L:
return Web::UIEvents::KeyCode::Key_LeftShift;
case GDK_KEY_Shift_R:
return Web::UIEvents::KeyCode::Key_RightShift;
case GDK_KEY_Control_L:
return Web::UIEvents::KeyCode::Key_LeftControl;
case GDK_KEY_Control_R:
return Web::UIEvents::KeyCode::Key_RightControl;
case GDK_KEY_Alt_L:
return Web::UIEvents::KeyCode::Key_LeftAlt;
case GDK_KEY_Alt_R:
return Web::UIEvents::KeyCode::Key_RightAlt;
case GDK_KEY_Super_L:
return Web::UIEvents::KeyCode::Key_LeftSuper;
case GDK_KEY_Super_R:
return Web::UIEvents::KeyCode::Key_RightSuper;
case GDK_KEY_Caps_Lock:
return Web::UIEvents::KeyCode::Key_CapsLock;
case GDK_KEY_Num_Lock:
return Web::UIEvents::KeyCode::Key_NumLock;
case GDK_KEY_Scroll_Lock:
return Web::UIEvents::KeyCode::Key_ScrollLock;
case GDK_KEY_Print:
return Web::UIEvents::KeyCode::Key_PrintScreen;
case GDK_KEY_F1:
return Web::UIEvents::KeyCode::Key_F1;
case GDK_KEY_F2:
return Web::UIEvents::KeyCode::Key_F2;
case GDK_KEY_F3:
return Web::UIEvents::KeyCode::Key_F3;
case GDK_KEY_F4:
return Web::UIEvents::KeyCode::Key_F4;
case GDK_KEY_F5:
return Web::UIEvents::KeyCode::Key_F5;
case GDK_KEY_F6:
return Web::UIEvents::KeyCode::Key_F6;
case GDK_KEY_F7:
return Web::UIEvents::KeyCode::Key_F7;
case GDK_KEY_F8:
return Web::UIEvents::KeyCode::Key_F8;
case GDK_KEY_F9:
return Web::UIEvents::KeyCode::Key_F9;
case GDK_KEY_F10:
return Web::UIEvents::KeyCode::Key_F10;
case GDK_KEY_F11:
return Web::UIEvents::KeyCode::Key_F11;
case GDK_KEY_F12:
return Web::UIEvents::KeyCode::Key_F12;
case GDK_KEY_a:
case GDK_KEY_A:
return Web::UIEvents::KeyCode::Key_A;
case GDK_KEY_b:
case GDK_KEY_B:
return Web::UIEvents::KeyCode::Key_B;
case GDK_KEY_c:
case GDK_KEY_C:
return Web::UIEvents::KeyCode::Key_C;
case GDK_KEY_d:
case GDK_KEY_D:
return Web::UIEvents::KeyCode::Key_D;
case GDK_KEY_e:
case GDK_KEY_E:
return Web::UIEvents::KeyCode::Key_E;
case GDK_KEY_f:
case GDK_KEY_F:
return Web::UIEvents::KeyCode::Key_F;
case GDK_KEY_g:
case GDK_KEY_G:
return Web::UIEvents::KeyCode::Key_G;
case GDK_KEY_h:
case GDK_KEY_H:
return Web::UIEvents::KeyCode::Key_H;
case GDK_KEY_i:
case GDK_KEY_I:
return Web::UIEvents::KeyCode::Key_I;
case GDK_KEY_j:
case GDK_KEY_J:
return Web::UIEvents::KeyCode::Key_J;
case GDK_KEY_k:
case GDK_KEY_K:
return Web::UIEvents::KeyCode::Key_K;
case GDK_KEY_l:
case GDK_KEY_L:
return Web::UIEvents::KeyCode::Key_L;
case GDK_KEY_m:
case GDK_KEY_M:
return Web::UIEvents::KeyCode::Key_M;
case GDK_KEY_n:
case GDK_KEY_N:
return Web::UIEvents::KeyCode::Key_N;
case GDK_KEY_o:
case GDK_KEY_O:
return Web::UIEvents::KeyCode::Key_O;
case GDK_KEY_p:
case GDK_KEY_P:
return Web::UIEvents::KeyCode::Key_P;
case GDK_KEY_q:
case GDK_KEY_Q:
return Web::UIEvents::KeyCode::Key_Q;
case GDK_KEY_r:
case GDK_KEY_R:
return Web::UIEvents::KeyCode::Key_R;
case GDK_KEY_s:
case GDK_KEY_S:
return Web::UIEvents::KeyCode::Key_S;
case GDK_KEY_t:
case GDK_KEY_T:
return Web::UIEvents::KeyCode::Key_T;
case GDK_KEY_u:
case GDK_KEY_U:
return Web::UIEvents::KeyCode::Key_U;
case GDK_KEY_v:
case GDK_KEY_V:
return Web::UIEvents::KeyCode::Key_V;
case GDK_KEY_w:
case GDK_KEY_W:
return Web::UIEvents::KeyCode::Key_W;
case GDK_KEY_x:
case GDK_KEY_X:
return Web::UIEvents::KeyCode::Key_X;
case GDK_KEY_y:
case GDK_KEY_Y:
return Web::UIEvents::KeyCode::Key_Y;
case GDK_KEY_z:
case GDK_KEY_Z:
return Web::UIEvents::KeyCode::Key_Z;
case GDK_KEY_0:
case GDK_KEY_parenright:
return Web::UIEvents::KeyCode::Key_0;
case GDK_KEY_1:
case GDK_KEY_exclam:
return Web::UIEvents::KeyCode::Key_1;
case GDK_KEY_2:
case GDK_KEY_at:
return Web::UIEvents::KeyCode::Key_2;
case GDK_KEY_3:
case GDK_KEY_numbersign:
return Web::UIEvents::KeyCode::Key_3;
case GDK_KEY_4:
case GDK_KEY_dollar:
return Web::UIEvents::KeyCode::Key_4;
case GDK_KEY_5:
case GDK_KEY_percent:
return Web::UIEvents::KeyCode::Key_5;
case GDK_KEY_6:
case GDK_KEY_asciicircum:
return Web::UIEvents::KeyCode::Key_6;
case GDK_KEY_7:
case GDK_KEY_ampersand:
return Web::UIEvents::KeyCode::Key_7;
case GDK_KEY_8:
case GDK_KEY_asterisk:
return Web::UIEvents::KeyCode::Key_8;
case GDK_KEY_9:
case GDK_KEY_parenleft:
return Web::UIEvents::KeyCode::Key_9;
case GDK_KEY_minus:
case GDK_KEY_underscore:
return Web::UIEvents::KeyCode::Key_Minus;
case GDK_KEY_equal:
case GDK_KEY_plus:
return Web::UIEvents::KeyCode::Key_Equal;
case GDK_KEY_bracketleft:
case GDK_KEY_braceleft:
return Web::UIEvents::KeyCode::Key_LeftBracket;
case GDK_KEY_bracketright:
case GDK_KEY_braceright:
return Web::UIEvents::KeyCode::Key_RightBracket;
case GDK_KEY_backslash:
case GDK_KEY_bar:
return Web::UIEvents::KeyCode::Key_Backslash;
case GDK_KEY_semicolon:
case GDK_KEY_colon:
return Web::UIEvents::KeyCode::Key_Semicolon;
case GDK_KEY_apostrophe:
case GDK_KEY_quotedbl:
return Web::UIEvents::KeyCode::Key_Apostrophe;
case GDK_KEY_grave:
case GDK_KEY_asciitilde:
return Web::UIEvents::KeyCode::Key_Backtick;
case GDK_KEY_comma:
case GDK_KEY_less:
return Web::UIEvents::KeyCode::Key_Comma;
case GDK_KEY_period:
case GDK_KEY_greater:
return Web::UIEvents::KeyCode::Key_Period;
case GDK_KEY_slash:
case GDK_KEY_question:
return Web::UIEvents::KeyCode::Key_Slash;
default:
return Web::UIEvents::KeyCode::Key_Invalid;
}
}
StringView standard_cursor_to_css_name(Gfx::StandardCursor cursor)
{
switch (cursor) {
case Gfx::StandardCursor::Hidden:
return "none"sv;
case Gfx::StandardCursor::Arrow:
return "default"sv;
case Gfx::StandardCursor::IBeam:
return "text"sv;
case Gfx::StandardCursor::Crosshair:
return "crosshair"sv;
case Gfx::StandardCursor::Hand:
return "pointer"sv;
case Gfx::StandardCursor::ResizeHorizontal:
return "ew-resize"sv;
case Gfx::StandardCursor::ResizeVertical:
return "ns-resize"sv;
case Gfx::StandardCursor::ResizeDiagonalTLBR:
return "nwse-resize"sv;
case Gfx::StandardCursor::ResizeDiagonalBLTR:
return "nesw-resize"sv;
case Gfx::StandardCursor::ResizeColumn:
return "col-resize"sv;
case Gfx::StandardCursor::ResizeRow:
return "row-resize"sv;
case Gfx::StandardCursor::Move:
return "move"sv;
case Gfx::StandardCursor::Wait:
return "wait"sv;
default:
return "default"sv;
}
}
}

23
UI/Gtk/Events.h Normal file
View file

@ -0,0 +1,23 @@
/*
* Copyright (c) 2026, Johan Dahlin <jdahlin@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibGfx/Cursor.h>
#include <LibWeb/UIEvents/KeyCode.h>
#include <LibWeb/UIEvents/MouseButton.h>
#include <gdk/gdk.h>
namespace Ladybird {
Web::UIEvents::MouseButton gdk_buttons_to_web(GdkModifierType state);
Web::UIEvents::MouseButton gdk_button_to_web(guint button);
Web::UIEvents::KeyModifier gdk_modifier_to_web(GdkModifierType state);
Web::UIEvents::KeyCode gdk_keyval_to_web(guint keyval);
StringView standard_cursor_to_css_name(Gfx::StandardCursor cursor);
}

318
UI/Gtk/WebContentView.cpp Normal file
View file

@ -0,0 +1,318 @@
/*
* Copyright (c) 2026, Johan Dahlin <jdahlin@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibCore/Resource.h>
#include <LibGfx/Bitmap.h>
#include <LibGfx/Palette.h>
#include <LibGfx/SystemTheme.h>
#include <LibWebView/Application.h>
#include <LibWebView/Menu.h>
#include <LibWebView/WebContentClient.h>
#include <UI/Gtk/Events.h>
#include <UI/Gtk/GLibPtr.h>
#include <UI/Gtk/WebContentView.h>
#include <adwaita.h>
#include <gdk/gdk.h>
namespace Ladybird {
WebContentView::WebContentView(LadybirdWebView* widget, RefPtr<WebView::WebContentClient> parent_client, size_t page_index)
: m_widget(widget)
{
m_client_state.client = parent_client;
m_client_state.page_index = page_index;
m_device_pixel_ratio = gtk_widget_get_scale_factor(GTK_WIDGET(widget));
// Store ourselves in the GObject widget
ladybird_web_view_set_impl(widget, this);
initialize_client((parent_client == nullptr) ? CreateNewClient::Yes : CreateNewClient::No);
on_ready_to_paint = [this]() {
if (m_widget)
gtk_widget_queue_draw(GTK_WIDGET(m_widget));
};
on_finish_handling_key_event = [this](Web::KeyEvent const& event) {
finish_handling_key_event(event);
};
// Re-send palette when system theme changes (light <-> dark)
g_signal_connect_swapped(adw_style_manager_get_default(), "notify::dark", G_CALLBACK(+[](WebContentView* self, GParamSpec*) {
self->update_palette();
if (self->m_widget)
gtk_widget_queue_draw(GTK_WIDGET(self->m_widget));
}),
this);
}
WebContentView::~WebContentView()
{
g_signal_handlers_disconnect_by_data(adw_style_manager_get_default(), this);
if (m_widget)
ladybird_web_view_set_impl(m_widget, nullptr);
}
void WebContentView::enqueue_native_event(Web::MouseEvent::Type type, double x, double y, unsigned button, GdkModifierType state, int click_count)
{
auto device_pixel_ratio = m_device_pixel_ratio;
auto position = Web::DevicePixelPoint { static_cast<int>(x * device_pixel_ratio), static_cast<int>(y * device_pixel_ratio) };
auto web_button = gdk_button_to_web(button);
auto modifiers = gdk_modifier_to_web(state);
Web::UIEvents::MouseButton buttons;
switch (type) {
case Web::MouseEvent::Type::MouseDown:
buttons = web_button;
break;
case Web::MouseEvent::Type::MouseMove:
buttons = gdk_buttons_to_web(state);
break;
default:
buttons = Web::UIEvents::MouseButton::None;
break;
}
Web::MouseEvent event {
.type = type,
.position = position,
.screen_position = position,
.button = web_button,
.buttons = buttons,
.modifiers = modifiers,
.wheel_delta_x = 0,
.wheel_delta_y = 0,
.click_count = click_count,
.browser_data = {},
};
enqueue_input_event(move(event));
}
void WebContentView::enqueue_native_event(Web::KeyEvent::Type type, guint keyval, GdkModifierType state)
{
Web::KeyEvent event {
.type = type,
.key = gdk_keyval_to_web(keyval),
.modifiers = gdk_modifier_to_web(state),
.code_point = gdk_keyval_to_unicode(keyval),
.repeat = false,
.browser_data = {},
};
enqueue_input_event(move(event));
}
void WebContentView::finish_handling_key_event(Web::KeyEvent const& event)
{
// FIXME: Re-dispatch the original event through GTK's event system so that
// unhandled keys propagate to accelerators/shortcuts generically,
// matching the Qt UI's approach of re-sending the original QKeyEvent.
if (event.type != Web::KeyEvent::Type::KeyDown)
return;
if (!(event.modifiers & Web::UIEvents::KeyModifier::Mod_Ctrl))
return;
auto& app = WebView::Application::the();
switch (event.key) {
case Web::UIEvents::Key_C:
app.copy_selection_action().activate();
break;
case Web::UIEvents::Key_V:
app.paste_action().activate();
break;
case Web::UIEvents::Key_A:
app.select_all_action().activate();
break;
default:
break;
}
}
void WebContentView::paint(GtkSnapshot* snapshot)
{
auto width = gtk_widget_get_width(GTK_WIDGET(m_widget));
auto height = gtk_widget_get_height(GTK_WIDGET(m_widget));
if (width == 0 || height == 0)
return;
Gfx::Bitmap const* bitmap = nullptr;
Gfx::IntSize bitmap_size;
if (m_client_state.has_usable_bitmap) {
VERIFY(m_client_state.front_bitmap.shared_image_buffer);
bitmap = m_client_state.front_bitmap.shared_image_buffer->bitmap().ptr();
bitmap_size = m_client_state.front_bitmap.last_painted_size.to_type<int>();
} else if (m_backup_shared_image_buffer) {
bitmap = m_backup_shared_image_buffer->bitmap().ptr();
bitmap_size = m_backup_bitmap_size.to_type<int>();
}
if (bitmap) {
auto painted_width = bitmap_size.width();
auto painted_height = bitmap_size.height();
if (painted_width == 0 || painted_height == 0)
painted_width = bitmap->width(), painted_height = bitmap->height();
// Rebuild the GdkTexture when the bitmap data or size changes.
// Use GdkMemoryTextureBuilder so we can set update-texture to hint GSK
// that this is an incremental update of the previous frame, allowing it
// to reuse GPU resources and only re-upload changed regions.
if (bitmap != m_cached_bitmap || bitmap_size != m_cached_painted_size || !m_cached_texture.ptr()) {
auto* bytes = g_bytes_new_static(bitmap->scanline_u8(0), bitmap->pitch() * painted_height);
GObjectPtr builder { gdk_memory_texture_builder_new() };
gdk_memory_texture_builder_set_bytes(GDK_MEMORY_TEXTURE_BUILDER(builder.ptr()), bytes);
gdk_memory_texture_builder_set_stride(GDK_MEMORY_TEXTURE_BUILDER(builder.ptr()), bitmap->pitch());
gdk_memory_texture_builder_set_width(GDK_MEMORY_TEXTURE_BUILDER(builder.ptr()), painted_width);
gdk_memory_texture_builder_set_height(GDK_MEMORY_TEXTURE_BUILDER(builder.ptr()), painted_height);
gdk_memory_texture_builder_set_format(GDK_MEMORY_TEXTURE_BUILDER(builder.ptr()), GDK_MEMORY_B8G8R8A8_PREMULTIPLIED);
if (m_cached_texture.ptr()) {
gdk_memory_texture_builder_set_update_texture(GDK_MEMORY_TEXTURE_BUILDER(builder.ptr()), m_cached_texture);
cairo_rectangle_int_t full_rect = { 0, 0, painted_width, painted_height };
auto* update_region = cairo_region_create_rectangle(&full_rect);
gdk_memory_texture_builder_set_update_region(GDK_MEMORY_TEXTURE_BUILDER(builder.ptr()), update_region);
cairo_region_destroy(update_region);
}
m_cached_texture = GObjectPtr<GdkTexture> { gdk_memory_texture_builder_build(GDK_MEMORY_TEXTURE_BUILDER(builder.ptr())) };
g_bytes_unref(bytes);
m_cached_bitmap = bitmap;
m_cached_painted_size = bitmap_size;
}
auto device_pixel_ratio = m_device_pixel_ratio;
auto draw_width = static_cast<float>(painted_width / device_pixel_ratio);
auto draw_height = static_cast<float>(painted_height / device_pixel_ratio);
graphene_rect_t texture_rect = GRAPHENE_RECT_INIT(0, 0, draw_width, draw_height);
gtk_snapshot_append_texture(snapshot, m_cached_texture, &texture_rect);
// Fill uncovered areas with theme-appropriate background
auto is_dark = adw_style_manager_get_dark(adw_style_manager_get_default());
GdkRGBA bg = is_dark ? GdkRGBA { 0.14, 0.14, 0.14, 1.0 } : GdkRGBA { 1.0, 1.0, 1.0, 1.0 };
if (draw_width < width) {
graphene_rect_t right_rect = GRAPHENE_RECT_INIT(draw_width, 0, static_cast<float>(width) - draw_width, static_cast<float>(height));
gtk_snapshot_append_color(snapshot, &bg, &right_rect);
}
if (draw_height < height) {
graphene_rect_t bottom_rect = GRAPHENE_RECT_INIT(0, draw_height, static_cast<float>(width), static_cast<float>(height) - draw_height);
gtk_snapshot_append_color(snapshot, &bg, &bottom_rect);
}
} else {
auto is_dark = adw_style_manager_get_dark(adw_style_manager_get_default());
GdkRGBA bg = is_dark ? GdkRGBA { 0.14, 0.14, 0.14, 1.0 } : GdkRGBA { 1.0, 1.0, 1.0, 1.0 };
graphene_rect_t full_rect = GRAPHENE_RECT_INIT(0, 0, static_cast<float>(width), static_cast<float>(height));
gtk_snapshot_append_color(snapshot, &bg, &full_rect);
}
}
void WebContentView::update_viewport_size()
{
if (!m_widget)
return;
auto width = gtk_widget_get_width(GTK_WIDGET(m_widget));
auto height = gtk_widget_get_height(GTK_WIDGET(m_widget));
update_viewport_size(width, height);
}
void WebContentView::update_viewport_size(int width, int height)
{
auto device_pixel_ratio = m_device_pixel_ratio;
m_viewport_size = { static_cast<int>(width * device_pixel_ratio), static_cast<int>(height * device_pixel_ratio) };
handle_resize();
}
void WebContentView::set_device_pixel_ratio(double device_pixel_ratio)
{
m_device_pixel_ratio = device_pixel_ratio;
update_viewport_size();
}
Web::DevicePixelSize WebContentView::viewport_size() const
{
return { m_viewport_size.width(), m_viewport_size.height() };
}
Gfx::IntPoint WebContentView::to_content_position(Gfx::IntPoint widget_position) const
{
return widget_position;
}
Gfx::IntPoint WebContentView::to_widget_position(Gfx::IntPoint content_position) const
{
return content_position;
}
void WebContentView::initialize_client(CreateNewClient create_new_client)
{
ViewImplementation::initialize_client(create_new_client);
update_palette();
update_screen_rects();
}
void WebContentView::update_zoom()
{
ViewImplementation::update_zoom();
gtk_widget_queue_draw(GTK_WIDGET(m_widget));
if (on_zoom_level_changed)
on_zoom_level_changed();
}
void WebContentView::set_has_focus(bool has_focus)
{
client().async_set_has_focus(page_id(), has_focus);
}
void WebContentView::update_palette()
{
auto is_dark = adw_style_manager_get_dark(adw_style_manager_get_default());
auto theme_file = is_dark ? "Dark"sv : "Default"sv;
auto theme_ini = MUST(Core::Resource::load_from_uri(MUST(String::formatted("resource://themes/{}.ini", theme_file))));
auto theme_or_error = Gfx::load_system_theme(theme_ini->filesystem_path().to_byte_string());
if (theme_or_error.is_error())
return;
auto theme = theme_or_error.release_value();
set_preferred_color_scheme(is_dark ? Web::CSS::PreferredColorScheme::Dark : Web::CSS::PreferredColorScheme::Light);
client().async_update_system_theme(page_id(), move(theme));
}
void WebContentView::update_screen_rects()
{
auto* display = gdk_display_get_default();
if (!display)
return;
auto* monitors = gdk_display_get_monitors(display);
auto n_monitors = g_list_model_get_n_items(monitors);
Vector<Web::DevicePixelRect> screen_rects;
size_t main_screen_index = 0;
for (guint i = 0; i < n_monitors; i++) {
GObjectPtr monitor { g_list_model_get_item(monitors, i) };
GdkRectangle geometry;
gdk_monitor_get_geometry(GDK_MONITOR(monitor.ptr()), &geometry);
auto scale = gdk_monitor_get_scale_factor(GDK_MONITOR(monitor.ptr()));
screen_rects.append(Web::DevicePixelRect {
geometry.x * scale,
geometry.y * scale,
geometry.width * scale,
geometry.height * scale });
}
if (screen_rects.is_empty())
screen_rects.append(Web::DevicePixelRect { 0, 0, 1920, 1080 });
client().async_update_screen_rects(page_id(), move(screen_rects), main_screen_index);
}
}

62
UI/Gtk/WebContentView.h Normal file
View file

@ -0,0 +1,62 @@
/*
* Copyright (c) 2026, Johan Dahlin <jdahlin@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/OwnPtr.h>
#include <LibGfx/Forward.h>
#include <LibWeb/Forward.h>
#include <LibWebView/ViewImplementation.h>
#include <UI/Gtk/GLibPtr.h>
#include <UI/Gtk/Widgets/LadybirdWebView.h>
namespace Ladybird {
class WebContentView final : public WebView::ViewImplementation {
public:
WebContentView(LadybirdWebView* widget, RefPtr<WebView::WebContentClient> parent_client = nullptr, size_t page_index = 0);
virtual ~WebContentView() override;
LadybirdWebView* gtk_widget() const { return m_widget; }
void update_viewport_size();
void update_viewport_size(int width, int height);
void set_device_pixel_ratio(double device_pixel_ratio);
void set_widget(LadybirdWebView* widget) { m_widget = widget; }
void paint(GtkSnapshot* snapshot);
void update_palette();
void update_screen_rects();
WebView::WebContentClient& client() { return ViewImplementation::client(); }
void set_has_focus(bool has_focus);
Gfx::IntPoint to_content_position(Gfx::IntPoint widget_position) const override;
Gfx::IntPoint to_widget_position(Gfx::IntPoint content_position) const override;
void enqueue_native_event(Web::MouseEvent::Type, double x, double y, unsigned button, GdkModifierType state, int click_count);
void enqueue_native_event(Web::KeyEvent::Type, guint keyval, GdkModifierType state);
Function<void()> on_zoom_level_changed;
private:
Web::DevicePixelSize viewport_size() const override;
void initialize_client(CreateNewClient = CreateNewClient::Yes) override;
void update_zoom() override;
void finish_handling_key_event(Web::KeyEvent const&);
LadybirdWebView* m_widget { nullptr };
Gfx::IntSize m_viewport_size;
// Cached texture to avoid recreation on every snapshot
GObjectPtr<GdkTexture> m_cached_texture;
Gfx::Bitmap const* m_cached_bitmap { nullptr };
Gfx::IntSize m_cached_painted_size;
};
}

22
UI/Gtk/Widgets/Builder.h Normal file
View file

@ -0,0 +1,22 @@
/*
* Copyright (c) 2026, Johan Dahlin <jdahlin@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Assertions.h>
#include <gtk/gtk.h>
namespace LadybirdWidgets {
template<typename T>
T* get_builder_object(GtkBuilder* builder, char const* name)
{
auto* object = gtk_builder_get_object(builder, name);
VERIFY(object);
return reinterpret_cast<T*>(object);
}
}

View file

@ -0,0 +1,271 @@
/*
* Copyright (c) 2026, Johan Dahlin <jdahlin@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <UI/Gtk/Events.h>
#include <UI/Gtk/WebContentView.h>
#include <adwaita.h>
#include <gdk/gdk.h>
#define LADYBIRD_WEB_VIEW(obj) (reinterpret_cast<LadybirdWebView*>(obj))
#define LADYBIRD_TYPE_WEB_VIEW (ladybird_web_view_get_type())
struct LadybirdWebView {
GtkWidget parent_instance;
Ladybird::WebContentView* impl { nullptr };
};
struct LadybirdWebViewClass {
GtkWidgetClass parent_class;
};
G_DEFINE_FINAL_TYPE(LadybirdWebView, ladybird_web_view, GTK_TYPE_WIDGET)
// GObject vfunc implementations
static void ladybird_web_view_finalize(GObject* object)
{
auto* self = LADYBIRD_WEB_VIEW(object);
// Don't delete impl - it's owned by Tab's OwnPtr<WebContentView>.
// Just tell the C++ side the widget is gone.
if (self->impl)
self->impl->set_widget(nullptr);
self->impl = nullptr;
G_OBJECT_CLASS(ladybird_web_view_parent_class)->finalize(object);
}
static void ladybird_web_view_snapshot(GtkWidget* widget, GtkSnapshot* snapshot)
{
auto* self = LADYBIRD_WEB_VIEW(widget);
if (self->impl)
self->impl->paint(snapshot);
}
static void ladybird_web_view_measure(GtkWidget*, GtkOrientation orientation, int, int* minimum, int* natural, int* minimum_baseline, int* natural_baseline)
{
if (orientation == GTK_ORIENTATION_HORIZONTAL) {
*minimum = 100;
*natural = 800;
} else {
*minimum = 100;
*natural = 600;
}
*minimum_baseline = -1;
*natural_baseline = -1;
}
static void ladybird_web_view_size_allocate(GtkWidget* widget, int width, int height, int)
{
auto* self = LADYBIRD_WEB_VIEW(widget);
if (self->impl)
self->impl->update_viewport_size(width, height);
}
static void ladybird_web_view_class_init(LadybirdWebViewClass* klass)
{
auto* widget_class = GTK_WIDGET_CLASS(klass);
widget_class->snapshot = ladybird_web_view_snapshot;
widget_class->measure = ladybird_web_view_measure;
widget_class->size_allocate = ladybird_web_view_size_allocate;
auto* object_class = G_OBJECT_CLASS(klass);
object_class->finalize = ladybird_web_view_finalize;
gtk_widget_class_set_css_name(widget_class, "ladybird-web-view");
}
// Input event callbacks
static gboolean on_key_pressed(GtkEventControllerKey*, guint keyval, guint, GdkModifierType state, gpointer user_data)
{
auto* self = LADYBIRD_WEB_VIEW(user_data);
if (!self->impl)
return GDK_EVENT_PROPAGATE;
self->impl->enqueue_native_event(Web::KeyEvent::Type::KeyDown, keyval, state);
return GDK_EVENT_STOP;
}
static void on_key_released(GtkEventControllerKey*, guint keyval, guint, GdkModifierType state, gpointer user_data)
{
auto* self = LADYBIRD_WEB_VIEW(user_data);
if (!self->impl)
return;
self->impl->enqueue_native_event(Web::KeyEvent::Type::KeyUp, keyval, state);
}
static void on_mouse_pressed(GtkGestureClick* gesture, gint n_press, gdouble x, gdouble y, gpointer user_data)
{
auto* self = LADYBIRD_WEB_VIEW(user_data);
if (!self->impl)
return;
gtk_widget_grab_focus(GTK_WIDGET(self));
auto button = gtk_gesture_single_get_current_button(GTK_GESTURE_SINGLE(gesture));
auto state = gtk_event_controller_get_current_event_state(GTK_EVENT_CONTROLLER(gesture));
self->impl->enqueue_native_event(Web::MouseEvent::Type::MouseDown, x, y, button, state, n_press);
}
static void on_mouse_released(GtkGestureClick* gesture, gint n_press, gdouble x, gdouble y, gpointer user_data)
{
auto* self = LADYBIRD_WEB_VIEW(user_data);
if (!self->impl)
return;
auto button = gtk_gesture_single_get_current_button(GTK_GESTURE_SINGLE(gesture));
auto state = gtk_event_controller_get_current_event_state(GTK_EVENT_CONTROLLER(gesture));
self->impl->enqueue_native_event(Web::MouseEvent::Type::MouseUp, x, y, button, state, n_press);
}
static void on_mouse_motion(GtkEventControllerMotion* controller, gdouble x, gdouble y, gpointer user_data)
{
auto* self = LADYBIRD_WEB_VIEW(user_data);
if (!self->impl)
return;
auto state = gtk_event_controller_get_current_event_state(GTK_EVENT_CONTROLLER(controller));
self->impl->enqueue_native_event(Web::MouseEvent::Type::MouseMove, x, y, 0, state, 0);
}
static void on_mouse_leave(GtkEventControllerMotion*, gpointer user_data)
{
auto* self = LADYBIRD_WEB_VIEW(user_data);
if (!self->impl)
return;
self->impl->enqueue_native_event(Web::MouseEvent::Type::MouseLeave, 0, 0, 0, static_cast<GdkModifierType>(0), 0);
}
static gboolean on_scroll(GtkEventControllerScroll* controller, gdouble dx, gdouble dy, gpointer user_data)
{
auto* self = LADYBIRD_WEB_VIEW(user_data);
if (!self->impl)
return GDK_EVENT_PROPAGATE;
auto state = gtk_event_controller_get_current_event_state(GTK_EVENT_CONTROLLER(controller));
// Ctrl+scroll = zoom
if (state & GDK_CONTROL_MASK) {
if (dy < 0)
self->impl->zoom_in();
else if (dy > 0)
self->impl->zoom_out();
return GDK_EVENT_STOP;
}
auto device_pixel_ratio = self->impl->device_pixel_ratio();
int wheel_delta_x = 0;
int wheel_delta_y = 0;
auto* gdk_event = gtk_event_controller_get_current_event(GTK_EVENT_CONTROLLER(controller));
auto unit = gdk_scroll_event_get_unit(gdk_event);
if (unit == GDK_SCROLL_UNIT_SURFACE) {
wheel_delta_x = static_cast<int>(dx * device_pixel_ratio);
wheel_delta_y = static_cast<int>(dy * device_pixel_ratio);
} else {
static constexpr double scroll_lines = 3.0;
static constexpr double scroll_step_size = 24.0;
wheel_delta_x = static_cast<int>(dx * scroll_lines * scroll_step_size * device_pixel_ratio);
wheel_delta_y = static_cast<int>(dy * scroll_lines * scroll_step_size * device_pixel_ratio);
}
Web::MouseEvent event {
.type = Web::MouseEvent::Type::MouseWheel,
.position = {},
.screen_position = {},
.button = Web::UIEvents::MouseButton::None,
.buttons = Web::UIEvents::MouseButton::None,
.modifiers = Ladybird::gdk_modifier_to_web(state),
.wheel_delta_x = wheel_delta_x,
.wheel_delta_y = wheel_delta_y,
.click_count = 0,
.browser_data = {},
};
self->impl->enqueue_input_event(move(event));
return GDK_EVENT_STOP;
}
static void on_map(GtkWidget* widget, gpointer)
{
auto* self = LADYBIRD_WEB_VIEW(widget);
if (!self->impl)
return;
self->impl->set_system_visibility_state(Web::HTML::VisibilityState::Visible);
self->impl->update_viewport_size();
gtk_widget_queue_draw(widget);
}
static void on_unmap(GtkWidget* widget, gpointer)
{
auto* self = LADYBIRD_WEB_VIEW(widget);
if (!self->impl)
return;
self->impl->set_system_visibility_state(Web::HTML::VisibilityState::Hidden);
}
static void ladybird_web_view_init(LadybirdWebView* self)
{
self->impl = nullptr;
gtk_widget_set_focusable(GTK_WIDGET(self), TRUE);
g_signal_connect(GTK_WIDGET(self), "map", G_CALLBACK(on_map), nullptr);
g_signal_connect(GTK_WIDGET(self), "unmap", G_CALLBACK(on_unmap), nullptr);
auto* focus_controller = gtk_event_controller_focus_new();
g_signal_connect_swapped(focus_controller, "enter", G_CALLBACK(+[](LadybirdWebView* self, GtkEventControllerFocus*) {
if (self->impl)
self->impl->set_has_focus(true);
}),
self);
g_signal_connect_swapped(focus_controller, "leave", G_CALLBACK(+[](LadybirdWebView* self, GtkEventControllerFocus*) {
if (self->impl)
self->impl->set_has_focus(false);
}),
self);
gtk_widget_add_controller(GTK_WIDGET(self), GTK_EVENT_CONTROLLER(focus_controller));
auto* key_controller = gtk_event_controller_key_new();
g_signal_connect(key_controller, "key-pressed", G_CALLBACK(on_key_pressed), self);
g_signal_connect(key_controller, "key-released", G_CALLBACK(on_key_released), self);
gtk_widget_add_controller(GTK_WIDGET(self), key_controller);
auto* click_gesture = gtk_gesture_click_new();
gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(click_gesture), 0);
g_signal_connect(click_gesture, "pressed", G_CALLBACK(on_mouse_pressed), self);
g_signal_connect(click_gesture, "released", G_CALLBACK(on_mouse_released), self);
gtk_widget_add_controller(GTK_WIDGET(self), GTK_EVENT_CONTROLLER(click_gesture));
auto* motion_controller = gtk_event_controller_motion_new();
g_signal_connect(motion_controller, "motion", G_CALLBACK(on_mouse_motion), self);
g_signal_connect(motion_controller, "leave", G_CALLBACK(on_mouse_leave), self);
gtk_widget_add_controller(GTK_WIDGET(self), motion_controller);
auto* scroll_controller = gtk_event_controller_scroll_new(GTK_EVENT_CONTROLLER_SCROLL_BOTH_AXES);
g_signal_connect(scroll_controller, "scroll", G_CALLBACK(on_scroll), self);
gtk_widget_add_controller(GTK_WIDGET(self), scroll_controller);
}
// Public API
LadybirdWebView* ladybird_web_view_new()
{
return LADYBIRD_WEB_VIEW(g_object_new(LADYBIRD_TYPE_WEB_VIEW, nullptr));
}
Ladybird::WebContentView* ladybird_web_view_get_impl(LadybirdWebView* self)
{
return self->impl;
}
void ladybird_web_view_set_impl(LadybirdWebView* self, Ladybird::WebContentView* impl)
{
self->impl = impl;
}

View file

@ -0,0 +1,22 @@
/*
* Copyright (c) 2026, Johan Dahlin <jdahlin@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <gtk/gtk.h>
namespace Ladybird {
class WebContentView;
}
struct LadybirdWebView;
GType ladybird_web_view_get_type(void);
LadybirdWebView* ladybird_web_view_new(void);
Ladybird::WebContentView* ladybird_web_view_get_impl(LadybirdWebView* self);
void ladybird_web_view_set_impl(LadybirdWebView* self, Ladybird::WebContentView* impl);