fix(electrobun): X11 XMoveResizeWindow bypass for resize (no GTK involvement)

begin_resize_drag + XUngrabPointer still fails because GTK's layout
cycle re-asserts WebView preferred size, fighting the WM resize.

New approach: JS mousemove → XMoveResizeWindow via libX11.so.6 FFI.
Completely bypasses GTK size negotiation. GTK only receives
ConfigureNotify after the X server has already resized the window.

Added: x11SetFrame() using gdk_x11_display_get_xdisplay +
gdk_x11_window_get_xid + XMoveResizeWindow.
This commit is contained in:
Hibryda 2026-03-25 14:19:27 +01:00
parent 058ae563d5
commit 0e6408a447
3 changed files with 98 additions and 10 deletions

View file

@ -27,6 +27,7 @@ function getX11() {
x11 = dlopen("libX11.so.6", {
XUngrabPointer: { args: [FFIType.ptr, FFIType.u64], returns: FFIType.i32 },
XFlush: { args: [FFIType.ptr], returns: FFIType.i32 },
XMoveResizeWindow: { args: [FFIType.ptr, FFIType.u64, FFIType.i32, FFIType.i32, FFIType.u32, FFIType.u32], returns: FFIType.i32 },
});
return x11;
} catch { return null; }
@ -38,6 +39,7 @@ function getGdk() {
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 },
gdk_x11_window_get_xid: { args: [FFIType.ptr], returns: FFIType.u64 },
});
return gdk3;
} catch { return null; }
@ -151,6 +153,11 @@ function getGtk() {
args: [FFIType.ptr],
returns: FFIType.void,
},
// Get GdkWindow from GtkWidget (needed for X11 window ID)
gtk_widget_get_window: {
args: [FFIType.ptr],
returns: FFIType.ptr,
},
// Sensitivity (disable input processing on widget)
gtk_widget_set_sensitive: {
args: [FFIType.ptr, FFIType.bool],
@ -266,6 +273,42 @@ function forceSmallMinSize(lib: NonNullable<typeof gtk3>, windowPtr: any) {
} catch { /* ignore */ }
}
/**
* Set window frame directly via X11, BYPASSING GTK's size negotiation.
* GTK receives ConfigureNotify after the fact and adjusts.
* This prevents the WebView's preferred size from fighting the resize.
*/
export function x11SetFrame(
windowPtr: number | bigint,
x: number, y: number, width: number, height: number,
): boolean {
const gtkLib = getGtk();
const gdkLib = getGdk();
const x11Lib = getX11();
if (!gtkLib || !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;
const gdkWindow = gtkLib.symbols.gtk_widget_get_window(windowPtr as any);
if (!gdkWindow) return false;
const xid = gdkLib.symbols.gdk_x11_window_get_xid(gdkWindow);
if (!xid) return false;
x11Lib.symbols.XMoveResizeWindow(
xDisplay, xid,
Math.round(x), Math.round(y),
Math.max(400, Math.round(width)),
Math.max(300, Math.round(height)),
);
x11Lib.symbols.XFlush(xDisplay);
return true;
} catch (err) {
console.error("[gtk-window] x11SetFrame failed:", err);
return false;
}
}
/**
* Delegate resize to the window manager.
* Releases X11 grabs first (SDL2 pattern) so WM can take the pointer.

View file

@ -161,6 +161,13 @@ const rpc = BrowserView.defineRPC<PtyRPCSchema>({
return { ok };
} catch (err) { console.error("[window.gtkSetFrame]", err); return { ok: false }; }
},
"window.x11SetFrame": ({ x, y, width, height }: { x: number; y: number; width: number; height: number }) => {
try {
const { x11SetFrame } = require("./gtk-window.ts");
const ok = x11SetFrame((mainWindow as any).ptr, x, y, width, height);
return { ok };
} catch (err) { console.error("[window.x11SetFrame]", err); return { ok: false }; }
},
},
messages: {},
},