diff --git a/ui-electrobun/src/bun/gtk-window.ts b/ui-electrobun/src/bun/gtk-window.ts index 7f5d9b4..c6481e6 100644 --- a/ui-electrobun/src/bun/gtk-window.ts +++ b/ui-electrobun/src/bun/gtk-window.ts @@ -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. diff --git a/ui-electrobun/src/bun/index.ts b/ui-electrobun/src/bun/index.ts index f413610..1db1f07 100644 --- a/ui-electrobun/src/bun/index.ts +++ b/ui-electrobun/src/bun/index.ts @@ -114,7 +114,9 @@ const rpc = BrowserView.defineRPC({ 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. diff --git a/ui-electrobun/src/mainview/App.svelte b/ui-electrobun/src/mainview/App.svelte index e48c97a..e98a80a 100644 --- a/ui-electrobun/src/mainview/App.svelte +++ b/ui-electrobun/src/mainview/App.svelte @@ -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;