From 83f25e321f2a91b8b385412cbcae1561198235b3 Mon Sep 17 00:00:00 2001 From: AntimonyBullets Date: Mon, 9 Nov 2026 17:29:58 +0530 Subject: [PATCH 1/3] Ignore HUD and webcam clicks when generating auto-zooms --- electron/electron-env.d.ts | 3 ++ electron/ipc/cursor/interaction.ts | 18 +++++++++++ electron/preload.ts | 5 +++ electron/windows.ts | 37 +++++++++++++++++++++ src/components/launch/LaunchWindow.tsx | 45 ++++++++++++++++++++++++++ 5 files changed, 108 insertions(+) diff --git a/electron/electron-env.d.ts b/electron/electron-env.d.ts index 1900a9e2..fbd1b67b 100644 --- a/electron/electron-env.d.ts +++ b/electron/electron-env.d.ts @@ -253,6 +253,9 @@ interface Window { onEditorModeChanged: (callback: (inEditor: boolean) => void) => () => void; hudOverlayRendererReady: () => void; hudOverlaySetWebcamPreviewVisible: (visible: boolean) => void; + hudOverlaySetInteractiveBounds: ( + bounds: Array<{ left: number; top: number; right: number; bottom: number }> | null, + ) => void; getHudOverlayCaptureProtection: () => Promise<{ success: boolean; enabled: boolean }>; getHudOverlayMousePassthroughSupported: () => Promise<{ success: boolean; diff --git a/electron/ipc/cursor/interaction.ts b/electron/ipc/cursor/interaction.ts index 90dd10eb..ea8dfd17 100644 --- a/electron/ipc/cursor/interaction.ts +++ b/electron/ipc/cursor/interaction.ts @@ -11,6 +11,8 @@ import { setLastLeftClick, setLinuxCursorScreenPoint, } from "../state"; +import { getScreen } from "../utils"; +import { getHudOverlayInteractiveBounds, getHudOverlayWindow } from "../../windows"; import type { CursorInteractionType, HookMouseEvent, @@ -192,6 +194,22 @@ export function recordCursorMouseDown(button: 1 | 2 | 3) { return; } + const hudBounds = getHudOverlayInteractiveBounds(); + const hud = getHudOverlayWindow(); + if (hud?.isVisible() && !hud.isMinimized() && hudBounds) { + const cursor = getScreen().getCursorScreenPoint(); + const isInsideHudInteractiveElement = hudBounds.some( + (bounds) => + cursor.x >= bounds.left && + cursor.x <= bounds.right && + cursor.y >= bounds.top && + cursor.y <= bounds.bottom, + ); + if (isInsideHudInteractiveElement) { + return; + } + } + const point = getNormalizedCursorPoint(); if (!point) { return; diff --git a/electron/preload.ts b/electron/preload.ts index 762c1555..3a5ea455 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -228,6 +228,11 @@ contextBridge.exposeInMainWorld("electronAPI", { hudOverlaySetWebcamPreviewVisible: (visible: boolean) => { ipcRenderer.send("hud-overlay-set-webcam-preview-visible", visible); }, + hudOverlaySetInteractiveBounds: ( + bounds: Array<{ left: number; top: number; right: number; bottom: number }> | null, + ) => { + ipcRenderer.send("hud-overlay-set-interactive-bounds", bounds); + }, getHudOverlayCaptureProtection: () => { return ipcRenderer.invoke("get-hud-overlay-capture-protection"); }, diff --git a/electron/windows.ts b/electron/windows.ts index 506a54e8..96577c13 100644 --- a/electron/windows.ts +++ b/electron/windows.ts @@ -55,6 +55,12 @@ let hudOverlayMouseReassertTimer: NodeJS.Timeout | null = null; let hudOverlayRecordingActive = false; let hudCaptureStarting = false; let hudOverlayWebcamPreviewVisible = false; +let hudOverlayInteractiveBounds: Array<{ + left: number; + top: number; + right: number; + bottom: number; +}> | null = null; let countdownWindow: BrowserWindow | null = null; let capturePickerWindows: BrowserWindow[] = []; let areaRecordingBorderWindow: BrowserWindow | null = null; @@ -487,6 +493,36 @@ ipcMain.on("hud-overlay-set-webcam-preview-visible", (_event, visible: boolean) } }); +ipcMain.on( + "hud-overlay-set-interactive-bounds", + ( + _event, + bounds: + | Array<{ left: number; top: number; right: number; bottom: number }> + | null + | undefined, + ) => { + if ( + !bounds || + bounds.some( + (rect) => + ![rect.left, rect.top, rect.right, rect.bottom].every(Number.isFinite) || + rect.right < rect.left || + rect.bottom < rect.top, + ) + ) { + hudOverlayInteractiveBounds = null; + return; + } + + hudOverlayInteractiveBounds = bounds; + }, +); + +export function getHudOverlayInteractiveBounds() { + return hudOverlayInteractiveBounds; +} + let hudCaptureProtectionUpdates = Promise.resolve(); ipcMain.handle("set-hud-overlay-capture-protection", (_event, enabled: boolean) => { // Settings can be open in more than one window. Match each helper acknowledgement @@ -730,6 +766,7 @@ export function createHudOverlayWindow(): BrowserWindow { screen.removeListener("display-metrics-changed", handleDisplayMetricsChanged); if (hudOverlayWindow === win) { hudOverlayWindow = null; + hudOverlayInteractiveBounds = null; recordingPreparationActive = false; hudCaptureStarting = false; hudOverlayRecordingActive = false; diff --git a/src/components/launch/LaunchWindow.tsx b/src/components/launch/LaunchWindow.tsx index cc97ff79..b26efbdb 100644 --- a/src/components/launch/LaunchWindow.tsx +++ b/src/components/launch/LaunchWindow.tsx @@ -156,6 +156,51 @@ function LaunchWindowContent() { docked: showRecordingWebcamPreview, }); + useEffect(() => { + const reportInteractiveBounds = () => { + const elements = [hudBarRef.current, recordingWebcamPreviewContainerRef.current].filter( + (element): element is HTMLDivElement => element !== null, + ); + + if (elements.length === 0) { + window.electronAPI?.hudOverlaySetInteractiveBounds?.(null); + return; + } + + window.electronAPI?.hudOverlaySetInteractiveBounds?.( + elements.map((element) => { + const rect = element.getBoundingClientRect(); + return { + left: window.screenX + rect.left, + top: window.screenY + rect.top, + right: window.screenX + rect.right, + bottom: window.screenY + rect.bottom, + }; + }), + ); + }; + + reportInteractiveBounds(); + const resizeObserver = new ResizeObserver(reportInteractiveBounds); + if (hudBarRef.current) resizeObserver.observe(hudBarRef.current); + if (recordingWebcamPreviewContainerRef.current) { + resizeObserver.observe(recordingWebcamPreviewContainerRef.current); + } + window.addEventListener("mousemove", reportInteractiveBounds); + window.addEventListener("resize", reportInteractiveBounds); + + return () => { + resizeObserver.disconnect(); + window.removeEventListener("mousemove", reportInteractiveBounds); + window.removeEventListener("resize", reportInteractiveBounds); + window.electronAPI?.hudOverlaySetInteractiveBounds?.(null); + }; + }, [ + recordingHudOffset, + recordingWebcamPreviewContainerRef, + showRecordingWebcamPreview, + ]); + const { handleHudMouseEnter, handleHudMouseLeave, beginInteractiveHudAction } = useLaunchHudInteractionState({ openId, From 2eee9afd9b3caccd0030f4bbcf096ead2a59a6b6 Mon Sep 17 00:00:00 2001 From: AntimonyBullets Date: Tue, 10 Nov 2026 16:10:24 +0530 Subject: [PATCH 2/3] Fix overlay bounds handling and tests --- electron/ipc/cursor/interaction.test.ts | 5 +++++ electron/windows.ts | 4 +++- src/components/launch/LaunchWindow.tsx | 1 + 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/electron/ipc/cursor/interaction.test.ts b/electron/ipc/cursor/interaction.test.ts index 4ea66241..d64f0d06 100644 --- a/electron/ipc/cursor/interaction.test.ts +++ b/electron/ipc/cursor/interaction.test.ts @@ -11,6 +11,11 @@ vi.mock("electron", () => ({ }, })); +vi.mock("../../windows", () => ({ + getHudOverlayInteractiveBounds: vi.fn(() => null), + getHudOverlayWindow: vi.fn(() => null), +})); + import { repairBundledUiohookBinaryForCurrentArch, shouldStartGlobalInteractionHook, diff --git a/electron/windows.ts b/electron/windows.ts index 96577c13..32bac0e9 100644 --- a/electron/windows.ts +++ b/electron/windows.ts @@ -503,9 +503,11 @@ ipcMain.on( | undefined, ) => { if ( - !bounds || + !Array.isArray(bounds) || bounds.some( (rect) => + !rect || + typeof rect !== "object" || ![rect.left, rect.top, rect.right, rect.bottom].every(Number.isFinite) || rect.right < rect.left || rect.bottom < rect.top, diff --git a/src/components/launch/LaunchWindow.tsx b/src/components/launch/LaunchWindow.tsx index b26efbdb..68a7758d 100644 --- a/src/components/launch/LaunchWindow.tsx +++ b/src/components/launch/LaunchWindow.tsx @@ -199,6 +199,7 @@ function LaunchWindowContent() { recordingHudOffset, recordingWebcamPreviewContainerRef, showRecordingWebcamPreview, + webcamPreview.webcamPreviewOffset, ]); const { handleHudMouseEnter, handleHudMouseLeave, beginInteractiveHudAction } = From 908bb938fb6119474d037db32f6fa64a3c5cf82a Mon Sep 17 00:00:00 2001 From: webadderall <131426131+webadderall@users.noreply.github.com> Date: Thu, 12 Nov 2026 06:43:33 +1100 Subject: [PATCH 3/3] Test auto-zoom click filtering for recording overlays --- electron/ipc/cursor/interaction.test.ts | 62 ++++++++++++++++++++++++- 1 file changed, 60 insertions(+), 2 deletions(-) diff --git a/electron/ipc/cursor/interaction.test.ts b/electron/ipc/cursor/interaction.test.ts index d64f0d06..d239bb2a 100644 --- a/electron/ipc/cursor/interaction.test.ts +++ b/electron/ipc/cursor/interaction.test.ts @@ -12,14 +12,72 @@ vi.mock("electron", () => ({ })); vi.mock("../../windows", () => ({ - getHudOverlayInteractiveBounds: vi.fn(() => null), - getHudOverlayWindow: vi.fn(() => null), + getHudOverlayInteractiveBounds: vi.fn(), + getHudOverlayWindow: vi.fn(() => ({ + isVisible: () => true, + isMinimized: () => false, + })), })); +vi.mock("../utils", () => ({ + getScreen: vi.fn(), +})); + +vi.mock("./telemetry", () => ({ + getCursorCaptureElapsedMs: () => 42, + getNormalizedCursorPoint: () => ({ cx: 0.2, cy: 0.3 }), + isCursorCapturePaused: () => false, + pushCursorSample: vi.fn(), +})); + +import { getHudOverlayInteractiveBounds } from "../../windows"; +import { setIsCursorCaptureActive, setLastLeftClick } from "../state"; +import { getScreen } from "../utils"; import { + recordCursorMouseDown, repairBundledUiohookBinaryForCurrentArch, shouldStartGlobalInteractionHook, } from "./interaction"; +import { pushCursorSample } from "./telemetry"; + +describe("recordCursorMouseDown with recording overlays", () => { + afterEach(() => { + setIsCursorCaptureActive(false); + setLastLeftClick(null); + vi.clearAllMocks(); + }); + + it("skips clicks on the HUD and webcam preview and records clicks outside both", () => { + setIsCursorCaptureActive(true); + setLastLeftClick(null); + vi.mocked(getHudOverlayInteractiveBounds).mockReturnValue([ + { left: 100, top: 100, right: 400, bottom: 150 }, + { left: 450, top: 200, right: 550, bottom: 300 }, + ]); + const getCursorScreenPoint = vi + .fn() + .mockReturnValueOnce({ x: 200, y: 125 }) + .mockReturnValueOnce({ x: 500, y: 250 }) + .mockReturnValueOnce({ x: 425, y: 180 }); + vi.mocked(getScreen).mockReturnValue({ getCursorScreenPoint } as ReturnType< + typeof getScreen + >); + + recordCursorMouseDown(1); + recordCursorMouseDown(1); + expect(pushCursorSample).not.toHaveBeenCalled(); + + recordCursorMouseDown(1); + expect(pushCursorSample).toHaveBeenCalledExactlyOnceWith( + 0.2, + 0.3, + 42, + "click", + undefined, + 1, + ); + }); +}); describe("shouldStartGlobalInteractionHook", () => { it("does not start the synchronous uiohook event tap on macOS", () => {