From 31338ad9496cfec5872ff21dffade55c8a680bee Mon Sep 17 00:00:00 2001 From: Hibryda Date: Wed, 25 Mar 2026 01:52:17 +0100 Subject: [PATCH] =?UTF-8?q?feat(electrobun):=20custom=20window=20chrome=20?= =?UTF-8?q?=E2=80=94=20no=20title=20bar,=20sidebar=20drag,=20edge=20resize?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - titleBarStyle: "hidden" removes native GTK decoration - Sidebar + right-bar: mousedown starts window drag (skips buttons) - 8 resize handles (N/S/E/W + 4 corners) with 4px hot zones - window.setFrame RPC for atomic position+size updates - Min window size: 600x400 - Cursor feedback: grab on sidebars, directional resize on edges - Frame persisted to SQLite on drag/resize end (debounced) --- ui-electrobun/src/bun/index.ts | 5 +- ui-electrobun/src/mainview/App.svelte | 106 +++++++++++++++++++++++++- 2 files changed, 106 insertions(+), 5 deletions(-) diff --git a/ui-electrobun/src/bun/index.ts b/ui-electrobun/src/bun/index.ts index bf7da04..0815e6b 100644 --- a/ui-electrobun/src/bun/index.ts +++ b/ui-electrobun/src/bun/index.ts @@ -120,6 +120,9 @@ const rpc = BrowserView.defineRPC({ "window.close": () => { try { mainWindow.close(); return { ok: true }; } catch (err) { console.error("[window.close]", err); return { ok: false }; } }, "window.getFrame": () => { try { return mainWindow.getFrame(); } catch { return { x: 0, y: 0, width: 1400, height: 900 }; } }, "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 { mainWindow.setFrame({ x, y, width, height }); return { ok: true }; } catch { return { ok: false }; } + }, }, messages: {}, }, @@ -181,7 +184,7 @@ const savedHeight = Number(settingsDb.getSetting("win_height") ?? 900); mainWindow = new BrowserWindow({ title: "Agent Orchestrator", - titleBarStyle: "default", + titleBarStyle: "hidden", url, rpc, frame: { diff --git a/ui-electrobun/src/mainview/App.svelte b/ui-electrobun/src/mainview/App.svelte index 65ef3c9..fea2b2a 100644 --- a/ui-electrobun/src/mainview/App.svelte +++ b/ui-electrobun/src/mainview/App.svelte @@ -91,6 +91,66 @@ appRpc.request["window.minimize"]({}).catch(console.error); } + // ── Window drag (on sidebar/right-bar empty space) ────────── + let isDragging = false; + let dragStartX = 0; + let dragStartY = 0; + let winStartX = 0; + let winStartY = 0; + + function onDragStart(e: MouseEvent) { + // Only start drag from the sidebars themselves, not child buttons + const target = e.target as HTMLElement; + if (target.tagName === 'BUTTON' || target.tagName === 'INPUT' || target.closest('button')) return; + isDragging = true; + dragStartX = e.screenX; + dragStartY = e.screenY; + appRpc.request['window.getFrame']({}).then(f => { + winStartX = f.x; winStartY = f.y; + }).catch(() => {}); + e.preventDefault(); + } + function onDragMove(e: MouseEvent) { + if (!isDragging) return; + const dx = e.screenX - dragStartX; + const dy = e.screenY - dragStartY; + appRpc.request['window.setPosition']({ x: winStartX + dx, y: winStartY + dy }).catch(() => {}); + } + function onDragEnd() { isDragging = false; saveWindowFrame(); } + + // ── Window resize (edge handles) ─────────────────────────── + let isResizing = false; + let resizeEdge = ''; + let resizeStartX = 0; + let resizeStartY = 0; + let resizeFrame = { x: 0, y: 0, width: 0, height: 0 }; + const MIN_W = 600; + const MIN_H = 400; + + function onResizeStart(e: MouseEvent, edge: string) { + isResizing = true; + resizeEdge = edge; + resizeStartX = e.screenX; + resizeStartY = e.screenY; + appRpc.request['window.getFrame']({}).then(f => { + resizeFrame = { ...f }; + }).catch(() => {}); + e.preventDefault(); + e.stopPropagation(); + } + function onResizeMove(e: MouseEvent) { + if (!isResizing) return; + const dx = e.screenX - resizeStartX; + const dy = e.screenY - resizeStartY; + let { x, y, width, height } = resizeFrame; + if (resizeEdge.includes('e')) width = Math.max(MIN_W, width + dx); + if (resizeEdge.includes('s')) height = Math.max(MIN_H, height + dy); + if (resizeEdge.includes('w')) { const nw = Math.max(MIN_W, width - dx); x = x + (width - nw); width = nw; } + if (resizeEdge.includes('n')) { const nh = Math.max(MIN_H, height - dy); y = y + (height - nh); height = nh; } + appRpc.request['window.setFrame']({ x, y, width, height }).catch(() => {}); + } + function onResizeEnd() { isResizing = false; saveWindowFrame(); } + // ── Window frame persistence (debounced 500ms) ────────────── let frameSaveTimer: ReturnType | null = null; function saveWindowFrame() { @@ -189,6 +249,12 @@ setAgentToastFn(showToast); setupErrorBoundary(); + // Window drag + resize global listeners + const handleGlobalMove = (e: MouseEvent) => { onDragMove(e); onResizeMove(e); }; + const handleGlobalUp = () => { onDragEnd(); onResizeEnd(); }; + window.addEventListener('mousemove', handleGlobalMove); + window.addEventListener('mouseup', handleGlobalUp); + // Blink + session timers — MUST be in onMount, NOT $effect // $effect interacts with reactive graph and causes cycles // Blink timer is in blink-store — start it here @@ -302,11 +368,29 @@ onClose={() => setNotifDrawerOpen(false)} /> + + +
onResizeStart(e, 'n')}>
+ +
onResizeStart(e, 's')}>
+ +
onResizeStart(e, 'e')}>
+ +
onResizeStart(e, 'w')}>
+ +
onResizeStart(e, 'ne')}>
+ +
onResizeStart(e, 'nw')}>
+ +
onResizeStart(e, 'se')}>
+ +
onResizeStart(e, 'sw')}>
+