From 13e3d2bc249d4a94b8b96ea010e6688d757437f3 Mon Sep 17 00:00:00 2001 From: Alan Trebugeais Date: Sat, 9 May 2026 23:42:49 +0200 Subject: [PATCH 1/2] fix: mouse passthrough on windows and other oses --- electron/ipc/register/sources.ts | 22 ++++++++-- electron/windows.ts | 31 ++++++++++++++ .../hooks/useLaunchHudInteractionState.ts | 41 +++++++++++++++---- .../popovers/LaunchPopoverCoordinator.tsx | 8 +++- 4 files changed, 90 insertions(+), 12 deletions(-) diff --git a/electron/ipc/register/sources.ts b/electron/ipc/register/sources.ts index 0ce77fcb..4ba6b35a 100644 --- a/electron/ipc/register/sources.ts +++ b/electron/ipc/register/sources.ts @@ -13,6 +13,7 @@ import { resolveLinuxWindowBounds, stopWindowBoundsCapture, } from "../cursor/bounds"; +import { reassertHudOverlayMousePassthrough } from "../../windows"; const execFileAsync = promisify(execFile); const SOURCE_LIST_CACHE_TTL_MS = 1200; @@ -487,11 +488,24 @@ body{background:transparent;overflow:hidden;width:100vw;height:100vh} throw loadError } - setTimeout(() => { - if (!highlightWin.isDestroyed()) highlightWin.close() - }, 1700) + // The highlight window appearing (even with focusable:false) can corrupt + // the WS_EX_TRANSPARENT flag on the HUD on Windows 11+, breaking hover + // detection until the user moves their mouse over the bar again. + // Re-assert passthrough immediately so click-through is restored at once. + reassertHudOverlayMousePassthrough(); + + const highlightCloseTimer = setTimeout(() => { + if (!highlightWin.isDestroyed()) highlightWin.close() + }, 1700) + + highlightWin.on("closed", () => { + clearTimeout(highlightCloseTimer); + // Re-assert once more when the window is actually destroyed so the + // native flag is clean regardless of timing. + reassertHudOverlayMousePassthrough(); + }); - return { success: true } + return { success: true } } catch (error) { console.error('Failed to show source highlight:', error) return { success: false } diff --git a/electron/windows.ts b/electron/windows.ts index f1b448b6..550a4561 100644 --- a/electron/windows.ts +++ b/electron/windows.ts @@ -373,6 +373,7 @@ export function createHudOverlayWindow(): BrowserWindow { skipTaskbar: true, hasShadow: false, show: false, + focusable: false, webPreferences: { preload: path.join(electronWindowsDir, "preload.mjs"), nodeIntegration: false, @@ -513,6 +514,36 @@ export function getHudOverlayWindow(): BrowserWindow | null { return hudOverlayWindow && !hudOverlayWindow.isDestroyed() ? hudOverlayWindow : null; } +/** + * Re-initialise the HUD overlay's mouse passthrough state. + * + * On Windows 11+, any new BrowserWindow appearing (even focusable:false ones + * like the source highlight overlay) can silently corrupt the + * WS_EX_TRANSPARENT flag that backs setIgnoreMouseEvents forwarding. Call + * this after any operation that creates or destroys a sibling window so that + * hover detection on the HUD is immediately restored without requiring the + * user to move their mouse over the bar. + */ +export function reassertHudOverlayMousePassthrough(): void { + if (process.platform !== "win32" || !isHudOverlayMousePassthroughSupported()) { + return; + } + + const hud = getHudOverlayWindow(); + if (!hud) { + return; + } + + // Toggle off then back on so the native WS_EX_TRANSPARENT flag is fully + // re-initialised rather than merely re-asserted in a potentially broken state. + hud.setIgnoreMouseEvents(false); + setTimeout(() => { + if (!hud.isDestroyed()) { + hud.setIgnoreMouseEvents(true, { forward: true }); + } + }, 50); +} + export function createUpdateToastWindow(): BrowserWindow { const initialBounds = getUpdateToastBounds(); const parentWindow = diff --git a/src/components/launch/hooks/useLaunchHudInteractionState.ts b/src/components/launch/hooks/useLaunchHudInteractionState.ts index 3b1fd51e..85dd9c7d 100644 --- a/src/components/launch/hooks/useLaunchHudInteractionState.ts +++ b/src/components/launch/hooks/useLaunchHudInteractionState.ts @@ -11,23 +11,53 @@ export function useLaunchHudInteractionState({ isWebcamPreviewDraggingRef: RefObject; webcamPreviewDragStartRef: RefObject; }) { - const anyPopoverOpenRef = useRef(false); const isMouseOverHudRef = useRef(false); useEffect(() => { - anyPopoverOpenRef.current = openId !== null; if (openId !== null) { window.electronAPI?.hudOverlaySetIgnoreMouse?.(false); } else { // Proactively check if we should ignore mouse when popover closes setTimeout(() => { - if (!isMouseOverHudRef.current && !anyPopoverOpenRef.current) { + if (!isMouseOverHudRef.current) { window.electronAPI?.hudOverlaySetIgnoreMouse?.(true); } }, 150); } }, [openId]); + useEffect(() => { + const handleMouseOver = (e: globalThis.MouseEvent) => { + const target = e.target as HTMLElement | null; + if (!target) return; + const isInteractive = !!target.closest( + '.pointer-events-auto, [data-radix-popper-content-wrapper], [class*="menuCard"], [class*="recordingWebcamPreview"]' + ); + + if (isInteractive) { + isMouseOverHudRef.current = true; + if (timeoutRef.current) clearTimeout(timeoutRef.current); + window.electronAPI?.hudOverlaySetIgnoreMouse?.(false); + } else { + isMouseOverHudRef.current = false; + if (timeoutRef.current) clearTimeout(timeoutRef.current); + timeoutRef.current = setTimeout(() => { + if ( + !isHudDraggingRef.current && + !isWebcamPreviewDraggingRef.current && + !webcamPreviewDragStartRef.current && + !isMouseOverHudRef.current + ) { + window.electronAPI?.hudOverlaySetIgnoreMouse?.(true); + } + }, 300); + } + }; + + window.addEventListener("mouseover", handleMouseOver); + return () => window.removeEventListener("mouseover", handleMouseOver); + }, [isHudDraggingRef, isWebcamPreviewDraggingRef, webcamPreviewDragStartRef]); + const beginInteractiveHudAction = useCallback(() => { isMouseOverHudRef.current = true; window.electronAPI?.hudOverlaySetIgnoreMouse?.(false); @@ -56,11 +86,8 @@ export function useLaunchHudInteractionState({ !isHudDraggingRef.current && !isWebcamPreviewDraggingRef.current && !webcamPreviewDragStartRef.current && - !isMouseOverHudRef.current && - !anyPopoverOpenRef.current + !isMouseOverHudRef.current ) { - // If a popover is open, we can still ignore mouse if the mouse is truly gone, - // but we give a bit more breathing room (the 300ms timeout). window.electronAPI?.hudOverlaySetIgnoreMouse?.(true); } }, 300); diff --git a/src/components/launch/popovers/LaunchPopoverCoordinator.tsx b/src/components/launch/popovers/LaunchPopoverCoordinator.tsx index ead6cdc4..55aa5f47 100644 --- a/src/components/launch/popovers/LaunchPopoverCoordinator.tsx +++ b/src/components/launch/popovers/LaunchPopoverCoordinator.tsx @@ -1,4 +1,4 @@ -import { createContext, useCallback, useContext, useMemo, useState, type ReactNode } from "react"; +import { createContext, useCallback, useContext, useEffect, useMemo, useState, type ReactNode } from "react"; interface LaunchPopoverCoordinatorValue { openId: string | null; @@ -22,6 +22,12 @@ export function LaunchPopoverCoordinatorProvider({ children }: { children: React const isOpen = useCallback((id: string) => openId === id, [openId]); + useEffect(() => { + const handleBlur = () => setOpenId(null); + window.addEventListener("blur", handleBlur); + return () => window.removeEventListener("blur", handleBlur); + }, []); + const value = useMemo( () => ({ openId, From 46c7115ba532abd1d6268aa004f5be324fc57f72 Mon Sep 17 00:00:00 2001 From: Alan Trebugeais Date: Sun, 10 May 2026 00:25:19 +0200 Subject: [PATCH 2/2] fix code rabbits comment which is valid --- src/components/launch/LaunchWindow.tsx | 1 + src/components/launch/hooks/useLaunchHudInteractionState.ts | 5 ++--- src/components/launch/popovers/PopoverScaffold.tsx | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/launch/LaunchWindow.tsx b/src/components/launch/LaunchWindow.tsx index 80d9d222..49547069 100644 --- a/src/components/launch/LaunchWindow.tsx +++ b/src/components/launch/LaunchWindow.tsx @@ -515,6 +515,7 @@ function LaunchWindowContent() {
; }) { const isMouseOverHudRef = useRef(false); + const timeoutRef = useRef(null); useEffect(() => { if (openId !== null) { @@ -31,7 +32,7 @@ export function useLaunchHudInteractionState({ const target = e.target as HTMLElement | null; if (!target) return; const isInteractive = !!target.closest( - '.pointer-events-auto, [data-radix-popper-content-wrapper], [class*="menuCard"], [class*="recordingWebcamPreview"]' + ".pointer-events-auto, [data-hud-interactive], [data-radix-popper-content-wrapper]" ); if (isInteractive) { @@ -69,8 +70,6 @@ export function useLaunchHudInteractionState({ window.electronAPI?.hudOverlaySetIgnoreMouse?.(false); }, []); - const timeoutRef = useRef(null); - const handleHudMouseLeave = useCallback((event: MouseEvent) => { const nextTarget = event.relatedTarget; if (nextTarget instanceof Node && event.currentTarget.contains(nextTarget)) { diff --git a/src/components/launch/popovers/PopoverScaffold.tsx b/src/components/launch/popovers/PopoverScaffold.tsx index 856de855..be349192 100644 --- a/src/components/launch/popovers/PopoverScaffold.tsx +++ b/src/components/launch/popovers/PopoverScaffold.tsx @@ -80,6 +80,7 @@ export function HudPopover({ {trigger}