From 2573aff0903849569575a249f95ab6fe879076f0 Mon Sep 17 00:00:00 2001 From: Johan Dahlin Date: Wed, 8 Apr 2026 13:30:32 +0200 Subject: [PATCH] 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 --- UI/Gtk/CMakeLists.txt | 3 + UI/Gtk/Events.cpp | 326 +++++++++++++++++++++++++++++ UI/Gtk/Events.h | 23 ++ UI/Gtk/WebContentView.cpp | 318 ++++++++++++++++++++++++++++ UI/Gtk/WebContentView.h | 62 ++++++ UI/Gtk/Widgets/Builder.h | 22 ++ UI/Gtk/Widgets/LadybirdWebView.cpp | 271 ++++++++++++++++++++++++ UI/Gtk/Widgets/LadybirdWebView.h | 22 ++ 8 files changed, 1047 insertions(+) create mode 100644 UI/Gtk/Events.cpp create mode 100644 UI/Gtk/Events.h create mode 100644 UI/Gtk/WebContentView.cpp create mode 100644 UI/Gtk/WebContentView.h create mode 100644 UI/Gtk/Widgets/Builder.h create mode 100644 UI/Gtk/Widgets/LadybirdWebView.cpp create mode 100644 UI/Gtk/Widgets/LadybirdWebView.h diff --git a/UI/Gtk/CMakeLists.txt b/UI/Gtk/CMakeLists.txt index c5b369ee09..8d01752f39 100644 --- a/UI/Gtk/CMakeLists.txt +++ b/UI/Gtk/CMakeLists.txt @@ -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) diff --git a/UI/Gtk/Events.cpp b/UI/Gtk/Events.cpp new file mode 100644 index 0000000000..64d5116b6d --- /dev/null +++ b/UI/Gtk/Events.cpp @@ -0,0 +1,326 @@ +/* + * Copyright (c) 2026, Johan Dahlin + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include + +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(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(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; + } +} + +} diff --git a/UI/Gtk/Events.h b/UI/Gtk/Events.h new file mode 100644 index 0000000000..d981da4b18 --- /dev/null +++ b/UI/Gtk/Events.h @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2026, Johan Dahlin + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include + +#include + +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); + +} diff --git a/UI/Gtk/WebContentView.cpp b/UI/Gtk/WebContentView.cpp new file mode 100644 index 0000000000..4a1986a99a --- /dev/null +++ b/UI/Gtk/WebContentView.cpp @@ -0,0 +1,318 @@ +/* + * Copyright (c) 2026, Johan Dahlin + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace Ladybird { + +WebContentView::WebContentView(LadybirdWebView* widget, RefPtr 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(x * device_pixel_ratio), static_cast(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(); + } else if (m_backup_shared_image_buffer) { + bitmap = m_backup_shared_image_buffer->bitmap().ptr(); + bitmap_size = m_backup_bitmap_size.to_type(); + } + + 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 { 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(painted_width / device_pixel_ratio); + auto draw_height = static_cast(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(width) - draw_width, static_cast(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(width), static_cast(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(width), static_cast(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(width * device_pixel_ratio), static_cast(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 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); +} + +} diff --git a/UI/Gtk/WebContentView.h b/UI/Gtk/WebContentView.h new file mode 100644 index 0000000000..3986e8088d --- /dev/null +++ b/UI/Gtk/WebContentView.h @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2026, Johan Dahlin + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include + +#include +#include + +namespace Ladybird { + +class WebContentView final : public WebView::ViewImplementation { +public: + WebContentView(LadybirdWebView* widget, RefPtr 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 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 m_cached_texture; + Gfx::Bitmap const* m_cached_bitmap { nullptr }; + Gfx::IntSize m_cached_painted_size; +}; + +} diff --git a/UI/Gtk/Widgets/Builder.h b/UI/Gtk/Widgets/Builder.h new file mode 100644 index 0000000000..42fe788bd3 --- /dev/null +++ b/UI/Gtk/Widgets/Builder.h @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2026, Johan Dahlin + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include + +namespace LadybirdWidgets { + +template +T* get_builder_object(GtkBuilder* builder, char const* name) +{ + auto* object = gtk_builder_get_object(builder, name); + VERIFY(object); + return reinterpret_cast(object); +} + +} diff --git a/UI/Gtk/Widgets/LadybirdWebView.cpp b/UI/Gtk/Widgets/LadybirdWebView.cpp new file mode 100644 index 0000000000..dc48ea9915 --- /dev/null +++ b/UI/Gtk/Widgets/LadybirdWebView.cpp @@ -0,0 +1,271 @@ +/* + * Copyright (c) 2026, Johan Dahlin + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include + +#include +#include + +#define LADYBIRD_WEB_VIEW(obj) (reinterpret_cast(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. + // 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(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(dx * device_pixel_ratio); + wheel_delta_y = static_cast(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(dx * scroll_lines * scroll_step_size * device_pixel_ratio); + wheel_delta_y = static_cast(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; +} diff --git a/UI/Gtk/Widgets/LadybirdWebView.h b/UI/Gtk/Widgets/LadybirdWebView.h new file mode 100644 index 0000000000..a9fc6e50ae --- /dev/null +++ b/UI/Gtk/Widgets/LadybirdWebView.h @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2026, Johan Dahlin + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include + +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);