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:
parent
9da9d96ebd
commit
d84feb6c67
3 changed files with 134 additions and 10 deletions
|
|
@ -31,6 +31,46 @@ function getGtk() {
|
||||||
args: [FFIType.ptr, FFIType.i32, FFIType.i32, FFIType.i32, FFIType.u32],
|
args: [FFIType.ptr, FFIType.i32, FFIType.i32, FFIType.i32, FFIType.u32],
|
||||||
returns: FFIType.void,
|
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;
|
return gtk3;
|
||||||
} catch (err) {
|
} 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.
|
* Delegate resize to the window manager.
|
||||||
* The WM handles cursor, animation, constraints — zero CPU from us.
|
* The WM handles cursor, animation, constraints — zero CPU from us.
|
||||||
|
|
|
||||||
|
|
@ -114,7 +114,9 @@ const rpc = BrowserView.defineRPC<PtyRPCSchema>({
|
||||||
const { beginResizeDrag, edgeStringToGdk } = require("./gtk-window.ts");
|
const { beginResizeDrag, edgeStringToGdk } = require("./gtk-window.ts");
|
||||||
const gdkEdge = edgeStringToGdk(edge);
|
const gdkEdge = edgeStringToGdk(edge);
|
||||||
if (gdkEdge === null) return { ok: false, error: `Unknown edge: ${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);
|
const ok = beginResizeDrag((mainWindow as any).ptr, gdkEdge, button, rootX, rootY);
|
||||||
|
console.log(`[resize] result: ${ok}`);
|
||||||
return { ok };
|
return { ok };
|
||||||
} catch (err) { console.error("[window.beginResize]", err); return { ok: false }; }
|
} 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.
|
// 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,
|
// 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.
|
// which Electrobun interprets as a Cmd+click → "open in new window" → closes the main window.
|
||||||
|
|
|
||||||
|
|
@ -618,16 +618,16 @@
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Resize handles ─────────────────────────────────── */
|
/* ── Resize handles — 6px edges, 12px corners, fixed on viewport ── */
|
||||||
.rz { position: fixed; z-index: 9999; }
|
.rz { position: fixed; z-index: 9999; background: rgba(255,0,0,0.15); }
|
||||||
.rz-n { top: 0; left: 4px; right: 4px; height: 4px; cursor: n-resize; }
|
.rz-n { top: 0; left: 12px; right: 12px; height: 6px; cursor: n-resize; }
|
||||||
.rz-s { bottom: 0; left: 4px; right: 4px; height: 4px; cursor: s-resize; }
|
.rz-s { bottom: 0; left: 12px; right: 12px; height: 6px; cursor: s-resize; }
|
||||||
.rz-e { right: 0; top: 4px; bottom: 4px; width: 4px; cursor: e-resize; }
|
.rz-e { right: 0; top: 12px; bottom: 12px; width: 6px; cursor: e-resize; }
|
||||||
.rz-w { left: 0; top: 4px; bottom: 4px; width: 4px; cursor: w-resize; }
|
.rz-w { left: 0; top: 12px; bottom: 12px; width: 6px; cursor: w-resize; }
|
||||||
.rz-ne { top: 0; right: 0; width: 8px; height: 8px; cursor: ne-resize; }
|
.rz-ne { top: 0; right: 0; width: 12px; height: 12px; cursor: ne-resize; }
|
||||||
.rz-nw { top: 0; left: 0; width: 8px; height: 8px; cursor: nw-resize; }
|
.rz-nw { top: 0; left: 0; width: 12px; height: 12px; cursor: nw-resize; }
|
||||||
.rz-se { bottom: 0; right: 0; width: 8px; height: 8px; cursor: se-resize; }
|
.rz-se { bottom: 0; right: 0; width: 12px; height: 12px; cursor: se-resize; }
|
||||||
.rz-sw { bottom: 0; left: 0; width: 8px; height: 8px; cursor: sw-resize; }
|
.rz-sw { bottom: 0; left: 0; width: 12px; height: 12px; cursor: sw-resize; }
|
||||||
|
|
||||||
.app-shell {
|
.app-shell {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue