From 316c11035c6fe622100841d59cf862b9155c4910 Mon Sep 17 00:00:00 2001 From: wiiiii123 Date: Tue, 21 Apr 2026 02:25:56 +0700 Subject: [PATCH 1/2] fix(launch): keep HUD interactive after short drags --- src/components/launch/LaunchWindow.tsx | 30 ++++++++++---- .../launch/hudMousePassthrough.test.ts | 39 +++++++++++++++++++ src/components/launch/hudMousePassthrough.ts | 23 +++++++++++ 3 files changed, 85 insertions(+), 7 deletions(-) create mode 100644 src/components/launch/hudMousePassthrough.test.ts create mode 100644 src/components/launch/hudMousePassthrough.ts diff --git a/src/components/launch/LaunchWindow.tsx b/src/components/launch/LaunchWindow.tsx index 2c4e6146..31ffd0c7 100644 --- a/src/components/launch/LaunchWindow.tsx +++ b/src/components/launch/LaunchWindow.tsx @@ -1,27 +1,27 @@ import { AppWindow, ArrowCircleUp as ArrowUpCircle, - ArrowClockwise as RefreshCw, - CaretUp as ChevronUp, CheckCircle as CheckCircle2, - DotsThreeVertical as MoreVertical, + CaretUp as ChevronUp, Eye, EyeSlash as EyeOff, FolderOpen, + Translate as Languages, Microphone as Mic, MicrophoneSlash as MicOff, Minus, Monitor, + DotsThreeVertical as MoreVertical, Pause, Play, - SpeakerHigh as Volume2, - SpeakerX as VolumeX, + ArrowClockwise as RefreshCw, Stop as Square, Timer, - Translate as Languages, VideoCamera as Video, VideoCamera as VideoIcon, VideoCameraSlash as VideoOff, + SpeakerHigh as Volume2, + SpeakerX as VolumeX, X, } from "@phosphor-icons/react"; import { AnimatePresence, motion } from "motion/react"; @@ -41,6 +41,7 @@ import { ContentClamp } from "../ui/content-clamp"; import ProjectBrowserDialog, { type ProjectLibraryEntry, } from "../video-editor/ProjectBrowserDialog"; +import { shouldRestoreHudMousePassthroughAfterDrag } from "./hudMousePassthrough"; import styles from "./LaunchWindow.module.css"; interface DesktopSource { @@ -465,7 +466,22 @@ export function LaunchWindow() { if (event.currentTarget.hasPointerCapture(event.pointerId)) { event.currentTarget.releasePointerCapture(event.pointerId); } - if (wasDragging) { + const hudBounds = hudContentRef.current?.getBoundingClientRect(); + if ( + wasDragging && + shouldRestoreHudMousePassthroughAfterDrag( + hudBounds + ? { + left: hudBounds.left, + top: hudBounds.top, + right: hudBounds.right, + bottom: hudBounds.bottom, + } + : null, + event.clientX, + event.clientY, + ) + ) { window.electronAPI?.hudOverlaySetIgnoreMouse?.(true); } }; diff --git a/src/components/launch/hudMousePassthrough.test.ts b/src/components/launch/hudMousePassthrough.test.ts new file mode 100644 index 00000000..bc17e697 --- /dev/null +++ b/src/components/launch/hudMousePassthrough.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, it } from "vitest"; + +import { shouldRestoreHudMousePassthroughAfterDrag } from "./hudMousePassthrough"; + +describe("shouldRestoreHudMousePassthroughAfterDrag", () => { + it("keeps the HUD interactive when the pointer is still inside the HUD", () => { + expect( + shouldRestoreHudMousePassthroughAfterDrag( + { left: 100, top: 200, right: 300, bottom: 260 }, + 180, + 230, + ), + ).toBe(false); + }); + + it("keeps the HUD interactive when the pointer ends on the HUD edge", () => { + expect( + shouldRestoreHudMousePassthroughAfterDrag( + { left: 100, top: 200, right: 300, bottom: 260 }, + 300, + 260, + ), + ).toBe(false); + }); + + it("restores passthrough when the pointer ends outside the HUD", () => { + expect( + shouldRestoreHudMousePassthroughAfterDrag( + { left: 100, top: 200, right: 300, bottom: 260 }, + 301, + 261, + ), + ).toBe(true); + }); + + it("restores passthrough when no HUD bounds are available", () => { + expect(shouldRestoreHudMousePassthroughAfterDrag(null, 180, 230)).toBe(true); + }); +}); diff --git a/src/components/launch/hudMousePassthrough.ts b/src/components/launch/hudMousePassthrough.ts new file mode 100644 index 00000000..50d190d4 --- /dev/null +++ b/src/components/launch/hudMousePassthrough.ts @@ -0,0 +1,23 @@ +export interface HudInteractiveBounds { + left: number; + top: number; + right: number; + bottom: number; +} + +export function shouldRestoreHudMousePassthroughAfterDrag( + bounds: HudInteractiveBounds | null, + clientX: number, + clientY: number, +): boolean { + if (!bounds) { + return true; + } + + return ( + clientX < bounds.left || + clientX > bounds.right || + clientY < bounds.top || + clientY > bounds.bottom + ); +} From fcccd1def641b816c9ab69dc716e8d690b0aba2c Mon Sep 17 00:00:00 2001 From: wiiiii123 Date: Tue, 21 Apr 2026 02:54:00 +0700 Subject: [PATCH 2/2] fix(launch): use live HUD bounds after drag --- src/components/launch/LaunchWindow.tsx | 33 +++++++----- .../launch/hudMousePassthrough.test.ts | 51 +++++++++++-------- src/components/launch/hudMousePassthrough.ts | 16 ++++++ 3 files changed, 65 insertions(+), 35 deletions(-) diff --git a/src/components/launch/LaunchWindow.tsx b/src/components/launch/LaunchWindow.tsx index 31ffd0c7..a9403304 100644 --- a/src/components/launch/LaunchWindow.tsx +++ b/src/components/launch/LaunchWindow.tsx @@ -41,7 +41,10 @@ import { ContentClamp } from "../ui/content-clamp"; import ProjectBrowserDialog, { type ProjectLibraryEntry, } from "../video-editor/ProjectBrowserDialog"; -import { shouldRestoreHudMousePassthroughAfterDrag } from "./hudMousePassthrough"; +import { + mergeHudInteractiveBounds, + shouldRestoreHudMousePassthroughAfterDrag, +} from "./hudMousePassthrough"; import styles from "./LaunchWindow.module.css"; interface DesktopSource { @@ -466,21 +469,25 @@ export function LaunchWindow() { if (event.currentTarget.hasPointerCapture(event.pointerId)) { event.currentTarget.releasePointerCapture(event.pointerId); } - const hudBounds = hudContentRef.current?.getBoundingClientRect(); - if ( - wasDragging && - shouldRestoreHudMousePassthroughAfterDrag( - hudBounds + const hudBounds = mergeHudInteractiveBounds( + [ + dropdownRef.current?.getBoundingClientRect(), + hudBarRef.current?.getBoundingClientRect(), + recordingWebcamPreviewContainerRef.current?.getBoundingClientRect(), + ].map((bounds) => + bounds ? { - left: hudBounds.left, - top: hudBounds.top, - right: hudBounds.right, - bottom: hudBounds.bottom, + left: bounds.left, + top: bounds.top, + right: bounds.right, + bottom: bounds.bottom, } : null, - event.clientX, - event.clientY, - ) + ), + ); + if ( + wasDragging && + shouldRestoreHudMousePassthroughAfterDrag(hudBounds, event.clientX, event.clientY) ) { window.electronAPI?.hudOverlaySetIgnoreMouse?.(true); } diff --git a/src/components/launch/hudMousePassthrough.test.ts b/src/components/launch/hudMousePassthrough.test.ts index bc17e697..9490b054 100644 --- a/src/components/launch/hudMousePassthrough.test.ts +++ b/src/components/launch/hudMousePassthrough.test.ts @@ -1,36 +1,43 @@ import { describe, expect, it } from "vitest"; -import { shouldRestoreHudMousePassthroughAfterDrag } from "./hudMousePassthrough"; +import { + mergeHudInteractiveBounds, + shouldRestoreHudMousePassthroughAfterDrag, +} from "./hudMousePassthrough"; + +const hudBounds = { left: 100, top: 200, right: 300, bottom: 260 }; + +describe("mergeHudInteractiveBounds", () => { + it("returns null when there are no interactive bounds", () => { + expect(mergeHudInteractiveBounds([null, undefined])).toBeNull(); + }); + + it("merges the dropdown, bar, and webcam preview bounds", () => { + expect( + mergeHudInteractiveBounds([ + { left: 120, top: 220, right: 260, bottom: 320 }, + { left: 100, top: 200, right: 300, bottom: 260 }, + { left: 140, top: 280, right: 340, bottom: 430 }, + ]), + ).toEqual({ left: 100, top: 200, right: 340, bottom: 430 }); + }); +}); describe("shouldRestoreHudMousePassthroughAfterDrag", () => { it("keeps the HUD interactive when the pointer is still inside the HUD", () => { - expect( - shouldRestoreHudMousePassthroughAfterDrag( - { left: 100, top: 200, right: 300, bottom: 260 }, - 180, - 230, - ), - ).toBe(false); + expect(shouldRestoreHudMousePassthroughAfterDrag(hudBounds, 180, 230)).toBe(false); }); it("keeps the HUD interactive when the pointer ends on the HUD edge", () => { - expect( - shouldRestoreHudMousePassthroughAfterDrag( - { left: 100, top: 200, right: 300, bottom: 260 }, - 300, - 260, - ), - ).toBe(false); + expect(shouldRestoreHudMousePassthroughAfterDrag(hudBounds, 100, 200)).toBe(false); + expect(shouldRestoreHudMousePassthroughAfterDrag(hudBounds, 300, 260)).toBe(false); }); it("restores passthrough when the pointer ends outside the HUD", () => { - expect( - shouldRestoreHudMousePassthroughAfterDrag( - { left: 100, top: 200, right: 300, bottom: 260 }, - 301, - 261, - ), - ).toBe(true); + expect(shouldRestoreHudMousePassthroughAfterDrag(hudBounds, 99, 230)).toBe(true); + expect(shouldRestoreHudMousePassthroughAfterDrag(hudBounds, 180, 199)).toBe(true); + expect(shouldRestoreHudMousePassthroughAfterDrag(hudBounds, 301, 230)).toBe(true); + expect(shouldRestoreHudMousePassthroughAfterDrag(hudBounds, 180, 261)).toBe(true); }); it("restores passthrough when no HUD bounds are available", () => { diff --git a/src/components/launch/hudMousePassthrough.ts b/src/components/launch/hudMousePassthrough.ts index 50d190d4..5d972f55 100644 --- a/src/components/launch/hudMousePassthrough.ts +++ b/src/components/launch/hudMousePassthrough.ts @@ -5,6 +5,22 @@ export interface HudInteractiveBounds { bottom: number; } +export function mergeHudInteractiveBounds( + bounds: Array, +): HudInteractiveBounds | null { + const presentBounds = bounds.filter((value): value is HudInteractiveBounds => Boolean(value)); + if (presentBounds.length === 0) { + return null; + } + + return presentBounds.reduce((merged, current) => ({ + left: Math.min(merged.left, current.left), + top: Math.min(merged.top, current.top), + right: Math.max(merged.right, current.right), + bottom: Math.max(merged.bottom, current.bottom), + })); +} + export function shouldRestoreHudMousePassthroughAfterDrag( bounds: HudInteractiveBounds | null, clientX: number,