From a78f7d45d4d71eb66c4ca6bac9b37854ab78f161 Mon Sep 17 00:00:00 2001 From: Andrew Foster <44451818+afostr@users.noreply.github.com> Date: Sat, 11 Apr 2026 09:23:37 -0500 Subject: [PATCH] fix(hud): preserve user-dragged position across recording state changes The HUD overlay snaps back to the centered default position whenever recording starts because the idle-to-recording UI swap triggers a resize, and applyHudOverlayBounds() always recomputes a centered location. This is disruptive when the user has intentionally moved the bar out of the way before a timed recording. Remember the position after a drag ends and reuse it for subsequent bounds updates, clamped to the current work area. The position resets on app restart or when displays change so the bar cannot get stranded off-screen. --- electron/windows.ts | 50 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/electron/windows.ts b/electron/windows.ts index 5e52e92e..78c73f5e 100644 --- a/electron/windows.ts +++ b/electron/windows.ts @@ -210,7 +210,25 @@ function applyHudOverlayBounds(expanded: boolean) { hudOverlayExpanded = expanded; - hudOverlayWindow.setBounds(getHudOverlayBounds(expanded), false); + const computed = getHudOverlayBounds(expanded); + + if (hudUserPosition) { + // Resize in-place at the user's dragged position, clamped so the + // window stays fully within the current display's work area. + const { workArea } = getHudOverlayDisplay(); + const x = Math.max( + workArea.x, + Math.min(hudUserPosition.x, workArea.x + workArea.width - computed.width), + ); + const y = Math.max( + workArea.y, + Math.min(hudUserPosition.y, workArea.y + workArea.height - computed.height), + ); + hudOverlayWindow.setBounds({ x, y, width: computed.width, height: computed.height }, false); + } else { + hudOverlayWindow.setBounds(computed, false); + } + positionUpdateToastWindow(); if (!hudOverlayWindow.isVisible()) { return; @@ -272,6 +290,10 @@ ipcMain.on("hud-overlay-set-ignore-mouse", (_event, ignore: boolean) => { } }); +// When the user drags the HUD, remember their chosen position so that +// subsequent size changes (e.g. idle → recording UI swap) resize in-place +// instead of snapping back to the default centered location. +let hudUserPosition: { x: number; y: number } | null = null; let hudDragOffset: { x: number; y: number } | null = null; let hudDragLastCursor: { x: number; y: number } | null = null; let hudDragFixedSize: { width: number; height: number } | null = null; @@ -304,6 +326,9 @@ ipcMain.on("hud-overlay-drag", (_event, phase: string, screenX: number, screenY: false, ); } else if (phase === "end") { + const finalBounds = hudOverlayWindow.getBounds(); + hudUserPosition = { x: finalBounds.x, y: finalBounds.y }; + hudDragOffset = null; hudDragLastCursor = null; hudDragFixedSize = null; @@ -477,6 +502,29 @@ export function createHudOverlayWindow(): BrowserWindow { } }); + // Reset the user's saved HUD position when displays change so the bar + // doesn't end up stranded off-screen after a monitor is disconnected. + const screen = getScreen(); + screen.on("display-removed", () => { + hudUserPosition = null; + }); + screen.on("display-metrics-changed", () => { + if (hudUserPosition) { + const displays = screen.getAllDisplays(); + const onScreen = displays.some( + (d) => + hudUserPosition!.x >= d.workArea.x && + hudUserPosition!.x < d.workArea.x + d.workArea.width && + hudUserPosition!.y >= d.workArea.y && + hudUserPosition!.y < d.workArea.y + d.workArea.height, + ); + if (!onScreen) { + hudUserPosition = null; + applyHudOverlayBounds(hudOverlayExpanded); + } + } + }); + if (VITE_DEV_SERVER_URL) { win.loadURL(VITE_DEV_SERVER_URL + "?windowType=hud-overlay"); } else {