aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.cache/clangd/index/saffronwebkit.c.AF389EFB28A2351D.idxbin0 -> 516 bytes
-rw-r--r--.cache/clangd/index/saffronwebkit.h.67ABB962FA06AEF7.idxbin0 -> 1630 bytes
-rw-r--r--.cache/clangd/index/sfwk-minimal.c.E2B36C4D9920C93F.idxbin0 -> 618 bytes
-rw-r--r--.cache/clangd/index/sfwk-wpe.c.3EB8ED2A64892E18.idxbin0 -> 13124 bytes
-rw-r--r--LICENSE21
-rw-r--r--include/saffronwebkit.h62
-rw-r--r--meson.build2
-rw-r--r--src/sfwk-wpe.c559
-rw-r--r--tests/sfwk-minimal.c32
9 files changed, 675 insertions, 1 deletions
diff --git a/.cache/clangd/index/saffronwebkit.c.AF389EFB28A2351D.idx b/.cache/clangd/index/saffronwebkit.c.AF389EFB28A2351D.idx
new file mode 100644
index 0000000..526496b
--- /dev/null
+++ b/.cache/clangd/index/saffronwebkit.c.AF389EFB28A2351D.idx
Binary files differ
diff --git a/.cache/clangd/index/saffronwebkit.h.67ABB962FA06AEF7.idx b/.cache/clangd/index/saffronwebkit.h.67ABB962FA06AEF7.idx
new file mode 100644
index 0000000..c6a4f41
--- /dev/null
+++ b/.cache/clangd/index/saffronwebkit.h.67ABB962FA06AEF7.idx
Binary files differ
diff --git a/.cache/clangd/index/sfwk-minimal.c.E2B36C4D9920C93F.idx b/.cache/clangd/index/sfwk-minimal.c.E2B36C4D9920C93F.idx
new file mode 100644
index 0000000..a8d23fe
--- /dev/null
+++ b/.cache/clangd/index/sfwk-minimal.c.E2B36C4D9920C93F.idx
Binary files differ
diff --git a/.cache/clangd/index/sfwk-wpe.c.3EB8ED2A64892E18.idx b/.cache/clangd/index/sfwk-wpe.c.3EB8ED2A64892E18.idx
new file mode 100644
index 0000000..93b324b
--- /dev/null
+++ b/.cache/clangd/index/sfwk-wpe.c.3EB8ED2A64892E18.idx
Binary files differ
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..136dbc3
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 Arslaan Pathan
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/include/saffronwebkit.h b/include/saffronwebkit.h
new file mode 100644
index 0000000..4287b48
--- /dev/null
+++ b/include/saffronwebkit.h
@@ -0,0 +1,62 @@
+// Copyright (C) 2026 Arslaan Pathan
+// This code contains excerpts from wpe-sdl-simple, which is released under MIT.
+// The original source code is available here: https://github.com/aperezdc/wpe-sdl-simple
+// See external_licenses/COPYING_wpe-sdl-simple.txt
+#ifndef SAFFRONWEBKIT_H
+#define SAFFRONWEBKIT_H
+
+#include "wpe/wpe-platform.h"
+#include <saffron.h>
+#include <wpe/webkit.h>
+#include <SDL3/SDL.h>
+#include <SDL3/SDL_egl.h>
+#include <SDL3/SDL_opengles2.h>
+
+// The web content area - view that renders to SDL
+struct _WPEViewSDL3 {
+ WPEView parent;
+ void* userdata;
+};
+
+// Window/toplevel container that owns all the web views - tab/window manager
+struct _WPEToplevelSDL3 {
+ WPEToplevel parent;
+ WPEView *view;
+ SDL_Texture *texture;
+};
+
+// Manages EGL/SDL display - global WPE context
+struct _WPEDisplaySDL3 {
+ WPEDisplay parent;
+
+ SDL_InitFlags init_flags;
+
+ SDL_Window *hidden_window;
+ SDL_GLContext gl_context;
+
+ EGLDisplay egl_display;
+
+ PFNEGLDESTROYIMAGEKHRPROC destroyImage;
+ PFNGLEGLIMAGETARGETTEXTURE2DOESPROC imageTargetTexture2DOES;
+};
+
+typedef struct {
+ GMainContext* main_context;
+ WPEDisplay* display;
+} SFWKContext;
+
+typedef struct {
+ SaffronWidget base;
+ WPEToplevel* toplevel;
+ WPEView* wpeview;
+ WebKitWebView* wkwebview;
+ SDL_Renderer* renderer;
+} SFWKWebView;
+
+SFWKContext* sfwk_init(); // can return NULL! be careful!
+
+void sfwk_process_event(SFWKContext* context, SFWKWebView* webview, SDL_Event* event); // the user needs to attach this to a saffron_hook_sdl_all_events
+
+SFWKWebView* sfwk_webview_new(SFWKContext* context, const char* url, int w, int h);
+
+#endif
diff --git a/meson.build b/meson.build
index 9164cf2..8c197d6 100644
--- a/meson.build
+++ b/meson.build
@@ -16,7 +16,7 @@ deps = [
inc = include_directories('include')
sources = [
- 'src/saffronwebkit.c'
+ 'src/sfwk-wpe.c'
]
sfwk_lib = static_library('saffronwebkit', sources, include_directories: inc, dependencies: deps)
diff --git a/src/sfwk-wpe.c b/src/sfwk-wpe.c
new file mode 100644
index 0000000..e5ba7ec
--- /dev/null
+++ b/src/sfwk-wpe.c
@@ -0,0 +1,559 @@
+// Copyright (C) 2026 Arslaan Pathan
+// This code contains excerpts from wpe-sdl-simple, which is released under MIT.
+// The original source code is available here: https://github.com/aperezdc/wpe-sdl-simple
+// See external_licenses/COPYING_wpe-sdl-simple.txt
+
+#include "glib.h"
+#include <SDL3/SDL_video.h>
+#include <saffron.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <wpe/webkit.h>
+#include <SDL3/SDL.h>
+#include <SDL3/SDL_egl.h>
+#include <SDL3/SDL_opengles2.h>
+#include <saffronwebkit.h>
+
+
+G_DECLARE_FINAL_TYPE(WPEViewSDL3, wpe_view_sdl3, WPE, VIEW_SDL3, WPEView)
+G_DECLARE_FINAL_TYPE(WPEToplevelSDL3, wpe_toplevel_sdl3, WPE, TOPLEVEL_SDL3, WPEToplevel)
+G_DECLARE_FINAL_TYPE(WPEDisplaySDL3, wpe_display_sdl3, WPE, DISPLAY_SDL3, WPEDisplay)
+
+G_DEFINE_FINAL_TYPE(WPEViewSDL3, wpe_view_sdl3, WPE_TYPE_VIEW)
+G_DEFINE_FINAL_TYPE(WPEToplevelSDL3, wpe_toplevel_sdl3, WPE_TYPE_TOPLEVEL)
+G_DEFINE_FINAL_TYPE(WPEDisplaySDL3, wpe_display_sdl3, WPE_TYPE_DISPLAY)
+
+static void
+wpe_toplevel_sdl3_ensure_texture(WPEToplevelSDL3 *self, SDL_Renderer* renderer, SDL_PixelFormat format, int width, int height)
+{
+ if (self->texture) {
+ if (self->texture->format == format && self->texture->w == width && self->texture->h == height)
+ return;
+ g_debug("%s: toplevel=%p, format=%#x, size=%dx%d, re-creating texture", G_STRFUNC, self, format, width, height);
+ SDL_DestroyTexture(self->texture);
+ }
+
+ self->texture = SDL_CreateTexture(renderer, format, SDL_TEXTUREACCESS_STREAMING, width, height);
+ g_assert(self->texture);
+}
+
+static gboolean
+wpe_toplevel_sdl3_render(WPEToplevelSDL3 *self, SDL_Renderer* renderer, WPEView *view [[maybe_unused]], WPEBuffer *buffer, GError **error)
+{
+ // g_debug("%s: toplevel=%p, view=%p, buffer=%p is a %s", G_STRFUNC, self, view, buffer, G_OBJECT_TYPE_NAME(buffer));
+
+ int buffer_width = wpe_buffer_get_width(buffer);
+ int buffer_height = wpe_buffer_get_height(buffer);
+
+ if (buffer) {
+ WPEDisplaySDL3 *display = (WPEDisplaySDL3*) wpe_toplevel_get_display((WPEToplevel*) self);
+ EGLImage image = wpe_buffer_import_to_egl_image(buffer, error);
+ if (image != EGL_NO_IMAGE && display->imageTargetTexture2DOES) {
+ g_assert(display->egl_display == SDL_EGL_GetCurrentDisplay());
+ wpe_toplevel_sdl3_ensure_texture(self, renderer, SDL_PIXELFORMAT_EXTERNAL_OES, buffer_width, buffer_height);
+ SDL_PropertiesID texture_props = SDL_GetTextureProperties(self->texture);
+ int texture_id = SDL_GetNumberProperty(texture_props, SDL_PROP_TEXTURE_OPENGL_TEXTURE_NUMBER, 0);
+ SDL_DestroyProperties(texture_props);
+ glBindTexture(GL_TEXTURE_2D, texture_id);
+ display->imageTargetTexture2DOES(GL_TEXTURE_2D, image);
+ } else if (g_error_matches(*error, WPE_BUFFER_ERROR, WPE_BUFFER_ERROR_NOT_SUPPORTED)) {
+ g_clear_error(error);
+ GBytes *bytes = wpe_buffer_import_to_pixels(buffer, error);
+ if (!bytes) {
+ g_set_error_literal(error, WPE_VIEW_ERROR, WPE_VIEW_ERROR_RENDER_FAILED,
+ "Cannot import buffer pixel data");
+ return FALSE;
+ }
+ wpe_toplevel_sdl3_ensure_texture(self, renderer, SDL_PIXELFORMAT_BGRA32, buffer_width, buffer_height);
+ // TODO: Update only the damaged rectangles.
+ void *pixels = NULL;
+ int stride = 0;
+ if (!SDL_LockTexture(self->texture, NULL, &pixels, &stride)) {
+ g_set_error(error, WPE_VIEW_ERROR, WPE_VIEW_ERROR_RENDER_FAILED,
+ "Cannot lock SDL texture to update from SHM buffer: %s", SDL_GetError());
+ return FALSE;
+ }
+ size_t size;
+ const void *source = g_bytes_get_data(bytes, &size);
+ if (stride == buffer_width * 4) {
+ memcpy(pixels, source, size);
+ } else {
+ for (unsigned i = 0; i < buffer_height; i++) {
+ memcpy(pixels, source, buffer_width * 4);
+ pixels = (void*) ((uintptr_t) pixels + stride);
+ source = (const void*) ((uintptr_t) source + buffer_width * 4);
+ }
+ }
+ SDL_UnlockTexture(self->texture);
+ } else {
+ g_set_error_literal(error, WPE_VIEW_ERROR, WPE_VIEW_ERROR_RENDER_FAILED,
+ "Cannot import buffer as EGLImage, nor import pixel data");
+ return FALSE;
+ }
+ if (image != EGL_NO_IMAGE)
+ display->destroyImage(display->egl_display, image);
+
+ // At this point the SDL_Texture contains the data to render.
+ if (!SDL_RenderTexture(renderer, self->texture, NULL, NULL)) {
+ g_set_error(error, WPE_VIEW_ERROR, WPE_VIEW_ERROR_RENDER_FAILED,
+ "Cannot render SDL texture: %s", SDL_GetError());
+ return FALSE;
+ }
+ } else {
+ SDL_SetRenderDrawColor(renderer, 255, 255, 255, SDL_ALPHA_OPAQUE);
+ SDL_RenderClear(renderer);
+ }
+
+ SDL_RenderPresent(renderer);
+ return TRUE;
+}
+
+static gboolean
+wpe_view_sdl3_render_buffer(WPEView *view, WPEBuffer *buffer, const WPERectangle *damage_rects, unsigned n_damage_rects, GError **error)
+{
+ WPEToplevel *toplevel = wpe_view_get_toplevel(view);
+ g_assert(WPE_IS_TOPLEVEL_SDL3(toplevel));
+
+ WPEViewSDL3* view_impl = (WPEViewSDL3*)view;
+ SFWKWebView* webview = (SFWKWebView*)view_impl->userdata;
+ SDL_Renderer* renderer = webview->renderer;
+
+ if (!wpe_toplevel_sdl3_render((WPEToplevelSDL3*) toplevel, renderer, view, buffer, error))
+ return FALSE;
+
+ wpe_view_buffer_rendered(view, buffer);
+ wpe_view_buffer_released(view, buffer);
+ return TRUE;
+}
+
+static void
+wpe_view_sdl3_on_notify_toplevel(WPEView *view)
+{
+ WPEToplevel *toplevel = wpe_view_get_toplevel(view);
+ if (toplevel) {
+ ((WPEToplevelSDL3*) toplevel)->view = view;
+ int width, height;
+ wpe_toplevel_get_size(toplevel, &width, &height);
+ if (width && height)
+ wpe_view_resized(view, width, height);
+ wpe_view_map(view);
+ } else {
+ wpe_view_unmap(view);
+ }
+}
+
+static void
+wpe_view_sdl3_constructed(GObject *object)
+{
+ G_OBJECT_CLASS(wpe_view_sdl3_parent_class)->constructed(object);
+ g_signal_connect(object, "notify::toplevel", G_CALLBACK(wpe_view_sdl3_on_notify_toplevel), NULL);
+ g_debug("%s: view=%p", G_STRFUNC, object);
+}
+
+static void
+wpe_view_sdl3_dispose(GObject *object)
+{
+ g_debug("%s: view=%p", G_STRFUNC, object);
+ G_OBJECT_CLASS(wpe_view_sdl3_parent_class)->dispose(object);
+}
+
+static void
+wpe_view_sdl3_init(WPEViewSDL3 *self)
+{
+}
+
+static void
+wpe_view_sdl3_class_init(WPEViewSDL3Class *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS(klass);
+ object_class->constructed = wpe_view_sdl3_constructed;
+ object_class->dispose = wpe_view_sdl3_dispose;
+
+ WPEViewClass *view_class = WPE_VIEW_CLASS(klass);
+ view_class->render_buffer = wpe_view_sdl3_render_buffer;
+}
+
+static void
+wpe_toplevel_sdl3_init(WPEToplevelSDL3 *self)
+{
+}
+
+static WPEView*
+wpe_view_sdl3_new(WPEDisplay *display)
+{
+ g_return_val_if_fail(WPE_IS_DISPLAY_SDL3(display), NULL);
+ return g_object_new(wpe_view_sdl3_get_type(), "display", display, NULL);
+}
+
+
+static gboolean
+wpe_toplevel_sdl3_each_view_resized(WPEToplevel *toplevel, WPEView *view, void *userdata)
+{
+ int width, height;
+ wpe_toplevel_get_size(toplevel, &width, &height);
+ wpe_view_resized(view, width, height);
+ return FALSE; // Continue iterating views.
+}
+
+// static gboolean
+// wpe_toplevel_sdl3_set_fullscreen(WPEToplevel *toplevel, gboolean fullscreen)
+// {
+// WPEToplevelSDL3 *self = WPE_TOPLEVEL_SDL3(toplevel);
+// if (!SDL_SetWindowFullscreen(self->window, !!fullscreen))
+// g_warning("Could not %s SDL window for toplevel %p",
+// fullscreen ? "fullscreen" : "un-fullscreen",
+// toplevel);
+// return FALSE;
+//
+// WPEToplevelState state = wpe_toplevel_get_state(toplevel);
+// if (fullscreen)
+// state |= WPE_TOPLEVEL_STATE_FULLSCREEN;
+// else
+// state &= ~WPE_TOPLEVEL_STATE_FULLSCREEN;
+// wpe_toplevel_state_changed(toplevel, state);
+// return TRUE;
+// }
+
+// static void
+// wpe_toplevel_sdl3_set_title(WPEToplevel *toplevel, const char *title)
+// {
+// WPEToplevelSDL3 *self = WPE_TOPLEVEL_SDL3(toplevel);
+// if (self->window)
+// SDL_SetWindowTitle(self->window, title ? title : SDL_GetAppMetadataProperty(SDL_PROP_APP_METADATA_NAME_STRING));
+// }
+
+// static gboolean
+// wpe_toplevel_sdl3_resize(WPEToplevel *toplevel, int width, int height)
+// {
+// WPEToplevelSDL3 *self = WPE_TOPLEVEL_SDL3(toplevel);
+// if (!SDL_SetWindowSize(self->window, width, height)) {
+// g_warning("Could not resize SDL window for toplevel %p: %s", self, SDL_GetError());
+// return FALSE;
+// }
+//
+// wpe_toplevel_resized(toplevel, width, height);
+// wpe_toplevel_foreach_view(toplevel, wpe_toplevel_sdl3_each_view_resized, NULL);
+// return TRUE;
+// }
+
+static void
+wpe_toplevel_sdl3_constructed(GObject *object)
+{
+ G_OBJECT_CLASS(wpe_toplevel_sdl3_parent_class)->constructed(object);
+}
+
+// what the fuck is this
+// im just going to steal this function and put it in my bindings
+// GLib is so confusing T_T
+//
+// coming back: i think this like, removes a view? e.g. closing a tab or smth
+static gboolean
+wpe_toplevel_sdl3_each_view_detach(WPEToplevel *toplevel [[maybe_unused]], WPEView *view, void *userdata [[maybe_unused]])
+{
+ wpe_view_set_toplevel(view, NULL);
+ return FALSE; // Continue iterating views.
+}
+
+static void
+wpe_toplevel_sdl3_dispose(GObject *object)
+{
+ WPEToplevelSDL3 *self = WPE_TOPLEVEL_SDL3(object);
+ g_debug("%s: toplevel=%p", G_STRFUNC, self);
+
+ wpe_toplevel_foreach_view(WPE_TOPLEVEL(object), wpe_toplevel_sdl3_each_view_detach, NULL);
+
+ g_clear_pointer(&self->texture, SDL_DestroyTexture);
+
+ G_OBJECT_CLASS(wpe_toplevel_sdl3_parent_class)->dispose(object);
+}
+
+static void
+wpe_toplevel_sdl3_class_init(WPEToplevelSDL3Class *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS(klass);
+ object_class->constructed = wpe_toplevel_sdl3_constructed;
+ object_class->finalize = wpe_toplevel_sdl3_dispose;
+
+ WPEToplevelClass *toplevel_class = WPE_TOPLEVEL_CLASS(klass);
+ // TODO add handlers for this in the future, maybe let the user hook into them with the abstractions
+ // toplevel_class->set_fullscreen = wpe_toplevel_sdl3_set_fullscreen;
+ // toplevel_class->set_title = wpe_toplevel_sdl3_set_title;
+ // toplevel_class->resize = wpe_toplevel_sdl3_resize;
+}
+
+static WPEToplevel*
+wpe_toplevel_sdl3_new(WPEDisplay *display)
+{
+ g_return_val_if_fail(WPE_IS_DISPLAY_SDL3(display), NULL);
+ return g_object_new(wpe_toplevel_sdl3_get_type(), "display", display, "max-views", 1, NULL);
+}
+
+static WPEView*
+wpe_toplevel_sdl3_get_view(WPEToplevelSDL3 *self)
+{
+ return wpe_toplevel_get_n_views((WPEToplevel*) self) ? self->view : NULL;
+}
+
+static void
+wpe_display_sdl3_finalize(GObject *object)
+{
+ WPEDisplaySDL3 *self = WPE_DISPLAY_SDL3(object);
+ g_debug("%s: display=%p", G_STRFUNC, self);
+
+ self->egl_display = EGL_NO_DISPLAY;
+ self->destroyImage = NULL;
+ self->imageTargetTexture2DOES = NULL;
+
+ g_clear_pointer(&self->gl_context, SDL_GL_DestroyContext);
+ g_clear_pointer(&self->hidden_window, SDL_DestroyWindow);
+
+ if (self->init_flags) {
+ SDL_QuitSubSystem(self->init_flags);
+ self->init_flags = 0;
+ }
+
+ G_OBJECT_CLASS(wpe_display_sdl3_parent_class)->finalize(object);
+}
+
+static gboolean
+wpe_display_sdl3_connect(WPEDisplay *display, GError **error)
+{
+ WPEDisplaySDL3 *self = WPE_DISPLAY_SDL3(display);
+
+ if (!SDL_WasInit(SDL_INIT_EVENTS))
+ self->init_flags |= SDL_INIT_EVENTS;
+ if (!SDL_WasInit(SDL_INIT_VIDEO))
+ self->init_flags |= SDL_INIT_VIDEO;
+
+ if (self->init_flags && !SDL_Init(self->init_flags)) {
+ g_set_error(error, WPE_DISPLAY_ERROR, WPE_DISPLAY_ERROR_CONNECTION_FAILED,
+ "Could not initialize SDL: %s", SDL_GetError());
+ return FALSE;
+ }
+
+ if (!(self->hidden_window = SDL_CreateWindow("Dummy", 1, 1, SDL_WINDOW_HIDDEN | SDL_WINDOW_OPENGL))) {
+ g_set_error(error, WPE_DISPLAY_ERROR, WPE_DISPLAY_ERROR_CONNECTION_FAILED,
+ "Could not create SDL hidden window: %s", SDL_GetError());
+ return FALSE;
+ }
+
+ if (!(self->gl_context = SDL_GL_CreateContext(self->hidden_window))) {
+ g_set_error(error, WPE_DISPLAY_ERROR, WPE_DISPLAY_ERROR_CONNECTION_FAILED,
+ "Could not create SDL-managed EGL context: %s", SDL_GetError());
+ return FALSE;
+ }
+ SDL_GL_MakeCurrent(self->hidden_window, self->gl_context);
+
+ if (!SDL_SyncWindow(self->hidden_window)) {
+ g_set_error(error, WPE_DISPLAY_ERROR, WPE_DISPLAY_ERROR_CONNECTION_FAILED,
+ "Could not sync hidden SDL window: %s", SDL_GetError());
+ return FALSE;
+ }
+
+ if ((self->egl_display = SDL_EGL_GetCurrentDisplay()) == EGL_NO_DISPLAY) {
+ g_set_error(error, WPE_DISPLAY_ERROR, WPE_DISPLAY_ERROR_CONNECTION_FAILED,
+ "Could not get SDL-managed EGL display: %s", SDL_GetError());
+ return FALSE;
+ }
+
+ self->destroyImage = (PFNEGLDESTROYIMAGEKHRPROC)
+ SDL_EGL_GetProcAddress("eglDestroyImageKHR");
+ self->imageTargetTexture2DOES = (PFNGLEGLIMAGETARGETTEXTURE2DOESPROC)
+ SDL_EGL_GetProcAddress("glEGLImageTargetTexture2DOES");
+ g_debug("%s: done, eglDestroyImageKHR=%p, glEGLImageTargetTexture2DOES=%p", G_STRFUNC,
+ self->destroyImage, self->imageTargetTexture2DOES);
+
+ if (!self->destroyImage) {
+ g_set_error_literal(error, WPE_DISPLAY_ERROR, WPE_DISPLAY_ERROR_CONNECTION_FAILED,
+ "EGL does not support eglDestroyImageKHR");
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static EGLDisplay
+wpe_display_sdl3_get_egl_display(WPEDisplay *display, GError **error [[maybe_unused]])
+{
+ WPEDisplaySDL3 *self = WPE_DISPLAY_SDL3(display);
+ return self->egl_display;
+}
+//
+// creates a new wpe view and toplevel
+// oh and don't forget, GLib black magic
+// yaaayyyyyyyyyyyy... we love glib (sarc)
+static WPEView*
+wpe_display_sdl3_create_view(WPEDisplay *display)
+{
+ g_autoptr(WPEView) view = wpe_view_sdl3_new(display);
+
+ const gboolean create_toplevel = wpe_settings_get_boolean(wpe_display_get_settings(display), WPE_SETTING_CREATE_VIEWS_WITH_A_TOPLEVEL, NULL);
+ if (create_toplevel) {
+ g_autoptr(WPEToplevel) toplevel = wpe_toplevel_sdl3_new(display);
+ wpe_view_set_toplevel(view, toplevel);
+ }
+
+ return g_steal_pointer(&view);
+}
+
+// initializes the WPEDisplaySDL3Class class? what is this sh*t? why did they have to use GLib for the WPE api?
+// WHY IGALIA WHYYYYYYYY
+static void
+wpe_display_sdl3_class_init(WPEDisplaySDL3Class *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS(klass);
+ object_class->finalize = wpe_display_sdl3_finalize;
+
+ WPEDisplayClass *display_class = WPE_DISPLAY_CLASS(klass);
+ display_class->connect = wpe_display_sdl3_connect;
+ display_class->get_egl_display = wpe_display_sdl3_get_egl_display;
+ display_class->create_view = wpe_display_sdl3_create_view;
+}
+
+static void
+wpe_display_sdl3_init(WPEDisplaySDL3 *self)
+{
+ g_debug("%s: display=%p", G_STRFUNC, self);
+}
+
+// if the webview has a new title, go call that function that i annotated like 10 years ago and do the thing
+static void
+handle_web_view_notify_title(WebKitWebView *web_view)
+{
+ const char *title = webkit_web_view_get_title(web_view);
+ if (!(title && *title))
+ title = SDL_GetAppMetadataProperty(SDL_PROP_APP_METADATA_NAME_STRING);
+
+ g_message("View<%p>.title = %s", web_view, title);
+
+ WPEView *view = webkit_web_view_get_wpe_view(web_view);
+ wpe_toplevel_set_title(wpe_view_get_toplevel(view), title);
+}
+
+static WebKitWebView* handle_web_view_create(WebKitWebView*, WebKitNavigationAction*, SFWKContext*);
+
+
+static void
+free_weak_ref(void *ptr)
+{
+ GWeakRef *ref = ptr;
+ g_weak_ref_clear(ref);
+ g_free(ref);
+}
+
+static WebKitWebView*
+context_add_web_view(SFWKContext* self, WebKitWebView *related_view, const char *uri)
+{
+ g_autoptr(WebKitWebView) view = g_object_new(WEBKIT_TYPE_WEB_VIEW,
+ "display", self->display,
+ "related-view", related_view,
+ NULL);
+
+ GWeakRef *view_ref = g_new0(GWeakRef, 1);
+ g_weak_ref_init(view_ref, view);
+ g_object_set_data_full(G_OBJECT(webkit_web_view_get_wpe_view(view)), "weak-web-view", view_ref, free_weak_ref);
+
+ g_signal_connect(view, "notify::title", G_CALLBACK(handle_web_view_notify_title), NULL);
+ g_signal_connect(view, "create", G_CALLBACK(handle_web_view_create), self);
+
+ if (uri)
+ webkit_web_view_load_uri(view, uri);
+
+ return g_steal_pointer(&view);
+}
+
+static WebKitWebView*
+handle_web_view_create(WebKitWebView *view, WebKitNavigationAction *action [[maybe_unused]], SFWKContext *context)
+{
+ return context_add_web_view(context, view, NULL);
+}
+
+// map SDL buttons to WPE buttons
+static unsigned
+wpe_button_for_sdl_button(unsigned index)
+{
+ switch (index) {
+ case 1: return WPE_BUTTON_PRIMARY;
+ case 2: return WPE_BUTTON_MIDDLE;
+ case 3: return WPE_BUTTON_SECONDARY;
+ default: return 0;
+ }
+}
+
+// map SDL keymodifiers to WPE mods
+static WPEModifiers
+wpe_modifiers_for_sdl_keymod(SDL_Keymod keymod)
+{
+ WPEModifiers result = 0;
+ if (keymod & SDL_KMOD_CTRL)
+ result |= WPE_MODIFIER_KEYBOARD_CONTROL;
+ if (keymod & SDL_KMOD_SHIFT)
+ result |= WPE_MODIFIER_KEYBOARD_SHIFT;
+ if (keymod & SDL_KMOD_ALT)
+ result |= WPE_MODIFIER_KEYBOARD_ALT;
+ if (keymod & SDL_KMOD_GUI)
+ result |= WPE_MODIFIER_KEYBOARD_META;
+ if (keymod & SDL_KMOD_CAPS)
+ result |= WPE_MODIFIER_KEYBOARD_CAPS_LOCK;
+ return result;
+}
+
+SFWKContext* sfwk_init() {
+ g_io_extension_point_register(WPE_DISPLAY_EXTENSION_POINT_NAME);
+ g_io_extension_point_implement(WPE_DISPLAY_EXTENSION_POINT_NAME, wpe_display_sdl3_get_type(), "sdl3", 200);
+
+ SFWKContext* context = g_new0(SFWKContext, 1);
+ context->main_context = g_main_context_new_with_flags(G_MAIN_CONTEXT_FLAGS_OWNERLESS_POLLING);
+
+ g_autoptr(GError) error = NULL;
+
+ context->display = g_object_new(wpe_display_sdl3_get_type(), NULL);
+ if (!wpe_display_connect(context->display, &error)) {
+ g_printerr("cant connect to display T_T %s\n", error->message);
+ g_clear_object(&context->display);
+ g_clear_pointer(&context->main_context, g_main_context_unref);
+ g_free(context);
+ return NULL;
+ }
+
+ return context;
+}
+
+static void sfwk_webview_draw(SaffronWidget* widget, SDL_Renderer* renderer) {
+ SFWKWebView* webview = (SFWKWebView*)widget;
+ webview->renderer = renderer;
+
+ WPEToplevelSDL3 *toplevel = (WPEToplevelSDL3*)webview->toplevel;
+ if (toplevel->texture) {
+ SDL_RenderTexture(renderer, toplevel->texture, NULL, NULL);
+ }
+}
+
+void sfwk_process_event(SFWKContext *context, SFWKWebView* webview, SDL_Event *event) {
+ WPEToplevel* toplevel = webview->toplevel;
+ // for some reason we're doing this weird sh*t? we could just use webview->view but oh well
+ WPEView *view = toplevel ? wpe_toplevel_sdl3_get_view((WPEToplevelSDL3*) toplevel) : NULL;
+}
+
+SFWKWebView* sfwk_webview_new(SFWKContext* context, const char* url, int w, int h) {
+ SFWKWebView* webview = malloc(sizeof(SFWKWebView));
+ if (!webview) return NULL; // good fucking luck
+ //
+ saffron_widget_init((SaffronWidget*)webview);
+ ((SaffronWidget*)webview)->type = SAFFRON_WIDGET_UNKNOWN; // saffron does not have a builtin for webviews, and i cant be bothered to add one because "if you have to edit the library, it's not extensible" - arslaan 2026
+ ((SaffronWidget*)webview)->w = w;
+ ((SaffronWidget*)webview)->h = h;
+ ((SaffronWidget*)webview)->draw = sfwk_webview_draw;
+ // add an on_resize here
+
+ webview->toplevel = wpe_toplevel_sdl3_new(context->display);
+
+ webview->wpeview = wpe_view_sdl3_new(context->display);
+ ((WPEViewSDL3*)webview->wpeview)->userdata = webview;
+ wpe_view_set_toplevel(webview->wpeview, webview->toplevel);
+
+ webview->wkwebview = g_object_new(WEBKIT_TYPE_WEB_VIEW, "display", context->display, NULL);
+ webkit_web_view_load_uri(webview->wkwebview, url);
+
+ return webview;
+}
diff --git a/tests/sfwk-minimal.c b/tests/sfwk-minimal.c
index e69de29..fe88174 100644
--- a/tests/sfwk-minimal.c
+++ b/tests/sfwk-minimal.c
@@ -0,0 +1,32 @@
+#include <saffron.h>
+#include <saffronwebkit.h>
+
+// store globals because yes
+SFWKContext* ctx;
+SFWKWebView* webview;
+
+bool hook_callback(SDL_Event* event) {
+ sfwk_process_event(ctx, webview, event);
+ return true;
+}
+
+int main() {
+ saffron_init();
+
+ ctx = sfwk_init();
+ if (!ctx) return 1;
+
+ SaffronWindow* window = saffron_window_new("SaffronWebKit Test", 1024, 768);
+ window->root->theme = SF_MACRO_DEFAULT_THEME;
+
+ SFWKWebView* webview = sfwk_webview_new(ctx, "https://arslaancodes.com", 900, 600);
+ saffron_widget_add_child(window->root, (SaffronWidget*)webview);
+
+ saffron_hook_sdl_all_events(window, hook_callback, 999);
+
+ saffron_window_main(window);
+
+ saffron_window_free(window);
+ saffron_quit();
+ return 0;
+}