From 27bb3087ea8b0b9edfb151a858203dfbd67217e2 Mon Sep 17 00:00:00 2001 From: Filip Dvoran Date: Fri, 7 Aug 2026 13:09:15 +0200 Subject: [PATCH] fix(hud): stop the recording overlay from swallowing clicks While recording, the HUD overlay traded its full-work-area click-through window for a fixed 860x160 rectangle (860x540 with the webcam preview) anchored bottom-centre, with mouse events enabled on the whole window. Its transparent margins swallowed every click aimed at the app being recorded -- a Save button near the bottom of the screen simply did not respond. Dragging the bar only translated content inside that window, so moving the controls never freed the blocked area. The compact fallback exists because focus changes on Windows corrupt the WS_EX_TRANSPARENT flag behind setIgnoreMouseEvents forwarding, which would leave the stop button unclickable. Scope it to win32 and keep the click-through overlay everywhere else; macOS has no such failure mode and its renderer is already written for the full-work-area model. Also move the interactive box off the static column onto the transformed wrapper: transforms do not move layout boxes, so a pointer-events-auto parent kept eating clicks at the bar's original position after it was dragged away. Co-Authored-By: Claude Opus 5 (1M context) --- electron/hudOverlayBounds.test.ts | 27 ++++++++++++++++ electron/hudOverlayBounds.ts | 20 ++++++++++++ electron/windows.ts | 43 +++++++++++++++++++++----- src/components/launch/LaunchWindow.tsx | 14 ++++++--- 4 files changed, 92 insertions(+), 12 deletions(-) diff --git a/electron/hudOverlayBounds.test.ts b/electron/hudOverlayBounds.test.ts index db21e92bd..ecd823250 100644 --- a/electron/hudOverlayBounds.test.ts +++ b/electron/hudOverlayBounds.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest"; import { getHudOverlayWindowBounds, + recordingForcesHudOverlayFallback, resizeHudOverlayFallbackBounds, shouldExpandHudOverlayFallback, } from "./hudOverlayBounds"; @@ -145,6 +146,32 @@ describe("resizeHudOverlayFallbackBounds", () => { }); }); +describe("recordingForcesHudOverlayFallback", () => { + it("pins the HUD to the compact fallback while recording on Windows", () => { + expect( + recordingForcesHudOverlayFallback({ platform: "win32", recordingActive: true }), + ).toBe(true); + }); + + it("keeps the full-work-area click-through overlay while recording on macOS", () => { + expect( + recordingForcesHudOverlayFallback({ platform: "darwin", recordingActive: true }), + ).toBe(false); + }); + + it("keeps the full-work-area click-through overlay while recording on Linux", () => { + expect( + recordingForcesHudOverlayFallback({ platform: "linux", recordingActive: true }), + ).toBe(false); + }); + + it("never forces the fallback outside recording", () => { + expect( + recordingForcesHudOverlayFallback({ platform: "win32", recordingActive: false }), + ).toBe(false); + }); +}); + describe("shouldExpandHudOverlayFallback", () => { it("expands while recording only when the floating webcam preview is visible", () => { expect( diff --git a/electron/hudOverlayBounds.ts b/electron/hudOverlayBounds.ts index 8c51b88c7..cbd837790 100644 --- a/electron/hudOverlayBounds.ts +++ b/electron/hudOverlayBounds.ts @@ -13,6 +13,26 @@ function clamp(value: number, min: number, max: number): number { return Math.min(Math.max(value, min), max); } +/** + * While recording, Windows pins the HUD to a small, always-interactive window + * because focus changes there silently corrupt the WS_EX_TRANSPARENT flag that + * backs setIgnoreMouseEvents forwarding, which would leave the stop button + * unclickable. Every other platform keeps the full-work-area click-through + * overlay: shrinking it to a fixed rectangle turns the transparent margins + * around the bar into a dead zone that swallows clicks aimed at the app being + * recorded, and the bar can only be dragged inside that rectangle so moving it + * never frees the blocked area. + */ +export function recordingForcesHudOverlayFallback({ + platform, + recordingActive, +}: { + platform: string; + recordingActive: boolean; +}): boolean { + return recordingActive && platform === "win32"; +} + export function getHudOverlayWindowBounds( workArea: HudOverlayWorkArea, mousePassthroughSupported: boolean, diff --git a/electron/windows.ts b/electron/windows.ts index 55f6314cd..c37283ba3 100644 --- a/electron/windows.ts +++ b/electron/windows.ts @@ -7,6 +7,7 @@ import { app, BrowserWindow, ipcMain } from "electron"; import { USER_DATA_PATH } from "./appPaths"; import { getHudOverlayWindowBounds, + recordingForcesHudOverlayFallback, resizeHudOverlayFallbackBounds, shouldExpandHudOverlayFallback, } from "./hudOverlayBounds"; @@ -193,6 +194,13 @@ function getHudOverlayDisplay() { return getScreen().getPrimaryDisplay(); } +function recordingForcesHudFallback(): boolean { + return recordingForcesHudOverlayFallback({ + platform: process.platform, + recordingActive: hudOverlayRecordingActive, + }); +} + function getHudOverlayBounds() { const { workArea } = getHudOverlayDisplay(); const fallbackExpanded = shouldExpandHudOverlayFallback({ @@ -202,7 +210,7 @@ function getHudOverlayBounds() { }); return getHudOverlayWindowBounds( workArea, - isHudOverlayMousePassthroughSupported() && !hudOverlayRecordingActive, + isHudOverlayMousePassthroughSupported() && !recordingForcesHudFallback(), fallbackExpanded, ); } @@ -259,7 +267,7 @@ function positionUpdateToastWindow() { } function setHudOverlayFallbackExpanded(expanded: boolean) { - if (hudOverlayRecordingActive) { + if (recordingForcesHudFallback()) { hudOverlayFallbackExpanded = false; return; } @@ -290,7 +298,7 @@ function setHudOverlayMousePassthrough(ignore: boolean) { hudOverlayIgnoringMouse = hudOverlaySourceSelectionActive && !hudOverlayRecordingActive ? true - : hudOverlayRecordingActive + : recordingForcesHudFallback() ? false : ignore; @@ -303,7 +311,7 @@ function setHudOverlayMousePassthrough(ignore: boolean) { return; } - if (hudOverlayRecordingActive) { + if (recordingForcesHudFallback()) { hudOverlayFallbackExpanded = false; applyHudOverlayBounds(); hudOverlayWindow.setIgnoreMouseEvents(false); @@ -503,7 +511,7 @@ export function createHudOverlayWindow(): BrowserWindow { } if (isHudOverlayMousePassthroughSupported()) { - if (hudOverlayRecordingActive) { + if (recordingForcesHudFallback()) { hudOverlayIgnoringMouse = false; win.setIgnoreMouseEvents(false); } else { @@ -638,7 +646,7 @@ export function reassertHudOverlayMousePassthrough(): void { return; } - if (hudOverlayRecordingActive) { + if (recordingForcesHudFallback()) { hud.setIgnoreMouseEvents(false); return; } @@ -658,10 +666,31 @@ export function reassertHudOverlayMousePassthrough(): void { } export function setHudOverlayRecordingActive(recording: boolean): void { + const wasFallbackForced = recordingForcesHudFallback(); hudOverlayRecordingActive = Boolean(recording); hudOverlayFallbackExpanded = false; applyHudOverlayBounds(); - setHudOverlayMousePassthrough(!hudOverlayRecordingActive); + + if (recordingForcesHudFallback()) { + // Compact, always-interactive HUD window: the whole window is the bar. + setHudOverlayMousePassthrough(false); + return; + } + + if (wasFallbackForced) { + // Leaving the compact fallback re-expands the overlay to the whole work + // area, so it has to become click-through again before it swallows every + // click on the desktop. + setHudOverlayMousePassthrough(true); + return; + } + + // The overlay already spans the work area and stays click-through while + // recording; the renderer's hover tracking owns the interactive state, so + // preserve it instead of forcing the window interactive (which would block + // clicks everywhere) or click-through (which would drop a click already + // aimed at the bar). + setHudOverlayMousePassthrough(hudOverlayIgnoringMouse); } export function createUpdateToastWindow(): BrowserWindow { diff --git a/src/components/launch/LaunchWindow.tsx b/src/components/launch/LaunchWindow.tsx index 09cca63ef..a271d0898 100644 --- a/src/components/launch/LaunchWindow.tsx +++ b/src/components/launch/LaunchWindow.tsx @@ -451,13 +451,17 @@ function LaunchWindowContent() { ref={hudContentRef} className="flex items-center overflow-visible flex-col-reverse pointer-events-none" > -
+
+ {/* The interactive area has to sit on the transformed wrapper, not on + the static column above it: transforms do not move layout boxes, so + a pointer-events-auto parent would keep swallowing clicks at the + bar's original bottom-centre position after the bar is dragged + away. */}