diff --git a/ui-electrobun/src/bun/gtk-window.ts b/ui-electrobun/src/bun/gtk-window.ts index 7173326..30abeca 100644 --- a/ui-electrobun/src/bun/gtk-window.ts +++ b/ui-electrobun/src/bun/gtk-window.ts @@ -6,7 +6,7 @@ * for smooth, zero-CPU resize/move behavior. */ -import { dlopen, FFIType, ptr } from "bun:ffi"; +import { dlopen, FFIType, ptr, CString } from "bun:ffi"; // GdkWindowEdge values export const GDK_EDGE = { @@ -18,6 +18,53 @@ export const GDK_EDGE = { type GdkEdge = (typeof GDK_EDGE)[keyof typeof GDK_EDGE]; let gtk3: ReturnType | null = null; +let gdk3: ReturnType | null = null; +let x11: ReturnType | null = null; + +function getX11() { + if (x11) return x11; + try { + x11 = dlopen("libX11.so.6", { + XUngrabPointer: { args: [FFIType.ptr, FFIType.u64], returns: FFIType.i32 }, + XFlush: { args: [FFIType.ptr], returns: FFIType.i32 }, + }); + return x11; + } catch { return null; } +} + +function getGdk() { + if (gdk3) return gdk3; + try { + gdk3 = dlopen("libgdk-3.so.0", { + gdk_display_get_default: { args: [], returns: FFIType.ptr }, + gdk_x11_display_get_xdisplay: { args: [FFIType.ptr], returns: FFIType.ptr }, + }); + return gdk3; + } catch { return null; } +} + +/** + * Release ALL X11 pointer grabs on the default display. + * This is the SDL2 pattern: XUngrabPointer(dpy, CurrentTime=0) + XFlush. + * Must be called BEFORE _NET_WM_MOVERESIZE so the WM can grab the pointer. + */ +function releaseX11Grab() { + const gdkLib = getGdk(); + const x11Lib = getX11(); + if (!gdkLib || !x11Lib) return false; + try { + const gdkDisplay = gdkLib.symbols.gdk_display_get_default(); + if (!gdkDisplay) return false; + const xDisplay = gdkLib.symbols.gdk_x11_display_get_xdisplay(gdkDisplay); + if (!xDisplay) return false; + x11Lib.symbols.XUngrabPointer(xDisplay, BigInt(0)); // CurrentTime = 0 + x11Lib.symbols.XFlush(xDisplay); + return true; + } catch (err) { + console.error("[gtk-window] releaseX11Grab failed:", err); + return false; + } +} function getGtk() { if (gtk3) return gtk3; @@ -219,41 +266,9 @@ function forceSmallMinSize(lib: NonNullable, windowPtr: any) { } catch { /* ignore */ } } -/** Cache the WebView widget pointer for sensitivity toggling during resize */ -let cachedWebView: any = null; - -function findWebView(lib: NonNullable, windowPtr: any): any { - if (cachedWebView) return cachedWebView; - try { - // Window → container → scrolledwindow/webview - let widget = lib.symbols.gtk_bin_get_child(windowPtr); - // Walk down the bin chain to find the deepest child (the WebView) - for (let i = 0; i < 5; i++) { - const child = lib.symbols.gtk_bin_get_child(widget); - if (!child) break; - widget = child; - } - cachedWebView = widget; - return widget; - } catch { return null; } -} - -/** - * Temporarily disable WebView input during resize to prevent grab interference. - * Re-enable after resize completes (WM sends configure-event). - */ -export function setWebViewSensitive(windowPtr: number | bigint, sensitive: boolean) { - const lib = getGtk(); - if (!lib) return; - const wv = findWebView(lib, windowPtr as any); - if (wv) { - lib.symbols.gtk_widget_set_sensitive(wv, sensitive); - } -} - /** * Delegate resize to the window manager. - * Disables WebView input first to prevent grab interference. + * Releases X11 grabs first (SDL2 pattern) so WM can take the pointer. */ export function beginResizeDrag( windowPtr: number | bigint, @@ -265,26 +280,22 @@ export function beginResizeDrag( const lib = getGtk(); if (!lib) return false; try { - // 1. Force small min-size + // 1. Force small min-size so WM allows shrink forceSmallMinSize(lib, windowPtr as any); - // 2. Disable WebView input to prevent grab interference - const wv = findWebView(lib, windowPtr as any); - if (wv) lib.symbols.gtk_widget_set_sensitive(wv, false); - // 3. Start WM resize + // 2. Release ALL X11 pointer grabs (SDL2 pattern) + // WebKitGTK holds an implicit grab from the button-press event. + // gdk_seat_ungrab inside begin_resize_drag targets the wrong device. + // Direct XUngrabPointer releases ALL grabs on this display connection. + releaseX11Grab(); + // 3. Start WM resize — WM can now grab the pointer successfully lib.symbols.gtk_window_begin_resize_drag( windowPtr as any, edge, button, Math.round(rootX), Math.round(rootY), - 0, // GDK_CURRENT_TIME + 0, // GDK_CURRENT_TIME — WM generates its own timestamp ); - // 4. Re-enable WebView after resize drag likely ends - // The WM holds the grab until mouse-up. 5s covers long drags. - // Shorter would risk re-enabling mid-drag. - setTimeout(() => { - if (wv) lib.symbols.gtk_widget_set_sensitive(wv, true); - }, 5000); return true; } catch (err) { console.error("[gtk-window] begin_resize_drag failed:", err);