// 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-object.h" #include "glib.h" #include "glibconfig.h" #include "saffron_api.h" #include #include #include #include #include #include #include #include #include #include #include #include // dont do this // TODO make this not use globals // if one were to make another webview in another window, they're f**ked // completely f**ked static EGLContext saved_egl_context = EGL_NO_CONTEXT; static EGLDisplay saved_egl_display = EGL_NO_DISPLAY; 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 on_load_changed(WebKitWebView *web_view, WebKitLoadEvent load_event, gpointer userdata) { const char* event_str = "UNKNOWN"; switch(load_event) { case WEBKIT_LOAD_STARTED: event_str = "STARTED"; break; case WEBKIT_LOAD_REDIRECTED: event_str = "REDIRECTED"; break; case WEBKIT_LOAD_COMMITTED: event_str = "COMMITTED"; break; case WEBKIT_LOAD_FINISHED: event_str = "FINISHED"; break; } g_debug("[sfwk] WebKit load event: %s", event_str); } static void on_load_failed(WebKitWebView *web_view, WebKitLoadEvent load_event, const char *failing_uri, GError *error, gpointer userdata) { g_warning("[sfwk] WebKit load failed (T_T): URI: %s, Error: %s", failing_uri, error->message); } 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("SFWK: wpe_toplevel_sdl3_render running"); 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); g_debug("Buffer size: %dx%d", buffer_width, buffer_height); 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); // don't render here because draw function does stuff } else { g_debug("SFWK: no buffer found T_T"); } 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_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; 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); self->egl_display = SDL_EGL_GetCurrentDisplay(); if (self->egl_display == EGL_NO_DISPLAY) { g_set_error(error, WPE_DISPLAY_ERROR, WPE_DISPLAY_ERROR_CONNECTION_FAILED, "No active EGL display. Saffron must initialize OpenGL first."); return FALSE; } self->destroyImage = (PFNEGLDESTROYIMAGEKHRPROC) SDL_EGL_GetProcAddress("eglDestroyImageKHR"); self->imageTargetTexture2DOES = (PFNGLEGLIMAGETARGETTEXTURE2DOESPROC) SDL_EGL_GetProcAddress("glEGLImageTargetTexture2DOES"); g_debug("%s: Using existing EGL display %p", G_STRFUNC, self->egl_display); 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_main_context_push_thread_default(context->main_context); context->display = g_object_new(wpe_display_sdl3_get_type(), NULL); // ensure_initialized will do the GL context later return context; } static bool sfwk_webview_ensure_initialized(SFWKWebView* webview) { if (webview->wpe.initialized) return true; if (!webview->window) { g_debug("sfwk: no window reference, cannot get GL context"); return false; } SDL_GLContext ctx = saffron_window_get_gl_context(webview->window); if (!ctx) { g_debug("sfwk: no gl context in window?? good luck"); return false; } EGLContext saffron_ctx = (EGLContext)ctx; EGLDisplay saffron_display = (EGLDisplay)saffron_window_get_egl_display(webview->window); EGLConfig saffron_conf = saffron_window_get_egl_config(webview->window); EGLContext webkit_ctx = eglCreateContext(saffron_display, saffron_conf, saffron_ctx, NULL); EGLint pbuffer_attribs[] = { EGL_WIDTH, 1, EGL_HEIGHT, 1, EGL_NONE }; EGLSurface dummy_surface = eglCreatePbufferSurface(saffron_display, saffron_conf, pbuffer_attribs); if (dummy_surface == EGL_NO_SURFACE) { g_debug("sfwk: eglCreatePbufferSurface failed: 0x%x", eglGetError()); eglDestroyContext(saffron_display, webkit_ctx); return false; } if (!eglMakeCurrent(saffron_display, dummy_surface, dummy_surface, webkit_ctx)) { g_debug("sfwk: eglMakeCurrent failed: 0x%x", eglGetError()); eglDestroySurface(saffron_display, dummy_surface); eglDestroyContext(saffron_display, webkit_ctx); return false; } webview->wpe.egl_display = saffron_display; webview->wpe.egl_context = webkit_ctx; webview->wpe.egl_dummy_surface = dummy_surface; saved_egl_context = eglGetCurrentContext(); saved_egl_display = eglGetCurrentDisplay(); if (saved_egl_context == EGL_NO_CONTEXT) { g_debug("sfwk: No current EGL context, waiting..."); return false; } g_autoptr(GError) error = NULL; if (!wpe_display_connect(webview->context->display, &error)) { g_warning("sfwk: Failed to connect display: %s", error->message); return false; } WPEDisplaySDL3 *display_impl = WPE_DISPLAY_SDL3(webview->context->display); display_impl->egl_display = saved_egl_display; webview->wpe.wkwebview = g_object_new(WEBKIT_TYPE_WEB_VIEW, "display", webview->context->display, NULL); webview->wpe.wpeview = webkit_web_view_get_wpe_view(webview->wpe.wkwebview); ((WPEViewSDL3*)webview->wpe.wpeview)->userdata = webview; webview->wpe.toplevel = wpe_view_get_toplevel(webview->wpe.wpeview); wpe_toplevel_resize(webview->wpe.toplevel, webview->w, webview->h); WebKitSettings* settings = webkit_web_view_get_settings(webview->wpe.wkwebview); webkit_settings_set_enable_webaudio(settings, TRUE); g_signal_connect(webview->wpe.wkwebview, "load-changed", G_CALLBACK(on_load_changed), webview); g_signal_connect(webview->wpe.wkwebview, "load-failed", G_CALLBACK(on_load_failed), webview); if (webview->url) { webkit_web_view_load_uri(webview->wpe.wkwebview, webview->url); g_debug("sfwk: Loading URL: %s", webview->url); } webview->wpe.initialized = TRUE; g_debug("sfwk: WPE initialized successfully"); return true; } static void sfwk_webview_draw(SaffronWidget* widget, SDL_Renderer* renderer) { SFWKWebView* webview = (SFWKWebView*)widget; webview->renderer = renderer; if (sfwk_webview_ensure_initialized(webview)) { g_main_context_iteration(webview->context->main_context, FALSE); static gboolean first_frame = TRUE; if (first_frame) { wpe_toplevel_resize(webview->wpe.toplevel, widget->w, widget->h); wpe_view_resized(webview->wpe.wpeview, widget->w, widget->h); first_frame = FALSE; } WPEToplevelSDL3* toplevel = (WPEToplevelSDL3*)webview->wpe.toplevel; if (toplevel && toplevel->texture) { SDL_FRect dst = { widget->x, widget->y, widget->w, widget->h }; SDL_RenderTexture(renderer, toplevel->texture, NULL, &dst); GLenum err = glGetError(); if (err != GL_NO_ERROR) { g_warning("OpenGL error: 0x%x", err); } return; } } // draw white to indicate loading // maybe change this to black, or even.. make it.. CONFIGURABLE!! with a texture??? SDL_SetRenderDrawColor(renderer, 240, 240, 240, 255); SDL_RenderFillRect(renderer, &(SDL_FRect){0, 0, widget->w, widget->h}); } WebKitWebView* sfwk_webview_get_wkwebview(SFWKWebView *webview) { if (!webview->wpe.initialized) { return NULL; } else { return webview->wpe.wkwebview; } } static void sfwk_webview_free(SaffronWidget* widget) { if (!widget) return; SFWKWebView* webview = (SFWKWebView*)widget; if (webview->wpe.egl_context) { eglMakeCurrent(webview->wpe.egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); if (webview->wpe.egl_dummy_surface) eglDestroySurface(webview->wpe.egl_display, webview->wpe.egl_dummy_surface); eglDestroyContext(webview->wpe.egl_display, webview->wpe.egl_context); } if (webview->url) free(webview->url); // Webview doesn't need to free itself, saffron handles that after calling widget->free(); } bool sfwk_process_event(SFWKContext *context, SFWKWebView* webview, SDL_Event *event) { if (!webview) return false; if (!sfwk_webview_ensure_initialized(webview)) return false; WPEToplevel* toplevel = webview->wpe.toplevel; WPEView* view = webview->wpe.wpeview; float mx, my; SDL_GetMouseState(&mx, &my); bool mouse_in_webview = mx >= webview->base.x && mx < webview->base.x + webview->base.w && my >= webview->base.y && my < webview->base.y + webview->base.h; g_autoptr(WPEEvent) wpe_event = NULL; switch (event->type) { case SDL_EVENT_WINDOW_SHOWN: wpe_toplevel_state_changed(toplevel, wpe_toplevel_get_state(toplevel) | WPE_TOPLEVEL_STATE_ACTIVE); return false; case SDL_EVENT_WINDOW_HIDDEN: wpe_toplevel_state_changed(toplevel, wpe_toplevel_get_state(toplevel) & ~WPE_TOPLEVEL_STATE_ACTIVE); return false; case SDL_EVENT_WINDOW_FOCUS_GAINED: if (view) wpe_view_focus_in(view); return false; case SDL_EVENT_WINDOW_FOCUS_LOST: if (view) wpe_view_focus_out(view); return false; case SDL_EVENT_WINDOW_CLOSE_REQUESTED: { GWeakRef *view_ref = g_object_get_data(G_OBJECT(view), "weak-web-view"); g_autoptr(WebKitWebView) web_view = NULL; if (view_ref) web_view = WEBKIT_WEB_VIEW(g_weak_ref_get(view_ref)); if (web_view) g_object_unref(web_view); return false; } case SDL_EVENT_WINDOW_MOUSE_ENTER: case SDL_EVENT_WINDOW_MOUSE_LEAVE: wpe_event = wpe_event_pointer_move_new( (event->type == SDL_EVENT_WINDOW_MOUSE_ENTER) ? WPE_EVENT_POINTER_ENTER : WPE_EVENT_POINTER_LEAVE, view, WPE_INPUT_SOURCE_MOUSE, event->window.timestamp, 0, 0, 0, 0, 0); break; case SDL_EVENT_MOUSE_MOTION: g_debug("SFWK: mouse motion at (%.1f, %.1f), webview at (%.1f, %.1f) size (%dx%d), in_webview=%d", mx, my, (double)webview->base.x, (double)webview->base.y, webview->base.w, webview->base.h, mouse_in_webview); if (!mouse_in_webview) return false; wpe_event = wpe_event_pointer_move_new(WPE_EVENT_POINTER_MOVE, view, WPE_INPUT_SOURCE_MOUSE, event->motion.timestamp, 0, event->motion.x - webview->base.x, event->motion.y - webview->base.y, event->motion.xrel, event->motion.yrel); break; case SDL_EVENT_MOUSE_BUTTON_DOWN: g_debug("SFWK: mouse button DOWN %d at (%.1f, %.1f), in_webview=%d, wpe_button=%d", event->button.button, event->button.x, event->button.y, mouse_in_webview, wpe_button_for_sdl_button(event->button.button)); if (!mouse_in_webview) return false; if (wpe_button_for_sdl_button(event->button.button)) wpe_event = wpe_event_pointer_button_new(WPE_EVENT_POINTER_DOWN, view, WPE_INPUT_SOURCE_MOUSE, event->button.timestamp, 0, wpe_button_for_sdl_button(event->button.button), event->button.x - webview->base.x, event->button.y - webview->base.y, event->button.clicks); break; case SDL_EVENT_MOUSE_BUTTON_UP: g_debug("SFWK: mouse button UP %d at (%.1f, %.1f), in_webview=%d", event->button.button, event->button.x, event->button.y, mouse_in_webview); if (!mouse_in_webview) return false; if (wpe_button_for_sdl_button(event->button.button)) wpe_event = wpe_event_pointer_button_new(WPE_EVENT_POINTER_UP, view, WPE_INPUT_SOURCE_MOUSE, event->button.timestamp, 0, wpe_button_for_sdl_button(event->button.button), event->button.x - webview->base.x, event->button.y - webview->base.y, 0); break; case SDL_EVENT_MOUSE_WHEEL: g_debug("SFWK: mouse wheel (%.1f, %.1f), in_webview=%d", event->wheel.x, event->wheel.y, mouse_in_webview); if (!mouse_in_webview) return false; wpe_event = wpe_event_scroll_new(view, WPE_INPUT_SOURCE_MOUSE, event->wheel.timestamp, 0, event->wheel.x, event->wheel.y, FALSE, FALSE, event->wheel.mouse_x - webview->base.x, event->wheel.mouse_y - webview->base.y); break; case SDL_EVENT_KEY_DOWN: case SDL_EVENT_KEY_UP: g_debug("SFWK: key %s scancode=%d key=%d", event->type == SDL_EVENT_KEY_DOWN ? "DOWN" : "UP", event->key.scancode, event->key.key); wpe_event = wpe_event_keyboard_new( (event->type == SDL_EVENT_KEY_DOWN) ? WPE_EVENT_KEYBOARD_KEY_DOWN : WPE_EVENT_KEYBOARD_KEY_UP, view, WPE_INPUT_SOURCE_KEYBOARD, event->key.timestamp, wpe_modifiers_for_sdl_keymod(event->key.mod), event->key.scancode, event->key.key); break; default: return false; } if (view && wpe_event) { g_debug("SFWK: dispatching wpe_event to view %p", view); wpe_view_event(view, wpe_event); } else { g_debug("SFWK: no wpe_event to dispatch (view=%p, wpe_event=%p)", view, wpe_event); } return false; } static void sfwk_webview_resize_handler(SaffronWidget* self) { if (!self) return; SFWKWebView* webview = (SFWKWebView*)self; if (!webview) return; if (!webview->wpe.initialized) return; wpe_toplevel_resized(webview->wpe.toplevel, self->w, self->h); wpe_toplevel_foreach_view(webview->wpe.toplevel, wpe_toplevel_sdl3_each_view_resized, 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 // TODO for saffron: Maybe instead of checking type enums, just have the objects themselves have pre-layout/post-layout functions. thats PEAK ((SaffronWidget*)webview)->w = webview->w = w; ((SaffronWidget*)webview)->h = webview->h = h; ((SaffronWidget*)webview)->draw = sfwk_webview_draw; ((SaffronWidget*)webview)->free = sfwk_webview_free; ((SaffronWidget*)webview)->on_resize = sfwk_webview_resize_handler; webview->context = context; webview->url = strdup(url); webview->wpe.initialized = FALSE; webview->window = NULL; // ensure initialized will init this later ^_^ return webview; }