diff --git a/electron/hudOverlayBounds.test.ts b/electron/hudOverlayBounds.test.ts index b45c9a56..728c6db2 100644 --- a/electron/hudOverlayBounds.test.ts +++ b/electron/hudOverlayBounds.test.ts @@ -21,19 +21,19 @@ describe("getHudOverlayWindowBounds", () => { it("uses a bottom-centered compact fallback when mouse passthrough is unavailable", () => { expect(getHudOverlayWindowBounds(workArea, false)).toEqual({ - x: 650, - y: 920, - width: 860, - height: 160, + x: 720, + y: 960, + width: 720, + height: 120, }); }); it("expands the non-passthrough fallback for HUD menus and hover interaction", () => { expect(getHudOverlayWindowBounds(workArea, false, true)).toEqual({ - x: 650, - y: 540, - width: 860, - height: 540, + x: 720, + y: 560, + width: 720, + height: 520, }); }); @@ -50,9 +50,9 @@ describe("getHudOverlayWindowBounds", () => { ), ).toEqual({ x: -100, - y: 280, + y: 320, width: 640, - height: 160, + height: 120, }); }); @@ -92,16 +92,16 @@ describe("resizeHudOverlayFallbackBounds", () => { { x: 420, y: 700, - width: 860, - height: 160, + width: 720, + height: 120, }, true, ), ).toEqual({ x: 420, - y: 320, - width: 860, - height: 540, + y: 300, + width: 720, + height: 520, }); }); @@ -111,17 +111,17 @@ describe("resizeHudOverlayFallbackBounds", () => { workArea, { x: 420, - y: 320, - width: 860, - height: 540, + y: 300, + width: 720, + height: 520, }, false, ), ).toEqual({ x: 420, y: 700, - width: 860, - height: 160, + width: 720, + height: 120, }); }); @@ -132,16 +132,16 @@ describe("resizeHudOverlayFallbackBounds", () => { { x: 1500, y: 900, - width: 860, - height: 160, + width: 720, + height: 120, }, true, ), ).toEqual({ - x: 1060, - y: 520, - width: 860, - height: 540, + x: 1200, + y: 500, + width: 720, + height: 520, }); }); }); diff --git a/electron/hudOverlayBounds.ts b/electron/hudOverlayBounds.ts index e56c3e8b..4835cabf 100644 --- a/electron/hudOverlayBounds.ts +++ b/electron/hudOverlayBounds.ts @@ -5,9 +5,9 @@ export interface HudOverlayWorkArea { height: number; } -const NON_PASSTHROUGH_HUD_WIDTH_DIP = 860; -const NON_PASSTHROUGH_HUD_COMPACT_HEIGHT_DIP = 160; -const NON_PASSTHROUGH_HUD_EXPANDED_HEIGHT_DIP = 540; +const NON_PASSTHROUGH_HUD_WIDTH_DIP = 720; +const NON_PASSTHROUGH_HUD_COMPACT_HEIGHT_DIP = 120; +const NON_PASSTHROUGH_HUD_EXPANDED_HEIGHT_DIP = 520; function clamp(value: number, min: number, max: number): number { return Math.min(Math.max(value, min), max); diff --git a/electron/windows.ts b/electron/windows.ts index 6a5abf67..a6b10fbd 100644 --- a/electron/windows.ts +++ b/electron/windows.ts @@ -1,6 +1,5 @@ import fs from "node:fs"; import { createRequire } from "node:module"; -import os from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { app, BrowserWindow, ipcMain } from "electron"; @@ -118,22 +117,15 @@ function isHudOverlayCaptureProtectionSupported(): boolean { return process.platform !== "linux"; } -function getWindowsBuildNumber(): number | null { - if (process.platform !== "win32") { - return null; - } - - const build = Number.parseInt(os.release().split(".")[2] ?? "", 10); - return Number.isFinite(build) ? build : null; -} - export function isHudOverlayMousePassthroughSupported(): boolean { if (process.platform === "linux") { return false; } - const build = getWindowsBuildNumber(); - if (build !== null && build < 22000) { + // Windows can forward transparent-window mouse events, but keeping a full-screen + // topmost HUD in that mode interferes with native popups and file pickers. + // Treat Windows as compact-HUD-only so the overlay never spans the desktop. + if (process.platform === "win32") { return false; } diff --git a/src/components/launch/LaunchWindow.tsx b/src/components/launch/LaunchWindow.tsx index 09cca63e..d82f3627 100644 --- a/src/components/launch/LaunchWindow.tsx +++ b/src/components/launch/LaunchWindow.tsx @@ -182,6 +182,7 @@ function LaunchWindowContent() { isHudDraggingRef, isWebcamPreviewDraggingRef, webcamPreviewDragStartRef, + useMousePassthroughTracking: hudOverlayMousePassthroughSupported !== false, }); useEffect(() => { diff --git a/src/components/launch/hooks/useLaunchHudInteractionState.ts b/src/components/launch/hooks/useLaunchHudInteractionState.ts index d7c086b1..e930fabe 100644 --- a/src/components/launch/hooks/useLaunchHudInteractionState.ts +++ b/src/components/launch/hooks/useLaunchHudInteractionState.ts @@ -5,31 +5,40 @@ export function useLaunchHudInteractionState({ isHudDraggingRef, isWebcamPreviewDraggingRef, webcamPreviewDragStartRef, + useMousePassthroughTracking, }: { openId: string | null; isHudDraggingRef: RefObject; isWebcamPreviewDraggingRef: RefObject; webcamPreviewDragStartRef: RefObject; + useMousePassthroughTracking: boolean; }) { const isMouseOverHudRef = useRef(false); const timeoutRef = useRef(null); const lastInteractiveReassertAtRef = useRef(0); - const setHudMouseInteractive = useCallback((force = false) => { - const now = performance.now(); - if ( - !force && - isMouseOverHudRef.current && - now - lastInteractiveReassertAtRef.current < 250 - ) { - return; - } + const setHudMouseInteractive = useCallback( + (force = false) => { + if (!useMousePassthroughTracking) { + return; + } + + const now = performance.now(); + if ( + !force && + isMouseOverHudRef.current && + now - lastInteractiveReassertAtRef.current < 250 + ) { + return; + } - isMouseOverHudRef.current = true; - lastInteractiveReassertAtRef.current = now; - if (timeoutRef.current) clearTimeout(timeoutRef.current); - window.electronAPI?.hudOverlaySetIgnoreMouse?.(false); - }, []); + isMouseOverHudRef.current = true; + lastInteractiveReassertAtRef.current = now; + if (timeoutRef.current) clearTimeout(timeoutRef.current); + window.electronAPI?.hudOverlaySetIgnoreMouse?.(false); + }, + [useMousePassthroughTracking], + ); useEffect(() => { if (openId !== null) { @@ -46,6 +55,10 @@ export function useLaunchHudInteractionState({ useEffect(() => { const handleMouseTracking = (e: globalThis.MouseEvent) => { + if (!useMousePassthroughTracking) { + return; + } + const target = e.target as HTMLElement | null; if (!target) return; const isInteractive = !!target.closest( @@ -70,6 +83,10 @@ export function useLaunchHudInteractionState({ } }; + if (!useMousePassthroughTracking) { + return; + } + window.addEventListener("mouseover", handleMouseTracking); window.addEventListener("mousemove", handleMouseTracking); return () => { @@ -80,6 +97,7 @@ export function useLaunchHudInteractionState({ isHudDraggingRef, isWebcamPreviewDraggingRef, setHudMouseInteractive, + useMousePassthroughTracking, webcamPreviewDragStartRef, ]); @@ -93,6 +111,10 @@ export function useLaunchHudInteractionState({ const handleHudMouseLeave = useCallback( (event: MouseEvent) => { + if (!useMousePassthroughTracking) { + return; + } + const nextTarget = event.relatedTarget; if (nextTarget instanceof Node && event.currentTarget.contains(nextTarget)) { return; @@ -113,7 +135,12 @@ export function useLaunchHudInteractionState({ } }, 300); }, - [isHudDraggingRef, isWebcamPreviewDraggingRef, webcamPreviewDragStartRef], + [ + isHudDraggingRef, + isWebcamPreviewDraggingRef, + useMousePassthroughTracking, + webcamPreviewDragStartRef, + ], ); return {