From 513e79a8be3821f0256ad0105a118fc53141c109 Mon Sep 17 00:00:00 2001 From: wiiiii123 Date: Sun, 24 May 2026 20:50:49 +0700 Subject: [PATCH 1/2] fix(recording): compact HUD fallback on non-passthrough Windows --- electron/hudOverlayBounds.test.ts | 73 ++++++++++++++++++++++++++ electron/hudOverlayBounds.ts | 35 ++++++++++++ electron/windows.ts | 32 ++++++++--- src/components/launch/LaunchWindow.tsx | 47 ++++++++--------- 4 files changed, 156 insertions(+), 31 deletions(-) create mode 100644 electron/hudOverlayBounds.test.ts create mode 100644 electron/hudOverlayBounds.ts diff --git a/electron/hudOverlayBounds.test.ts b/electron/hudOverlayBounds.test.ts new file mode 100644 index 000000000..cc00daac0 --- /dev/null +++ b/electron/hudOverlayBounds.test.ts @@ -0,0 +1,73 @@ +import { describe, expect, it } from "vitest"; + +import { getHudOverlayWindowBounds } from "./hudOverlayBounds"; + +describe("getHudOverlayWindowBounds", () => { + const workArea = { + x: 120, + y: 40, + width: 1920, + height: 1040, + }; + + it("uses the full work area when mouse passthrough is supported", () => { + expect(getHudOverlayWindowBounds(workArea, true)).toEqual(workArea); + }); + + 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, + }); + }); + + 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, + }); + }); + + it("keeps the compact fallback inside small displays", () => { + expect( + getHudOverlayWindowBounds( + { + x: -100, + y: 20, + width: 640, + height: 420, + }, + false, + ), + ).toEqual({ + x: -100, + y: 280, + width: 640, + height: 160, + }); + }); + + it("fits the expanded fallback inside small displays", () => { + expect( + getHudOverlayWindowBounds( + { + x: -100, + y: 20, + width: 640, + height: 420, + }, + false, + true, + ), + ).toEqual({ + x: -100, + y: 20, + width: 640, + height: 420, + }); + }); +}); diff --git a/electron/hudOverlayBounds.ts b/electron/hudOverlayBounds.ts new file mode 100644 index 000000000..9168281fc --- /dev/null +++ b/electron/hudOverlayBounds.ts @@ -0,0 +1,35 @@ +export interface HudOverlayWorkArea { + x: number; + y: number; + width: number; + 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; + +export function getHudOverlayWindowBounds( + workArea: HudOverlayWorkArea, + mousePassthroughSupported: boolean, + fallbackExpanded = false, +): HudOverlayWorkArea { + if (mousePassthroughSupported) { + return { ...workArea }; + } + + const width = Math.min(workArea.width, NON_PASSTHROUGH_HUD_WIDTH_DIP); + const height = Math.min( + workArea.height, + fallbackExpanded + ? NON_PASSTHROUGH_HUD_EXPANDED_HEIGHT_DIP + : NON_PASSTHROUGH_HUD_COMPACT_HEIGHT_DIP, + ); + + return { + x: Math.round(workArea.x + (workArea.width - width) / 2), + y: Math.round(workArea.y + workArea.height - height), + width, + height, + }; +} diff --git a/electron/windows.ts b/electron/windows.ts index 26b948923..a1a303585 100644 --- a/electron/windows.ts +++ b/electron/windows.ts @@ -5,6 +5,7 @@ import path from "node:path"; import { fileURLToPath } from "node:url"; import { app, BrowserWindow, ipcMain } from "electron"; import { USER_DATA_PATH } from "./appPaths"; +import { getHudOverlayWindowBounds } from "./hudOverlayBounds"; import { getPackagedRendererBaseUrl } from "./rendererServer"; const electronWindowsDir = path.dirname(fileURLToPath(import.meta.url)); @@ -23,6 +24,7 @@ const WINDOW_ICON_PATH = path.join( let hudOverlayWindow: BrowserWindow | null = null; let hudOverlayHiddenFromCapture = true; let hudOverlayCaptureProtectionLoaded = false; +let hudOverlayFallbackExpanded = false; let countdownWindow: BrowserWindow | null = null; let updateToastWindow: BrowserWindow | null = null; @@ -183,12 +185,11 @@ function getHudOverlayDisplay() { function getHudOverlayBounds() { const { workArea } = getHudOverlayDisplay(); - return { - x: workArea.x, - y: workArea.y, - width: workArea.width, - height: workArea.height, - }; + return getHudOverlayWindowBounds( + workArea, + isHudOverlayMousePassthroughSupported(), + hudOverlayFallbackExpanded, + ); } function applyHudOverlayBounds() { @@ -242,9 +243,27 @@ function positionUpdateToastWindow() { updateToastWindow.moveTop(); } +function setHudOverlayFallbackExpanded(expanded: boolean) { + hudOverlayFallbackExpanded = expanded; + if ( + !hudOverlayWindow || + hudOverlayWindow.isDestroyed() || + isHudOverlayMousePassthroughSupported() + ) { + return; + } + + hudOverlayWindow.setBounds(getHudOverlayBounds(), false); + positionUpdateToastWindow(); + if (hudOverlayWindow.isVisible()) { + hudOverlayWindow.moveTop(); + } +} + ipcMain.on("hud-overlay-set-ignore-mouse", (_event, ignore: boolean) => { if (hudOverlayWindow && !hudOverlayWindow.isDestroyed()) { if (!isHudOverlayMousePassthroughSupported()) { + setHudOverlayFallbackExpanded(!ignore); hudOverlayWindow.setIgnoreMouseEvents(false); return; } @@ -358,6 +377,7 @@ ipcMain.handle("set-hud-overlay-capture-protection", (_event, enabled: boolean) export function createHudOverlayWindow(): BrowserWindow { loadHudOverlayCaptureProtectionSetting(); + hudOverlayFallbackExpanded = false; const initialBounds = getHudOverlayBounds(); let hasShownHudWindow = false; diff --git a/src/components/launch/LaunchWindow.tsx b/src/components/launch/LaunchWindow.tsx index 49547069d..2158d7a27 100644 --- a/src/components/launch/LaunchWindow.tsx +++ b/src/components/launch/LaunchWindow.tsx @@ -1,46 +1,45 @@ import { + ArrowClockwiseIcon, CaretUpIcon, + DotsThreeVerticalIcon, MicrophoneIcon, MicrophoneSlashIcon, MinusIcon, MonitorIcon, - DotsThreeVerticalIcon, TimerIcon, VideoCameraIcon, VideoCameraSlashIcon, XIcon, - ArrowClockwiseIcon, } from "@phosphor-icons/react"; import { AnimatePresence, motion } from "motion/react"; +import { useEffect, useRef } from "react"; import { RxDragHandleDots2 } from "react-icons/rx"; +import { Separator } from "@/components/ui/separator"; import { useScopedT } from "../../contexts/I18nContext"; -import { useHudBarDrag } from "./hooks/useHudBarDrag"; import { useMicrophoneDevices } from "../../hooks/useMicrophoneDevices"; -import { useLaunchWindowSystemState } from "./hooks/useLaunchWindowSystemState"; -import { useLaunchHudInteractionState } from "./hooks/useLaunchHudInteractionState"; -import { useLaunchWindowActions } from "./hooks/useLaunchWindowActions"; -import { useRecordingTimer } from "./hooks/useRecordingTimer"; import { useScreenRecorder } from "../../hooks/useScreenRecorder"; import { useVideoDevices } from "../../hooks/useVideoDevices"; -import { useWebcamPreviewOverlay } from "./hooks/useWebcamPreviewOverlay"; +import { Button } from "../ui/button"; +import { HudInteractionContext } from "./contexts/HudInteractionContext"; import { canToggleFloatingWebcamPreview, } from "./floatingWebcamPreview"; -import { LaunchPopoverCoordinatorProvider, useLaunchPopoverCoordinator } from "./popovers/LaunchPopoverCoordinator"; +import { useHudBarDrag } from "./hooks/useHudBarDrag"; +import { useLaunchHudInteractionState } from "./hooks/useLaunchHudInteractionState"; +import { useLaunchWindowActions } from "./hooks/useLaunchWindowActions"; +import { useLaunchWindowSystemState } from "./hooks/useLaunchWindowSystemState"; +import { useRecordingTimer } from "./hooks/useRecordingTimer"; +import { useWebcamPreviewOverlay } from "./hooks/useWebcamPreviewOverlay"; +import styles from "./LaunchWindow.module.css"; import { CountdownPopover } from "./popovers/CountdownPopover"; +import { LaunchPopoverCoordinatorProvider, useLaunchPopoverCoordinator } from "./popovers/LaunchPopoverCoordinator"; import { MicPopover } from "./popovers/MicPopover"; import { MorePopover } from "./popovers/MorePopover"; import { ProjectPopover } from "./popovers/ProjectPopover"; import { SourcePopover } from "./popovers/SourcePopover"; import { WebcamPopover } from "./popovers/WebcamPopover"; -import { HudInteractionContext } from "./contexts/HudInteractionContext"; -import { MarqueeText } from "./SourceSelector"; -import styles from "./LaunchWindow.module.css"; - -import { Separator } from "@/components/ui/separator"; -import { Button } from "../ui/button"; import { RecordingControls } from "./RecordingControls"; -import { useEffect, useRef } from "react"; +import { MarqueeText } from "./SourceSelector"; const SHOW_DEV_UPDATE_PREVIEW = import.meta.env.DEV; @@ -427,6 +426,9 @@ function LaunchWindowContent() { ); const hudMode = finalizing ? "finalizing" : recording ? "recording" : "idle"; + const useNativeHudBarDrag = + (platform === "linux" || hudOverlayMousePassthroughSupported === false) && + !showRecordingWebcamPreview; return ( @@ -456,16 +458,11 @@ function LaunchWindowContent() { className={`${styles.bar} launch-theme mb-2`} >
Date: Sun, 24 May 2026 20:59:03 +0700 Subject: [PATCH 2/2] fix(recording): preserve compact HUD drag position --- electron/hudOverlayBounds.test.ts | 74 +++++++++++++++++++++++++- electron/hudOverlayBounds.ts | 20 +++++++ electron/windows.ts | 13 ++++- src/components/launch/LaunchWindow.tsx | 3 +- 4 files changed, 105 insertions(+), 5 deletions(-) diff --git a/electron/hudOverlayBounds.test.ts b/electron/hudOverlayBounds.test.ts index cc00daac0..dac1058c8 100644 --- a/electron/hudOverlayBounds.test.ts +++ b/electron/hudOverlayBounds.test.ts @@ -1,6 +1,9 @@ import { describe, expect, it } from "vitest"; -import { getHudOverlayWindowBounds } from "./hudOverlayBounds"; +import { + getHudOverlayWindowBounds, + resizeHudOverlayFallbackBounds, +} from "./hudOverlayBounds"; describe("getHudOverlayWindowBounds", () => { const workArea = { @@ -71,3 +74,72 @@ describe("getHudOverlayWindowBounds", () => { }); }); }); + +describe("resizeHudOverlayFallbackBounds", () => { + const workArea = { + x: 0, + y: 0, + width: 1920, + height: 1080, + }; + + it("preserves the dragged bottom edge when expanding", () => { + expect( + resizeHudOverlayFallbackBounds( + workArea, + { + x: 420, + y: 700, + width: 860, + height: 160, + }, + true, + ), + ).toEqual({ + x: 420, + y: 320, + width: 860, + height: 540, + }); + }); + + it("preserves the dragged bottom edge when compacting", () => { + expect( + resizeHudOverlayFallbackBounds( + workArea, + { + x: 420, + y: 320, + width: 860, + height: 540, + }, + false, + ), + ).toEqual({ + x: 420, + y: 700, + width: 860, + height: 160, + }); + }); + + it("keeps resized fallback bounds inside the display work area", () => { + expect( + resizeHudOverlayFallbackBounds( + workArea, + { + x: 1500, + y: 900, + width: 860, + height: 160, + }, + true, + ), + ).toEqual({ + x: 1060, + y: 520, + width: 860, + height: 540, + }); + }); +}); diff --git a/electron/hudOverlayBounds.ts b/electron/hudOverlayBounds.ts index 9168281fc..b8800e30d 100644 --- a/electron/hudOverlayBounds.ts +++ b/electron/hudOverlayBounds.ts @@ -9,6 +9,10 @@ const NON_PASSTHROUGH_HUD_WIDTH_DIP = 860; const NON_PASSTHROUGH_HUD_COMPACT_HEIGHT_DIP = 160; const NON_PASSTHROUGH_HUD_EXPANDED_HEIGHT_DIP = 540; +function clamp(value: number, min: number, max: number): number { + return Math.min(Math.max(value, min), max); +} + export function getHudOverlayWindowBounds( workArea: HudOverlayWorkArea, mousePassthroughSupported: boolean, @@ -33,3 +37,19 @@ export function getHudOverlayWindowBounds( height, }; } + +export function resizeHudOverlayFallbackBounds( + workArea: HudOverlayWorkArea, + currentBounds: HudOverlayWorkArea, + fallbackExpanded: boolean, +): HudOverlayWorkArea { + const nextBounds = getHudOverlayWindowBounds(workArea, false, fallbackExpanded); + const maxX = workArea.x + workArea.width - nextBounds.width; + const maxY = workArea.y + workArea.height - nextBounds.height; + + return { + ...nextBounds, + x: clamp(currentBounds.x, workArea.x, maxX), + y: clamp(currentBounds.y + currentBounds.height - nextBounds.height, workArea.y, maxY), + }; +} diff --git a/electron/windows.ts b/electron/windows.ts index a1a303585..dbdb4692d 100644 --- a/electron/windows.ts +++ b/electron/windows.ts @@ -5,7 +5,10 @@ import path from "node:path"; import { fileURLToPath } from "node:url"; import { app, BrowserWindow, ipcMain } from "electron"; import { USER_DATA_PATH } from "./appPaths"; -import { getHudOverlayWindowBounds } from "./hudOverlayBounds"; +import { + getHudOverlayWindowBounds, + resizeHudOverlayFallbackBounds, +} from "./hudOverlayBounds"; import { getPackagedRendererBaseUrl } from "./rendererServer"; const electronWindowsDir = path.dirname(fileURLToPath(import.meta.url)); @@ -253,7 +256,13 @@ function setHudOverlayFallbackExpanded(expanded: boolean) { return; } - hudOverlayWindow.setBounds(getHudOverlayBounds(), false); + const { workArea } = getHudOverlayDisplay(); + const nextBounds = resizeHudOverlayFallbackBounds( + workArea, + hudOverlayWindow.getBounds(), + expanded, + ); + hudOverlayWindow.setBounds(nextBounds, false); positionUpdateToastWindow(); if (hudOverlayWindow.isVisible()) { hudOverlayWindow.moveTop(); diff --git a/src/components/launch/LaunchWindow.tsx b/src/components/launch/LaunchWindow.tsx index 2158d7a27..b3c924760 100644 --- a/src/components/launch/LaunchWindow.tsx +++ b/src/components/launch/LaunchWindow.tsx @@ -427,8 +427,7 @@ function LaunchWindowContent() { const hudMode = finalizing ? "finalizing" : recording ? "recording" : "idle"; const useNativeHudBarDrag = - (platform === "linux" || hudOverlayMousePassthroughSupported === false) && - !showRecordingWebcamPreview; + platform === "linux" || hudOverlayMousePassthroughSupported === false; return (