From d1583f8ce43cf3dc5265a431678660c781f2a864 Mon Sep 17 00:00:00 2001 From: Hibryda Date: Wed, 25 Mar 2026 13:24:41 +0100 Subject: [PATCH] fix(electrobun): set_size_request(1,1) not (-1,-1) + revert to begin_resize_drag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex review found: set_size_request(-1,-1) means "use preferred size" which RE-ENABLES WebView content-based minimum. Using (1,1) FORCES a 1x1 minimum, actually overriding the preferred size. Reverted to native begin_resize_drag (WM handles resize smoothly). Fixed onResizeStart sync: e.stopPropagation() now runs BEFORE any async work, preventing sidebar drag handler from intercepting. Removed JS mousemove resize loop — native GTK resize is correct approach. --- ui-electrobun/src/bun/gtk-window.ts | 74 ++++++--------------------- ui-electrobun/src/mainview/App.svelte | 69 +++++-------------------- 2 files changed, 27 insertions(+), 116 deletions(-) diff --git a/ui-electrobun/src/bun/gtk-window.ts b/ui-electrobun/src/bun/gtk-window.ts index 7a4b989..1b0ecc1 100644 --- a/ui-electrobun/src/bun/gtk-window.ts +++ b/ui-electrobun/src/bun/gtk-window.ts @@ -102,60 +102,9 @@ export function ensureResizable(windowPtr: number | bigint): boolean { 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); - } + // Force small min-size on entire widget tree (1×1 per widget, 400×300 via geometry hints) + console.log("[gtk-window] Forcing small min-size on widget tree"); + forceSmallMinSize(lib, windowPtr as any); return true; } catch (err) { console.error("[gtk-window] ensureResizable failed:", err); @@ -168,10 +117,17 @@ export function ensureResizable(windowPtr: number | bigint): boolean { * WebKitWebView re-propagates content size as minimum on every layout cycle, * so we must clear it each time — not just at init. */ -function clearMinSizeTree(lib: NonNullable, windowPtr: any) { +/** + * Force small minimum size on the entire widget tree. + * Key insight (from Codex review): set_size_request(-1, -1) means "use preferred size" + * which RE-ENABLES the WebView's content-based minimum. Using set_size_request(1, 1) + * FORCES a 1×1 minimum, overriding the preferred size. + */ +function forceSmallMinSize(lib: NonNullable, windowPtr: any) { function walk(widget: any, depth: number) { if (!widget || depth > 10) return; - lib.symbols.gtk_widget_set_size_request(widget, -1, -1); + // Force 1x1 minimum — NOT -1,-1 which means "use preferred size" + lib.symbols.gtk_widget_set_size_request(widget, 1, 1); try { const child = lib.symbols.gtk_bin_get_child(widget); if (child) walk(child, depth + 1); @@ -189,7 +145,7 @@ function clearMinSizeTree(lib: NonNullable, windowPtr: any) { } catch { /* not a GtkContainer */ } } walk(windowPtr, 0); - // Re-apply geometry hints with small minimum + // Set geometry hints: min=400×300 (our real minimum), max=32767×32767 try { const buf = new ArrayBuffer(72); const view = new Int32Array(buf); @@ -213,7 +169,7 @@ export function beginResizeDrag( if (!lib) return false; try { // Clear min-size RIGHT BEFORE resize so shrinking is allowed - clearMinSizeTree(lib, windowPtr as any); + forceSmallMinSize(lib, windowPtr as any); lib.symbols.gtk_window_begin_resize_drag( windowPtr as any, edge, @@ -267,7 +223,7 @@ export function gtkSetFrame( if (!lib) return false; try { // Clear min-size on every frame update during resize - clearMinSizeTree(lib, windowPtr as any); + forceSmallMinSize(lib, windowPtr as any); lib.symbols.gtk_window_resize(windowPtr as any, Math.max(400, Math.round(width)), Math.max(300, Math.round(height))); lib.symbols.gtk_window_move(windowPtr as any, Math.round(x), Math.round(y)); return true; diff --git a/ui-electrobun/src/mainview/App.svelte b/ui-electrobun/src/mainview/App.svelte index 787b36b..b930d1c 100644 --- a/ui-electrobun/src/mainview/App.svelte +++ b/ui-electrobun/src/mainview/App.svelte @@ -104,60 +104,18 @@ e.preventDefault(); } - // ── Window resize — JS-based with GTK min-size clear ──────── - 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 = { - n: 'n-resize', s: 's-resize', e: 'e-resize', w: 'w-resize', - ne: 'ne-resize', nw: 'nw-resize', se: 'se-resize', sw: 'sw-resize', - }; - - async function onResizeStart(e: MouseEvent, edge: string) { - // Clear min-size constraints first (enables shrink) - appRpc.request['window.clearMinSize']({}).catch(() => {}); - // Capture current frame BEFORE starting resize - try { - const frame = await appRpc.request['window.getFrame']({}); - resizeFrame = { x: frame.x, y: frame.y, width: frame.width, height: frame.height }; - } catch { - // fallback — use defaults - resizeFrame = { x: 100, y: 100, width: 1400, height: 900 }; - } - resizeEdge = edge; - resizeStartX = e.screenX; - resizeStartY = e.screenY; - // Lock cursor during resize - document.body.style.cursor = CURSOR_MAP[edge] || 'default'; - document.body.style.userSelect = 'none'; + // ── Window resize — native GTK begin_resize_drag ──────── + function onResizeStart(e: MouseEvent, edge: string) { + // MUST stop propagation SYNCHRONOUSLY to prevent sidebar drag handler e.preventDefault(); e.stopPropagation(); - } - - 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; } - - // Use GTK FFI directly — bypasses Electrobun's setSize which respects WebView min-size - appRpc.request['window.gtkSetFrame']({ 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(); + // 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, + }).catch(() => {}); } // ── Window frame persistence (debounced 500ms) ────────────── @@ -258,9 +216,7 @@ setAgentToastFn(showToast); setupErrorBoundary(); - // JS-based resize needs global mouse listeners - document.addEventListener('mousemove', onResizeMove); - document.addEventListener('mouseup', onResizeEnd); + // Native GTK begin_resize_drag handles resize — no JS mousemove needed // Blink + session timers — MUST be in onMount, NOT $effect // $effect interacts with reactive graph and causes cycles @@ -356,8 +312,7 @@ clearInterval(sessionId); document.removeEventListener("keydown", handleSearchShortcut); window.removeEventListener("palette-command", handlePaletteCommand); - document.removeEventListener('mousemove', onResizeMove); - document.removeEventListener('mouseup', onResizeEnd); + // no resize listeners to clean up — native GTK handles it }; });