Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 26 additions & 26 deletions electron/hudOverlayBounds.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
});

Expand All @@ -50,9 +50,9 @@ describe("getHudOverlayWindowBounds", () => {
),
).toEqual({
x: -100,
y: 280,
y: 320,
width: 640,
height: 160,
height: 120,
});
});

Expand Down Expand Up @@ -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,
});
});

Expand All @@ -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,
});
});

Expand All @@ -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,
});
});
});
Expand Down
6 changes: 3 additions & 3 deletions electron/hudOverlayBounds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
16 changes: 4 additions & 12 deletions electron/windows.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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;
}

Expand Down
1 change: 1 addition & 0 deletions src/components/launch/LaunchWindow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ function LaunchWindowContent() {
isHudDraggingRef,
isWebcamPreviewDraggingRef,
webcamPreviewDragStartRef,
useMousePassthroughTracking: hudOverlayMousePassthroughSupported !== false,
});

useEffect(() => {
Expand Down
57 changes: 42 additions & 15 deletions src/components/launch/hooks/useLaunchHudInteractionState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,40 @@ export function useLaunchHudInteractionState({
isHudDraggingRef,
isWebcamPreviewDraggingRef,
webcamPreviewDragStartRef,
useMousePassthroughTracking,
}: {
openId: string | null;
isHudDraggingRef: RefObject<boolean>;
isWebcamPreviewDraggingRef: RefObject<boolean>;
webcamPreviewDragStartRef: RefObject<unknown>;
useMousePassthroughTracking: boolean;
}) {
const isMouseOverHudRef = useRef(false);
const timeoutRef = useRef<NodeJS.Timeout | null>(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) {
Expand All @@ -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(
Expand All @@ -70,6 +83,10 @@ export function useLaunchHudInteractionState({
}
};

if (!useMousePassthroughTracking) {
return;
}

window.addEventListener("mouseover", handleMouseTracking);
window.addEventListener("mousemove", handleMouseTracking);
return () => {
Expand All @@ -80,6 +97,7 @@ export function useLaunchHudInteractionState({
isHudDraggingRef,
isWebcamPreviewDraggingRef,
setHudMouseInteractive,
useMousePassthroughTracking,
webcamPreviewDragStartRef,
]);

Expand All @@ -93,6 +111,10 @@ export function useLaunchHudInteractionState({

const handleHudMouseLeave = useCallback(
(event: MouseEvent<HTMLDivElement>) => {
if (!useMousePassthroughTracking) {
return;
}

const nextTarget = event.relatedTarget;
if (nextTarget instanceof Node && event.currentTarget.contains(nextTarget)) {
return;
Expand All @@ -113,7 +135,12 @@ export function useLaunchHudInteractionState({
}
}, 300);
},
[isHudDraggingRef, isWebcamPreviewDraggingRef, webcamPreviewDragStartRef],
[
isHudDraggingRef,
isWebcamPreviewDraggingRef,
useMousePassthroughTracking,
webcamPreviewDragStartRef,
],
);

return {
Expand Down