From e120d8cd05a1e27dee450f63b6b1d2b585c0b469 Mon Sep 17 00:00:00 2001 From: wiiiii123 Date: Fri, 10 Jul 2026 20:34:50 +0700 Subject: [PATCH] fix(hud): keep controls visible when recording starts --- src/components/launch/hooks/useHudBarDrag.ts | 42 +++++++++++++++++++ .../launch/hudViewportBounds.test.ts | 35 ++++++++++++++++ src/components/launch/hudViewportBounds.ts | 39 +++++++++++++++++ 3 files changed, 116 insertions(+) create mode 100644 src/components/launch/hudViewportBounds.test.ts create mode 100644 src/components/launch/hudViewportBounds.ts diff --git a/src/components/launch/hooks/useHudBarDrag.ts b/src/components/launch/hooks/useHudBarDrag.ts index 92c294cb..3a8ca515 100644 --- a/src/components/launch/hooks/useHudBarDrag.ts +++ b/src/components/launch/hooks/useHudBarDrag.ts @@ -1,5 +1,6 @@ import { useCallback, useEffect, useRef, useState, type PointerEvent, type RefObject } from "react"; import { mergeHudInteractiveBounds, shouldRestoreHudMousePassthroughAfterDrag } from "../hudMousePassthrough"; +import { clampHudOffsetToViewport } from "../hudViewportBounds"; const DEFAULT_RECORDING_HUD_OFFSET = { x: 0, y: 0 }; @@ -41,6 +42,47 @@ export function useHudBarDrag({ } }, [recordingHudOffset]); + const keepHudBarInsideViewport = useCallback(() => { + if (isHudDraggingRef.current || !hudBarRef.current) { + return; + } + + const bounds = hudBarRef.current.getBoundingClientRect(); + const nextOffset = clampHudOffsetToViewport( + recordingHudOffsetRef.current, + bounds, + { width: window.innerWidth, height: window.innerHeight }, + ); + if ( + nextOffset.x === recordingHudOffsetRef.current.x && + nextOffset.y === recordingHudOffsetRef.current.y + ) { + return; + } + + recordingHudOffsetRef.current = nextOffset; + if (hudBarTransformRef.current) { + hudBarTransformRef.current.style.transform = `translate3d(${nextOffset.x}px, ${nextOffset.y}px, 0)`; + } + setRecordingHudOffset(nextOffset); + }, [hudBarRef]); + + useEffect(() => { + const resizeObserver = + typeof ResizeObserver === "undefined" + ? null + : new ResizeObserver(keepHudBarInsideViewport); + if (hudBarRef.current) { + resizeObserver?.observe(hudBarRef.current); + } + window.addEventListener("resize", keepHudBarInsideViewport); + + return () => { + window.removeEventListener("resize", keepHudBarInsideViewport); + resizeObserver?.disconnect(); + }; + }, [hudBarRef, keepHudBarInsideViewport]); + const handleHudBarPointerDown = useCallback((event: PointerEvent) => { if (event.button !== 0) { return; diff --git a/src/components/launch/hudViewportBounds.test.ts b/src/components/launch/hudViewportBounds.test.ts new file mode 100644 index 00000000..6d998a5d --- /dev/null +++ b/src/components/launch/hudViewportBounds.test.ts @@ -0,0 +1,35 @@ +import { describe, expect, it } from "vitest"; + +import { clampHudOffsetToViewport } from "./hudViewportBounds"; + +describe("clampHudOffsetToViewport", () => { + it("moves a dragged HUD back onscreen after the recording window becomes compact", () => { + expect( + clampHudOffsetToViewport( + { x: -463, y: -710 }, + { left: -244, top: -656, right: 179, bottom: -586 }, + { width: 860, height: 160 }, + ), + ).toEqual({ x: -219, y: -54 }); + }); + + it("leaves an already visible HUD unchanged", () => { + expect( + clampHudOffsetToViewport( + { x: 12, y: -8 }, + { left: 218, top: 54, right: 641, bottom: 124 }, + { width: 860, height: 160 }, + ), + ).toEqual({ x: 12, y: -8 }); + }); + + it("clamps the right and bottom edges after a viewport shrink", () => { + expect( + clampHudOffsetToViewport( + { x: 500, y: 200 }, + { left: 700, top: 140, right: 1120, bottom: 210 }, + { width: 860, height: 160 }, + ), + ).toEqual({ x: 240, y: 150 }); + }); +}); diff --git a/src/components/launch/hudViewportBounds.ts b/src/components/launch/hudViewportBounds.ts new file mode 100644 index 00000000..506e5b3d --- /dev/null +++ b/src/components/launch/hudViewportBounds.ts @@ -0,0 +1,39 @@ +export interface HudOffset { + x: number; + y: number; +} + +export interface HudViewportBounds { + left: number; + top: number; + right: number; + bottom: number; +} + +export interface HudViewportSize { + width: number; + height: number; +} + +function clampOffsetAxis(offset: number, start: number, end: number, viewportSize: number) { + if (start < 0) { + return offset - start; + } + + if (end > viewportSize) { + return offset + viewportSize - end; + } + + return offset; +} + +export function clampHudOffsetToViewport( + offset: HudOffset, + bounds: HudViewportBounds, + viewport: HudViewportSize, +): HudOffset { + return { + x: clampOffsetAxis(offset.x, bounds.left, bounds.right, viewport.width), + y: clampOffsetAxis(offset.y, bounds.top, bounds.bottom, viewport.height), + }; +}