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:
parent
7bb08697d6
commit
058ae563d5
1 changed files with 57 additions and 46 deletions
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue