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/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() {
;
webcamPreviewDragStartRef: RefObject
;
}) {
- const anyPopoverOpenRef = useRef(false);
const isMouseOverHudRef = useRef(false);
+ const timeoutRef = useRef(null);
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-hud-interactive], [data-radix-popper-content-wrapper]"
+ );
+
+ 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);
@@ -39,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)) {
@@ -56,11 +85,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,
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}