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

@ -104,20 +104,55 @@
e.preventDefault();
}
// ── Window resize — native GTK begin_resize_drag ────────
// ── Window resize — JS-based with X11 direct frame set ────────
let resizeEdge: string | null = null;
let resizeStartX = 0;
let resizeStartY = 0;
let resizeFrame = { x: 0, y: 0, width: 0, height: 0 };
const CURSOR_MAP: Record<string, string> = {
n: 'n-resize', s: 's-resize', e: 'e-resize', w: 'w-resize',
ne: 'ne-resize', nw: 'nw-resize', se: 'se-resize', sw: 'sw-resize',
};
function onResizeStart(e: MouseEvent, edge: string) {
// MUST stop propagation SYNCHRONOUSLY to prevent sidebar drag handler
e.preventDefault();
e.stopPropagation();
// Delegate to GTK window manager — handles cursor, animation, constraints
appRpc.request['window.beginResize']({
edge,
button: e.button + 1, // DOM: 0=left, GTK: 1=left
rootX: e.screenX,
rootY: e.screenY,
resizeEdge = edge;
resizeStartX = e.screenX;
resizeStartY = e.screenY;
document.body.style.cursor = CURSOR_MAP[edge] || 'default';
document.body.style.userSelect = 'none';
// Capture frame async — resize uses deltas so a slight delay is fine
appRpc.request['window.getFrame']({}).then((f: any) => {
resizeFrame = { x: f.x, y: f.y, width: f.width, height: f.height };
}).catch(() => {});
}
function onResizeMove(e: MouseEvent) {
if (!resizeEdge) return;
const dx = e.screenX - resizeStartX;
const dy = e.screenY - resizeStartY;
let { x, y, width, height } = resizeFrame;
const MIN_W = 400, MIN_H = 300;
if (resizeEdge.includes('e')) width = Math.max(MIN_W, width + dx);
if (resizeEdge.includes('w')) { const nw = Math.max(MIN_W, width - dx); x += width - nw; width = nw; }
if (resizeEdge.includes('s')) height = Math.max(MIN_H, height + dy);
if (resizeEdge.includes('n')) { const nh = Math.max(MIN_H, height - dy); y += height - nh; height = nh; }
// X11 direct — bypasses GTK size negotiation
appRpc.request['window.x11SetFrame']({
x: Math.round(x), y: Math.round(y),
width: Math.round(width), height: Math.round(height),
}).catch(() => {});
}
function onResizeEnd() {
if (!resizeEdge) return;
resizeEdge = null;
document.body.style.cursor = '';
document.body.style.userSelect = '';
saveWindowFrame();
}
// ── Window frame persistence (debounced 500ms) ──────────────
let frameSaveTimer: ReturnType<typeof setTimeout> | null = null;
function saveWindowFrame() {
@ -216,7 +251,9 @@
setAgentToastFn(showToast);
setupErrorBoundary();
// Native GTK begin_resize_drag handles resize — no JS mousemove needed
// JS resize uses document-level listeners
document.addEventListener('mousemove', onResizeMove);
document.addEventListener('mouseup', onResizeEnd);
// Blink + session timers — MUST be in onMount, NOT $effect
// $effect interacts with reactive graph and causes cycles
@ -312,7 +349,8 @@
clearInterval(sessionId);
document.removeEventListener("keydown", handleSearchShortcut);
window.removeEventListener("palette-command", handlePaletteCommand);
// no resize listeners to clean up — native GTK handles it
document.removeEventListener('mousemove', onResizeMove);
document.removeEventListener('mouseup', onResizeEnd);
};
});
</script>