// 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 #include #include #include #include #include #include #include #include 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; }