feat(electrobun): native GTK drag/resize via gtk_window_begin_resize_drag

- gtk-window.ts: FFI wrapper calling libgtk-3.so.0 directly via bun:ffi
- begin_resize_drag: delegates resize to window manager (zero CPU, smooth)
- begin_move_drag: delegates move to window manager (replaces JS drag)
- Removed all JavaScript-based drag/resize logic (no mousemove/mouseup)
- RPC: window.beginResize + window.beginMove
- Resize handles: 4px edges + 8px corners with proper cursors
This commit is contained in:
Hibryda 2026-03-25 02:23:24 +01:00
parent 48d32f6f28
commit 9da9d96ebd
4 changed files with 149 additions and 72 deletions

View file

@ -91,80 +91,30 @@
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;
// ── Window drag — delegates to GTK window manager ───────────
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;
winStartX = cachedFrame.x;
winStartY = cachedFrame.y;
// Delegate to GTK — the WM handles everything (smooth, zero CPU)
appRpc.request['window.beginMove']({
button: e.button + 1, // DOM: 0=left, GTK: 1=left
rootX: e.screenX,
rootY: e.screenY,
}).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() { if (isDragging) { updateCachedFrame(); saveWindowFrame(); } isDragging = false; }
// ── Window resize (edge handles) ───────────────────────────
let isResizing = false;
let resizeReady = false;
let resizeEdge = '';
let resizeStartX = 0;
let resizeStartY = 0;
let resizeFrame = { x: 0, y: 0, width: 0, height: 0 };
// Cache last known frame to avoid async race
let cachedFrame = { x: 100, y: 100, width: 1400, height: 900 };
const MIN_W = 600;
const MIN_H = 400;
// Keep cached frame updated on drag/resize end
function updateCachedFrame() {
appRpc.request['window.getFrame']({}).then(f => { cachedFrame = { ...f }; }).catch(() => {});
}
// ── Window resize — delegates to GTK window manager ────────
function onResizeStart(e: MouseEvent, edge: string) {
resizeEdge = edge;
resizeStartX = e.screenX;
resizeStartY = e.screenY;
resizeFrame = { ...cachedFrame };
isResizing = true;
resizeReady = true;
appRpc.request['window.beginResize']({
edge,
button: e.button + 1,
rootX: e.screenX,
rootY: e.screenY,
}).catch(() => {});
e.preventDefault();
e.stopPropagation();
}
let resizeThrottleId = 0;
function onResizeMove(e: MouseEvent) {
if (!isResizing || !resizeReady) return;
// Throttle to ~30fps to avoid overwhelming GTK
const now = Date.now();
if (now - resizeThrottleId < 33) return;
resizeThrottleId = now;
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() {
if (isResizing) { updateCachedFrame(); saveWindowFrame(); }
isResizing = false;
resizeReady = false;
}
// ── Window frame persistence (debounced 500ms) ──────────────
let frameSaveTimer: ReturnType<typeof setTimeout> | null = null;
@ -264,14 +214,7 @@
setAgentToastFn(showToast);
setupErrorBoundary();
// Seed cached frame from actual window position
updateCachedFrame();
// 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);
// No global mousemove/mouseup needed — GTK WM handles drag/resize natively
// Blink + session timers — MUST be in onMount, NOT $effect
// $effect interacts with reactive graph and causes cycles