feat(electrobun): custom window chrome — no title bar, sidebar drag, edge resize
- 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)
This commit is contained in:
parent
1de6c93e01
commit
31338ad949
2 changed files with 106 additions and 5 deletions
|
|
@ -120,6 +120,9 @@ const rpc = BrowserView.defineRPC<PtyRPCSchema>({
|
|||
"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: {
|
||||
|
|
|
|||
|
|
@ -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<typeof setTimeout> | 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)}
|
||||
/>
|
||||
|
||||
<!-- Resize handles (all edges + corners) -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div class="rz rz-n" onmousedown={(e) => onResizeStart(e, 'n')}></div>
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div class="rz rz-s" onmousedown={(e) => onResizeStart(e, 's')}></div>
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div class="rz rz-e" onmousedown={(e) => onResizeStart(e, 'e')}></div>
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div class="rz rz-w" onmousedown={(e) => onResizeStart(e, 'w')}></div>
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div class="rz rz-ne" onmousedown={(e) => onResizeStart(e, 'ne')}></div>
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div class="rz rz-nw" onmousedown={(e) => onResizeStart(e, 'nw')}></div>
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div class="rz rz-se" onmousedown={(e) => onResizeStart(e, 'se')}></div>
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div class="rz rz-sw" onmousedown={(e) => onResizeStart(e, 'sw')}></div>
|
||||
|
||||
<div class="app-shell" role="presentation">
|
||||
<!-- Left sidebar icon rail -->
|
||||
<aside class="sidebar" role="navigation" aria-label="Primary navigation">
|
||||
<!-- Left sidebar icon rail — draggable for window move -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<aside class="sidebar" role="navigation" aria-label="Primary navigation" onmousedown={onDragStart}>
|
||||
<!-- AGOR vertical title -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div class="agor-title" aria-hidden="true">AGOR</div>
|
||||
|
||||
<!-- Group icons — numbered circles -->
|
||||
|
|
@ -486,7 +570,8 @@
|
|||
</main>
|
||||
|
||||
<!-- Right sidebar: window controls + notification bell -->
|
||||
<aside class="right-bar" aria-label="Window controls and notifications">
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<aside class="right-bar" aria-label="Window controls and notifications" onmousedown={onDragStart}>
|
||||
<div
|
||||
class="window-controls-vertical"
|
||||
role="toolbar"
|
||||
|
|
@ -572,6 +657,17 @@
|
|||
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; }
|
||||
|
||||
.app-shell {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
|
|
@ -590,7 +686,9 @@
|
|||
align-items: center;
|
||||
padding: 0.375rem 0 0.5rem;
|
||||
gap: 0.125rem;
|
||||
cursor: grab;
|
||||
}
|
||||
.sidebar:active { cursor: grabbing; }
|
||||
|
||||
.agor-title {
|
||||
writing-mode: vertical-rl;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue