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:
parent
e8be4a88d9
commit
2573aff090
8 changed files with 1047 additions and 0 deletions
|
|
@ -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
326
UI/Gtk/Events.cpp
Normal 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
23
UI/Gtk/Events.h
Normal 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
318
UI/Gtk/WebContentView.cpp
Normal 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
62
UI/Gtk/WebContentView.h
Normal 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
22
UI/Gtk/Widgets/Builder.h
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
271
UI/Gtk/Widgets/LadybirdWebView.cpp
Normal file
271
UI/Gtk/Widgets/LadybirdWebView.cpp
Normal 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;
|
||||
}
|
||||
22
UI/Gtk/Widgets/LadybirdWebView.h
Normal file
22
UI/Gtk/Widgets/LadybirdWebView.h
Normal 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);
|
||||
Loading…
Reference in a new issue