diff --git a/src/components/launch/LaunchWindow.tsx b/src/components/launch/LaunchWindow.tsx index 2c4e6146..a9403304 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,10 @@ import { ContentClamp } from "../ui/content-clamp"; import ProjectBrowserDialog, { type ProjectLibraryEntry, } from "../video-editor/ProjectBrowserDialog"; +import { + mergeHudInteractiveBounds, + shouldRestoreHudMousePassthroughAfterDrag, +} from "./hudMousePassthrough"; import styles from "./LaunchWindow.module.css"; interface DesktopSource { @@ -465,7 +469,26 @@ export function LaunchWindow() { if (event.currentTarget.hasPointerCapture(event.pointerId)) { event.currentTarget.releasePointerCapture(event.pointerId); } - if (wasDragging) { + const hudBounds = mergeHudInteractiveBounds( + [ + dropdownRef.current?.getBoundingClientRect(), + hudBarRef.current?.getBoundingClientRect(), + recordingWebcamPreviewContainerRef.current?.getBoundingClientRect(), + ].map((bounds) => + bounds + ? { + left: bounds.left, + top: bounds.top, + right: bounds.right, + bottom: bounds.bottom, + } + : null, + ), + ); + 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 new file mode 100644 index 00000000..9490b054 --- /dev/null +++ b/src/components/launch/hudMousePassthrough.test.ts @@ -0,0 +1,46 @@ +import { describe, expect, it } from "vitest"; + +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(hudBounds, 180, 230)).toBe(false); + }); + + it("keeps the HUD interactive when the pointer ends on the HUD edge", () => { + 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(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", () => { + 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..5d972f55 --- /dev/null +++ b/src/components/launch/hudMousePassthrough.ts @@ -0,0 +1,39 @@ +export interface HudInteractiveBounds { + left: number; + top: number; + right: number; + 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, + clientY: number, +): boolean { + if (!bounds) { + return true; + } + + return ( + clientX < bounds.left || + clientX > bounds.right || + clientY < bounds.top || + clientY > bounds.bottom + ); +}