From e6635e436c9ba4758e05efaef41fb441802128b5 Mon Sep 17 00:00:00 2001 From: Hibryda Date: Wed, 25 Mar 2026 13:09:15 +0100 Subject: [PATCH] fix(electrobun): JS-based window resize replaces GTK begin_resize_drag GTK begin_resize_drag loses grip when cursor moves inward past the 6px handle zone. Replaced with document-level mousemove/mouseup listeners that compute delta from initial frame and call setPosition+setSize. - clearMinSize RPC clears WebView min-size before resize starts - Cursor locks to resize direction during drag (body.style.cursor) - user-select disabled during drag to prevent text selection - Frame captured async before resize starts (no race condition) --- ui-electrobun/src/bun/index.ts | 9 +++- ui-electrobun/src/mainview/App.svelte | 65 +++++++++++++++++++++++---- 2 files changed, 63 insertions(+), 11 deletions(-) diff --git a/ui-electrobun/src/bun/index.ts b/ui-electrobun/src/bun/index.ts index 1db1f07..441e4e3 100644 --- a/ui-electrobun/src/bun/index.ts +++ b/ui-electrobun/src/bun/index.ts @@ -142,13 +142,18 @@ const rpc = BrowserView.defineRPC({ "window.setPosition": ({ x, y }: { x: number; y: number }) => { try { mainWindow.setPosition(x, y); return { ok: true }; } catch { return { ok: false }; } }, "window.setFrame": ({ x, y, width, height }: { x: number; y: number; width: number; height: number }) => { try { - // Use separate setPosition + setSize — setFrame's C implementation - // may not resize on GTK with titleBarStyle:"hidden" mainWindow.setPosition(x, y); mainWindow.setSize(width, height); return { ok: true }; } catch (err) { console.error("[window.setFrame]", err); return { ok: false }; } }, + "window.clearMinSize": () => { + try { + const { ensureResizable } = require("./gtk-window.ts"); + ensureResizable((mainWindow as any).ptr); + return { ok: true }; + } catch (err) { console.error("[window.clearMinSize]", err); return { ok: false }; } + }, }, messages: {}, }, diff --git a/ui-electrobun/src/mainview/App.svelte b/ui-electrobun/src/mainview/App.svelte index 6262a25..daba3f5 100644 --- a/ui-electrobun/src/mainview/App.svelte +++ b/ui-electrobun/src/mainview/App.svelte @@ -104,18 +104,61 @@ e.preventDefault(); } - // ── Window resize — delegates to GTK window manager ──────── - function onResizeStart(e: MouseEvent, edge: string) { - appRpc.request['window.beginResize']({ - edge, - button: e.button + 1, - rootX: e.screenX, - rootY: e.screenY, - }).catch(() => {}); + // ── 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'; 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; } + + appRpc.request['window.setFrame']({ 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) ────────────── let frameSaveTimer: ReturnType | null = null; function saveWindowFrame() { @@ -214,7 +257,9 @@ setAgentToastFn(showToast); setupErrorBoundary(); - // No global mousemove/mouseup needed — GTK WM handles drag/resize natively + // JS-based resize needs global mouse listeners + document.addEventListener('mousemove', onResizeMove); + document.addEventListener('mouseup', onResizeEnd); // Blink + session timers — MUST be in onMount, NOT $effect // $effect interacts with reactive graph and causes cycles @@ -310,6 +355,8 @@ clearInterval(sessionId); document.removeEventListener("keydown", handleSearchShortcut); window.removeEventListener("palette-command", handlePaletteCommand); + document.removeEventListener('mousemove', onResizeMove); + document.removeEventListener('mouseup', onResizeEnd); }; });