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. */}