fix(electrobun): XUngrabPointer before begin_resize_drag (SDL2 pattern)

Root cause confirmed via X11 research: WebKitGTK holds an implicit X11
pointer grab from button-press. GTK's gdk_seat_ungrab targets the wrong
device, leaving the grab active. Mutter gets AlreadyGrabbed and gives up.

Fix: call XUngrabPointer(xdisplay, CurrentTime) + XFlush directly via
libX11.so.6 FFI before begin_resize_drag. This releases ALL grabs on
the display connection, matching SDL2's proven approach.

Removed failed approaches: WebView sensitivity toggle, findWebView cache.
This commit is contained in:
Hibryda 2026-03-25 14:06:45 +01:00
parent 7bb08697d6
commit 058ae563d5

View file

@ -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<typeof dlopen> | null = null;
let gdk3: ReturnType<typeof dlopen> | null = null;
let x11: ReturnType<typeof dlopen> | 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<typeof gtk3>, windowPtr: any) {
} catch { /* ignore */ }
}
/** Cache the WebView widget pointer for sensitivity toggling during resize */
let cachedWebView: any = null;
function findWebView(lib: NonNullable<typeof gtk3>, 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);