Skip to content

Commit e5cb2d7

Browse files
authored
Merge pull request #486 from ExtraBinoss/fix/mouse-passthrough
fix: Mouse passthrough bug, v 1.3.0beta
2 parents f6a40d7 + 46c7115 commit e5cb2d7

6 files changed

Lines changed: 93 additions & 14 deletions

File tree

‎electron/ipc/register/sources.ts‎

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
resolveLinuxWindowBounds,
1414
stopWindowBoundsCapture,
1515
} from "../cursor/bounds";
16+
import { reassertHudOverlayMousePassthrough } from "../../windows";
1617

1718
const execFileAsync = promisify(execFile);
1819
const SOURCE_LIST_CACHE_TTL_MS = 1200;
@@ -487,11 +488,24 @@ body{background:transparent;overflow:hidden;width:100vw;height:100vh}
487488
throw loadError
488489
}
489490

490-
setTimeout(() => {
491-
if (!highlightWin.isDestroyed()) highlightWin.close()
492-
}, 1700)
491+
// The highlight window appearing (even with focusable:false) can corrupt
492+
// the WS_EX_TRANSPARENT flag on the HUD on Windows 11+, breaking hover
493+
// detection until the user moves their mouse over the bar again.
494+
// Re-assert passthrough immediately so click-through is restored at once.
495+
reassertHudOverlayMousePassthrough();
496+
497+
const highlightCloseTimer = setTimeout(() => {
498+
if (!highlightWin.isDestroyed()) highlightWin.close()
499+
}, 1700)
500+
501+
highlightWin.on("closed", () => {
502+
clearTimeout(highlightCloseTimer);
503+
// Re-assert once more when the window is actually destroyed so the
504+
// native flag is clean regardless of timing.
505+
reassertHudOverlayMousePassthrough();
506+
});
493507

494-
return { success: true }
508+
return { success: true }
495509
} catch (error) {
496510
console.error('Failed to show source highlight:', error)
497511
return { success: false }

‎electron/windows.ts‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,7 @@ export function createHudOverlayWindow(): BrowserWindow {
373373
skipTaskbar: true,
374374
hasShadow: false,
375375
show: false,
376+
focusable: false,
376377
webPreferences: {
377378
preload: path.join(electronWindowsDir, "preload.mjs"),
378379
nodeIntegration: false,
@@ -513,6 +514,36 @@ export function getHudOverlayWindow(): BrowserWindow | null {
513514
return hudOverlayWindow && !hudOverlayWindow.isDestroyed() ? hudOverlayWindow : null;
514515
}
515516

517+
/**
518+
* Re-initialise the HUD overlay's mouse passthrough state.
519+
*
520+
* On Windows 11+, any new BrowserWindow appearing (even focusable:false ones
521+
* like the source highlight overlay) can silently corrupt the
522+
* WS_EX_TRANSPARENT flag that backs setIgnoreMouseEvents forwarding. Call
523+
* this after any operation that creates or destroys a sibling window so that
524+
* hover detection on the HUD is immediately restored without requiring the
525+
* user to move their mouse over the bar.
526+
*/
527+
export function reassertHudOverlayMousePassthrough(): void {
528+
if (process.platform !== "win32" || !isHudOverlayMousePassthroughSupported()) {
529+
return;
530+
}
531+
532+
const hud = getHudOverlayWindow();
533+
if (!hud) {
534+
return;
535+
}
536+
537+
// Toggle off then back on so the native WS_EX_TRANSPARENT flag is fully
538+
// re-initialised rather than merely re-asserted in a potentially broken state.
539+
hud.setIgnoreMouseEvents(false);
540+
setTimeout(() => {
541+
if (!hud.isDestroyed()) {
542+
hud.setIgnoreMouseEvents(true, { forward: true });
543+
}
544+
}, 50);
545+
}
546+
516547
export function createUpdateToastWindow(): BrowserWindow {
517548
const initialBounds = getUpdateToastBounds();
518549
const parentWindow =

‎src/components/launch/LaunchWindow.tsx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,7 @@ function LaunchWindowContent() {
515515
<div
516516
ref={recordingWebcamPreviewContainerRef}
517517
className={`${styles.recordingWebcamPreview} ${styles.electronNoDrag} pointer-events-auto`}
518+
data-hud-interactive
518519
title={t("recording.webcam")}
519520
style={{
520521
transform: `translate(${webcamPreviewOffset.x}px, ${webcamPreviewOffset.y}px)`,

‎src/components/launch/hooks/useLaunchHudInteractionState.ts‎

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,23 +11,54 @@ export function useLaunchHudInteractionState({
1111
isWebcamPreviewDraggingRef: RefObject<boolean>;
1212
webcamPreviewDragStartRef: RefObject<unknown>;
1313
}) {
14-
const anyPopoverOpenRef = useRef(false);
1514
const isMouseOverHudRef = useRef(false);
15+
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
1616

1717
useEffect(() => {
18-
anyPopoverOpenRef.current = openId !== null;
1918
if (openId !== null) {
2019
window.electronAPI?.hudOverlaySetIgnoreMouse?.(false);
2120
} else {
2221
// Proactively check if we should ignore mouse when popover closes
2322
setTimeout(() => {
24-
if (!isMouseOverHudRef.current && !anyPopoverOpenRef.current) {
23+
if (!isMouseOverHudRef.current) {
2524
window.electronAPI?.hudOverlaySetIgnoreMouse?.(true);
2625
}
2726
}, 150);
2827
}
2928
}, [openId]);
3029

30+
useEffect(() => {
31+
const handleMouseOver = (e: globalThis.MouseEvent) => {
32+
const target = e.target as HTMLElement | null;
33+
if (!target) return;
34+
const isInteractive = !!target.closest(
35+
".pointer-events-auto, [data-hud-interactive], [data-radix-popper-content-wrapper]"
36+
);
37+
38+
if (isInteractive) {
39+
isMouseOverHudRef.current = true;
40+
if (timeoutRef.current) clearTimeout(timeoutRef.current);
41+
window.electronAPI?.hudOverlaySetIgnoreMouse?.(false);
42+
} else {
43+
isMouseOverHudRef.current = false;
44+
if (timeoutRef.current) clearTimeout(timeoutRef.current);
45+
timeoutRef.current = setTimeout(() => {
46+
if (
47+
!isHudDraggingRef.current &&
48+
!isWebcamPreviewDraggingRef.current &&
49+
!webcamPreviewDragStartRef.current &&
50+
!isMouseOverHudRef.current
51+
) {
52+
window.electronAPI?.hudOverlaySetIgnoreMouse?.(true);
53+
}
54+
}, 300);
55+
}
56+
};
57+
58+
window.addEventListener("mouseover", handleMouseOver);
59+
return () => window.removeEventListener("mouseover", handleMouseOver);
60+
}, [isHudDraggingRef, isWebcamPreviewDraggingRef, webcamPreviewDragStartRef]);
61+
3162
const beginInteractiveHudAction = useCallback(() => {
3263
isMouseOverHudRef.current = true;
3364
window.electronAPI?.hudOverlaySetIgnoreMouse?.(false);
@@ -39,8 +70,6 @@ export function useLaunchHudInteractionState({
3970
window.electronAPI?.hudOverlaySetIgnoreMouse?.(false);
4071
}, []);
4172

42-
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
43-
4473
const handleHudMouseLeave = useCallback((event: MouseEvent<HTMLDivElement>) => {
4574
const nextTarget = event.relatedTarget;
4675
if (nextTarget instanceof Node && event.currentTarget.contains(nextTarget)) {
@@ -56,11 +85,8 @@ export function useLaunchHudInteractionState({
5685
!isHudDraggingRef.current &&
5786
!isWebcamPreviewDraggingRef.current &&
5887
!webcamPreviewDragStartRef.current &&
59-
!isMouseOverHudRef.current &&
60-
!anyPopoverOpenRef.current
88+
!isMouseOverHudRef.current
6189
) {
62-
// If a popover is open, we can still ignore mouse if the mouse is truly gone,
63-
// but we give a bit more breathing room (the 300ms timeout).
6490
window.electronAPI?.hudOverlaySetIgnoreMouse?.(true);
6591
}
6692
}, 300);

‎src/components/launch/popovers/LaunchPopoverCoordinator.tsx‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { createContext, useCallback, useContext, useMemo, useState, type ReactNode } from "react";
1+
import { createContext, useCallback, useContext, useEffect, useMemo, useState, type ReactNode } from "react";
22

33
interface LaunchPopoverCoordinatorValue {
44
openId: string | null;
@@ -22,6 +22,12 @@ export function LaunchPopoverCoordinatorProvider({ children }: { children: React
2222

2323
const isOpen = useCallback((id: string) => openId === id, [openId]);
2424

25+
useEffect(() => {
26+
const handleBlur = () => setOpenId(null);
27+
window.addEventListener("blur", handleBlur);
28+
return () => window.removeEventListener("blur", handleBlur);
29+
}, []);
30+
2531
const value = useMemo(
2632
() => ({
2733
openId,

‎src/components/launch/popovers/PopoverScaffold.tsx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ export function HudPopover({
8080
<PopoverTrigger asChild>{trigger}</PopoverTrigger>
8181
<PopoverContent
8282
className={`launch-theme ${styles.menuCard} ${styles.electronNoDrag}`}
83+
data-hud-interactive
8384
unstyled
8485
side="top"
8586
align={align}

0 commit comments

Comments
 (0)