fix(electrobun): enable window resize via recursive GTK min-size override

Root cause: WebKitWebView requests min_size = content_size (5120x1387 on
ultrawide), which GTK propagates to WM_NORMAL_HINTS, blocking all resize.

Fix: recursively walk the GTK widget tree (GtkWindow → GtkBin → WebView)
and call gtk_widget_set_size_request(-1, -1) on every widget. Then set
gtk_window_set_geometry_hints with min=400x300.

Result: WM_NORMAL_HINTS now shows min=10x10, window is fully resizable.
This commit is contained in:
Hibryda 2026-03-25 12:53:24 +01:00
parent 9da9d96ebd
commit d84feb6c67
3 changed files with 134 additions and 10 deletions

View file

@ -31,6 +31,46 @@ function getGtk() {
args: [FFIType.ptr, FFIType.i32, FFIType.i32, FFIType.i32, FFIType.u32],
returns: FFIType.void,
},
gtk_window_get_resizable: {
args: [FFIType.ptr],
returns: FFIType.bool,
},
gtk_window_set_resizable: {
args: [FFIType.ptr, FFIType.bool],
returns: FFIType.void,
},
// GtkWidget — controls minimum size request
gtk_widget_set_size_request: {
args: [FFIType.ptr, FFIType.i32, FFIType.i32],
returns: FFIType.void,
},
// GtkWindow — override geometry hints (min/max size sent to WM)
gtk_window_set_geometry_hints: {
args: [FFIType.ptr, FFIType.ptr, FFIType.ptr, FFIType.i32],
returns: FFIType.void,
},
// Container traversal
gtk_bin_get_child: {
args: [FFIType.ptr],
returns: FFIType.ptr,
},
gtk_container_get_children: {
args: [FFIType.ptr],
returns: FFIType.ptr, // GList*
},
// GList traversal
g_list_length: {
args: [FFIType.ptr],
returns: FFIType.u32,
},
g_list_nth_data: {
args: [FFIType.ptr, FFIType.u32],
returns: FFIType.ptr,
},
g_list_free: {
args: [FFIType.ptr],
returns: FFIType.void,
},
});
return gtk3;
} catch (err) {
@ -39,6 +79,82 @@ function getGtk() {
}
}
/**
* Ensure window is resizable at GTK level. Must be called after window creation.
*/
export function ensureResizable(windowPtr: number | bigint): boolean {
const lib = getGtk();
if (!lib) return false;
try {
const isResizable = lib.symbols.gtk_window_get_resizable(windowPtr as any);
console.log(`[gtk-window] gtk_window_get_resizable = ${isResizable}`);
if (!isResizable) {
console.log("[gtk-window] Window NOT resizable — forcing set_resizable(true)");
lib.symbols.gtk_window_set_resizable(windowPtr as any, true);
const nowResizable = lib.symbols.gtk_window_get_resizable(windowPtr as any);
console.log(`[gtk-window] After set_resizable(true): ${nowResizable}`);
}
// Override minimum size on the entire widget tree.
// WebKitWebView requests min_size = content_size, which GTK propagates
// to WM_NORMAL_HINTS, blocking resize. Fix: set -1 (no minimum) recursively.
console.log("[gtk-window] Resetting minimum size constraints on widget tree");
function clearMinSize(widget: any, depth: number) {
if (!widget || depth > 10) return;
lib!.symbols.gtk_widget_set_size_request(widget, -1, -1);
// Try as GtkBin (single child)
try {
const child = lib!.symbols.gtk_bin_get_child(widget);
if (child) {
console.log(`[gtk-window] ${" ".repeat(depth)}child (bin)`);
clearMinSize(child, depth + 1);
}
} catch { /* not a GtkBin */ }
// Try as GtkContainer (multiple children)
try {
const list = lib!.symbols.gtk_container_get_children(widget);
if (list) {
const len = lib!.symbols.g_list_length(list);
console.log(`[gtk-window] ${" ".repeat(depth)}container (${len} children)`);
for (let i = 0; i < len && i < 20; i++) {
const child = lib!.symbols.g_list_nth_data(list, i);
if (child) clearMinSize(child, depth + 1);
}
lib!.symbols.g_list_free(list);
}
} catch { /* not a GtkContainer */ }
}
clearMinSize(windowPtr as any, 0);
// Set geometry hints with small minimum via GdkGeometry struct
// GdkGeometry: min_width(i32), min_height(i32), ... (rest are padding)
// GdkWindowHints: GDK_HINT_MIN_SIZE = 1<<1 = 2
try {
// Allocate GdkGeometry struct (18 ints = 72 bytes)
const buf = new ArrayBuffer(72);
const view = new Int32Array(buf);
view[0] = 400; // min_width
view[1] = 300; // min_height
view[2] = 32767; // max_width
view[3] = 32767; // max_height
const GDK_HINT_MIN_SIZE = 2;
const GDK_HINT_MAX_SIZE = 4;
lib.symbols.gtk_window_set_geometry_hints(
windowPtr as any,
null, // widget = null → applies to window itself
ptr(buf) as any, // GdkGeometry*
GDK_HINT_MIN_SIZE | GDK_HINT_MAX_SIZE,
);
console.log("[gtk-window] Set geometry hints: min=400×300 max=32767×32767");
} catch (err) {
console.error("[gtk-window] geometry hints failed:", err);
}
return true;
} catch (err) {
console.error("[gtk-window] ensureResizable failed:", err);
return false;
}
}
/**
* Delegate resize to the window manager.
* The WM handles cursor, animation, constraints zero CPU from us.

View file

@ -114,7 +114,9 @@ const rpc = BrowserView.defineRPC<PtyRPCSchema>({
const { beginResizeDrag, edgeStringToGdk } = require("./gtk-window.ts");
const gdkEdge = edgeStringToGdk(edge);
if (gdkEdge === null) return { ok: false, error: `Unknown edge: ${edge}` };
console.log(`[resize] edge=${edge} gdkEdge=${gdkEdge} btn=${button} rootX=${rootX} rootY=${rootY} ptr=${(mainWindow as any).ptr}`);
const ok = beginResizeDrag((mainWindow as any).ptr, gdkEdge, button, rootX, rootY);
console.log(`[resize] result: ${ok}`);
return { ok };
} catch (err) { console.error("[window.beginResize]", err); return { ok: false }; }
},
@ -219,6 +221,12 @@ mainWindow = new BrowserWindow({
},
});
// Ensure GTK window is resizable (titleBarStyle:"hidden" may clear the flag)
{
const { ensureResizable } = require("./gtk-window.ts");
ensureResizable((mainWindow as any).ptr);
}
// Prevent GTK's false Ctrl+click detection from closing the window on initial load.
// WebKitGTK reports stale modifier state (0x14 = Ctrl+Alt) after SIGTERM of previous instance,
// which Electrobun interprets as a Cmd+click → "open in new window" → closes the main window.

View file

@ -618,16 +618,16 @@
height: 100vh;
}
/* ── Resize handles ─────────────────────────────────── */
.rz { position: fixed; z-index: 9999; }
.rz-n { top: 0; left: 4px; right: 4px; height: 4px; cursor: n-resize; }
.rz-s { bottom: 0; left: 4px; right: 4px; height: 4px; cursor: s-resize; }
.rz-e { right: 0; top: 4px; bottom: 4px; width: 4px; cursor: e-resize; }
.rz-w { left: 0; top: 4px; bottom: 4px; width: 4px; cursor: w-resize; }
.rz-ne { top: 0; right: 0; width: 8px; height: 8px; cursor: ne-resize; }
.rz-nw { top: 0; left: 0; width: 8px; height: 8px; cursor: nw-resize; }
.rz-se { bottom: 0; right: 0; width: 8px; height: 8px; cursor: se-resize; }
.rz-sw { bottom: 0; left: 0; width: 8px; height: 8px; cursor: sw-resize; }
/* ── Resize handles — 6px edges, 12px corners, fixed on viewport ── */
.rz { position: fixed; z-index: 9999; background: rgba(255,0,0,0.15); }
.rz-n { top: 0; left: 12px; right: 12px; height: 6px; cursor: n-resize; }
.rz-s { bottom: 0; left: 12px; right: 12px; height: 6px; cursor: s-resize; }
.rz-e { right: 0; top: 12px; bottom: 12px; width: 6px; cursor: e-resize; }
.rz-w { left: 0; top: 12px; bottom: 12px; width: 6px; cursor: w-resize; }
.rz-ne { top: 0; right: 0; width: 12px; height: 12px; cursor: ne-resize; }
.rz-nw { top: 0; left: 0; width: 12px; height: 12px; cursor: nw-resize; }
.rz-se { bottom: 0; right: 0; width: 12px; height: 12px; cursor: se-resize; }
.rz-sw { bottom: 0; left: 0; width: 12px; height: 12px; cursor: sw-resize; }
.app-shell {
flex: 1;