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:
parent
058ae563d5
commit
0e6408a447
3 changed files with 98 additions and 10 deletions
|
|
@ -27,6 +27,7 @@ function getX11() {
|
||||||
x11 = dlopen("libX11.so.6", {
|
x11 = dlopen("libX11.so.6", {
|
||||||
XUngrabPointer: { args: [FFIType.ptr, FFIType.u64], returns: FFIType.i32 },
|
XUngrabPointer: { args: [FFIType.ptr, FFIType.u64], returns: FFIType.i32 },
|
||||||
XFlush: { args: [FFIType.ptr], 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;
|
return x11;
|
||||||
} catch { return null; }
|
} catch { return null; }
|
||||||
|
|
@ -38,6 +39,7 @@ function getGdk() {
|
||||||
gdk3 = dlopen("libgdk-3.so.0", {
|
gdk3 = dlopen("libgdk-3.so.0", {
|
||||||
gdk_display_get_default: { args: [], returns: FFIType.ptr },
|
gdk_display_get_default: { args: [], returns: FFIType.ptr },
|
||||||
gdk_x11_display_get_xdisplay: { args: [FFIType.ptr], 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;
|
return gdk3;
|
||||||
} catch { return null; }
|
} catch { return null; }
|
||||||
|
|
@ -151,6 +153,11 @@ function getGtk() {
|
||||||
args: [FFIType.ptr],
|
args: [FFIType.ptr],
|
||||||
returns: FFIType.void,
|
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)
|
// Sensitivity (disable input processing on widget)
|
||||||
gtk_widget_set_sensitive: {
|
gtk_widget_set_sensitive: {
|
||||||
args: [FFIType.ptr, FFIType.bool],
|
args: [FFIType.ptr, FFIType.bool],
|
||||||
|
|
@ -266,6 +273,42 @@ function forceSmallMinSize(lib: NonNullable<typeof gtk3>, windowPtr: any) {
|
||||||
} catch { /* ignore */ }
|
} 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.
|
* Delegate resize to the window manager.
|
||||||
* Releases X11 grabs first (SDL2 pattern) so WM can take the pointer.
|
* Releases X11 grabs first (SDL2 pattern) so WM can take the pointer.
|
||||||
|
|
|
||||||
|
|
@ -161,6 +161,13 @@ const rpc = BrowserView.defineRPC<PtyRPCSchema>({
|
||||||
return { ok };
|
return { ok };
|
||||||
} catch (err) { console.error("[window.gtkSetFrame]", err); return { ok: false }; }
|
} 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: {},
|
messages: {},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -104,20 +104,55 @@
|
||||||
e.preventDefault();
|
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) {
|
function onResizeStart(e: MouseEvent, edge: string) {
|
||||||
// MUST stop propagation SYNCHRONOUSLY to prevent sidebar drag handler
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
// Delegate to GTK window manager — handles cursor, animation, constraints
|
resizeEdge = edge;
|
||||||
appRpc.request['window.beginResize']({
|
resizeStartX = e.screenX;
|
||||||
edge,
|
resizeStartY = e.screenY;
|
||||||
button: e.button + 1, // DOM: 0=left, GTK: 1=left
|
document.body.style.cursor = CURSOR_MAP[edge] || 'default';
|
||||||
rootX: e.screenX,
|
document.body.style.userSelect = 'none';
|
||||||
rootY: e.screenY,
|
// 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(() => {});
|
}).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) ──────────────
|
// ── Window frame persistence (debounced 500ms) ──────────────
|
||||||
let frameSaveTimer: ReturnType<typeof setTimeout> | null = null;
|
let frameSaveTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
function saveWindowFrame() {
|
function saveWindowFrame() {
|
||||||
|
|
@ -216,7 +251,9 @@
|
||||||
setAgentToastFn(showToast);
|
setAgentToastFn(showToast);
|
||||||
setupErrorBoundary();
|
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
|
// Blink + session timers — MUST be in onMount, NOT $effect
|
||||||
// $effect interacts with reactive graph and causes cycles
|
// $effect interacts with reactive graph and causes cycles
|
||||||
|
|
@ -312,7 +349,8 @@
|
||||||
clearInterval(sessionId);
|
clearInterval(sessionId);
|
||||||
document.removeEventListener("keydown", handleSearchShortcut);
|
document.removeEventListener("keydown", handleSearchShortcut);
|
||||||
window.removeEventListener("palette-command", handlePaletteCommand);
|
window.removeEventListener("palette-command", handlePaletteCommand);
|
||||||
// no resize listeners to clean up — native GTK handles it
|
document.removeEventListener('mousemove', onResizeMove);
|
||||||
|
document.removeEventListener('mouseup', onResizeEnd);
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue