diff --git a/electron/electron-env.d.ts b/electron/electron-env.d.ts index 303dd72a..7fc9b092 100644 --- a/electron/electron-env.d.ts +++ b/electron/electron-env.d.ts @@ -27,6 +27,7 @@ interface Window { hudOverlayHide: () => void; hudOverlayClose: () => void; setHudOverlayExpanded: (expanded: boolean) => void; + setHudOverlayCompactWidth: (width: number) => void; getHudOverlayCaptureProtection: () => Promise<{ success: boolean; enabled: boolean }>; setHudOverlayCaptureProtection: ( enabled: boolean, diff --git a/electron/preload.ts b/electron/preload.ts index 8f5cf2a4..94080f7e 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -10,6 +10,9 @@ contextBridge.exposeInMainWorld("electronAPI", { setHudOverlayExpanded: (expanded: boolean) => { ipcRenderer.send("set-hud-overlay-expanded", expanded); }, + setHudOverlayCompactWidth: (width: number) => { + ipcRenderer.send("set-hud-overlay-compact-width", width); + }, getHudOverlayCaptureProtection: () => { return ipcRenderer.invoke("get-hud-overlay-capture-protection"); }, diff --git a/electron/windows.ts b/electron/windows.ts index f9f047f5..f6d8aaf0 100644 --- a/electron/windows.ts +++ b/electron/windows.ts @@ -26,11 +26,14 @@ const HUD_BOTTOM_CLEARANCE_CM = 3.5; const DIP_PER_INCH = 96; const CM_PER_INCH = 2.54; const HUD_EDGE_MARGIN_DIP = 16; -const HUD_SHADOW_BLEED_DIP = 20; -const HUD_WINDOW_WIDTH = 560; +const HUD_SHADOW_BLEED_DIP = 36; +const HUD_MIN_WINDOW_WIDTH = 560; const HUD_COMPACT_HEIGHT = 96; const HUD_EXPANDED_HEIGHT = 520 + HUD_SHADOW_BLEED_DIP; +let hudOverlayExpanded = false; +let hudOverlayCompactWidth = HUD_MIN_WINDOW_WIDTH; + function isHudOverlayCaptureProtectionSupported(): boolean { return process.platform !== "linux"; } @@ -78,7 +81,11 @@ function getScreen() { function getHudOverlayBounds(expanded: boolean) { const primaryDisplay = getScreen().getPrimaryDisplay(); const { bounds, workArea } = primaryDisplay; - const windowWidth = HUD_WINDOW_WIDTH; + const maxWindowWidth = Math.max(HUD_MIN_WINDOW_WIDTH, workArea.width - HUD_EDGE_MARGIN_DIP * 2); + const windowWidth = Math.min( + maxWindowWidth, + Math.max(HUD_MIN_WINDOW_WIDTH, Math.round(hudOverlayCompactWidth)), + ); const windowHeight = expanded ? HUD_EXPANDED_HEIGHT : HUD_COMPACT_HEIGHT; const bottomClearanceDip = Math.round((HUD_BOTTOM_CLEARANCE_CM / CM_PER_INCH) * DIP_PER_INCH); const screenBottom = bounds.y + bounds.height; @@ -106,6 +113,8 @@ function applyHudOverlayBounds(expanded: boolean) { return; } + hudOverlayExpanded = expanded; + hudOverlayWindow.setBounds(getHudOverlayBounds(expanded), false); if (!hudOverlayWindow.isVisible()) { return; @@ -123,6 +132,26 @@ ipcMain.on("set-hud-overlay-expanded", (_event, expanded: boolean) => { applyHudOverlayBounds(Boolean(expanded)); }); +ipcMain.on("set-hud-overlay-compact-width", (_event, width: number) => { + if (!Number.isFinite(width)) { + return; + } + + const primaryDisplay = getScreen().getPrimaryDisplay(); + const maxWindowWidth = Math.max( + HUD_MIN_WINDOW_WIDTH, + primaryDisplay.workArea.width - HUD_EDGE_MARGIN_DIP * 2, + ); + const nextWidth = Math.min(maxWindowWidth, Math.max(HUD_MIN_WINDOW_WIDTH, Math.round(width))); + + if (nextWidth === hudOverlayCompactWidth) { + return; + } + + hudOverlayCompactWidth = nextWidth; + applyHudOverlayBounds(hudOverlayExpanded); +}); + ipcMain.handle("get-hud-overlay-capture-protection", () => { const enabled = loadHudOverlayCaptureProtectionSetting(); @@ -158,8 +187,7 @@ export function createHudOverlayWindow(): BrowserWindow { const win = new BrowserWindow({ width: initialBounds.width, height: initialBounds.height, - minWidth: HUD_WINDOW_WIDTH, - maxWidth: HUD_WINDOW_WIDTH, + minWidth: HUD_MIN_WINDOW_WIDTH, minHeight: HUD_COMPACT_HEIGHT, maxHeight: HUD_EXPANDED_HEIGHT, x: initialBounds.x, diff --git a/src/components/launch/LaunchWindow.module.css b/src/components/launch/LaunchWindow.module.css index adc4dd6f..bfa9de88 100644 --- a/src/components/launch/LaunchWindow.module.css +++ b/src/components/launch/LaunchWindow.module.css @@ -9,13 +9,16 @@ .bar { display: flex; align-items: center; - gap: 8px; + gap: 10px; + width: max-content; + max-width: 1200px; background: rgba(18, 18, 24, 0.97); border: 1px solid rgba(255, 255, 255, 0.07); - border-radius: 18px; - padding: 10px 14px; + border-radius: 20px; + padding: 11px 20px 11px 18px; box-shadow: - 0 8px 40px rgba(0, 0, 0, 0.45), + 0 10px 30px rgba(0, 0, 0, 0.24), + 0 2px 10px rgba(0, 0, 0, 0.12), inset 0 1px 0 rgba(255, 255, 255, 0.04); overflow: visible; position: relative; @@ -26,16 +29,19 @@ position: relative; display: flex; align-items: center; + flex: 0 1 auto; min-height: 46px; - min-width: 0; + min-width: fit-content; + padding-right: 2px; } .barState { display: flex; align-items: center; gap: 8px; + flex: 0 1 auto; white-space: nowrap; - min-width: 0; + min-width: fit-content; pointer-events: auto; } @@ -43,7 +49,7 @@ width: 1px; height: 26px; background: #2a2a34; - margin: 0 4px; + margin: 0 6px; flex-shrink: 0; } @@ -98,6 +104,8 @@ align-items: center; gap: 7px; height: 40px; + min-width: 0; + max-width: 640px; padding: 0 14px 0 12px; border-radius: 11px; border: 1px solid #2a2a34; @@ -107,7 +115,7 @@ font-weight: 500; cursor: pointer; transition: all 0.15s ease; - flex-shrink: 0; + flex: 0 1 auto; } .screenSel:hover { @@ -115,13 +123,22 @@ background: #20202a; } +.sourceLabel { + display: inline-block; + min-width: 0; + max-width: 520px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + .menuArea { flex: 1; display: flex; flex-direction: column; align-items: center; pointer-events: none; - overflow: hidden; + overflow: visible; min-height: 0; } @@ -135,7 +152,9 @@ padding: 8px; margin-top: auto; margin-bottom: 8px; - box-shadow: 0 12px 48px rgba(0, 0, 0, 0.5); + box-shadow: + 0 12px 32px rgba(0, 0, 0, 0.22), + 0 2px 10px rgba(0, 0, 0, 0.1); pointer-events: auto; animation: menuCardIn 0.18s ease; } diff --git a/src/components/launch/LaunchWindow.tsx b/src/components/launch/LaunchWindow.tsx index c8ec7135..0ab81fa2 100644 --- a/src/components/launch/LaunchWindow.tsx +++ b/src/components/launch/LaunchWindow.tsx @@ -175,6 +175,7 @@ export function LaunchWindow() { const [hideHudFromCapture, setHideHudFromCapture] = useState(true); const [platform, setPlatform] = useState(null); const dropdownRef = useRef(null); + const hudBarRef = useRef(null); const webcamPreviewRef = useRef(null); const micDropdownOpen = activeDropdown === "mic"; @@ -358,6 +359,43 @@ export function LaunchWindow() { }; }, [activeDropdown]); + useEffect(() => { + const hudBar = hudBarRef.current; + if (!hudBar || typeof ResizeObserver === "undefined") { + return; + } + + let frameId = 0; + const reportWidth = () => { + frameId = 0; + const measuredWidth = Math.ceil( + Math.max(hudBar.getBoundingClientRect().width, hudBar.scrollWidth) + 24, + ); + window.electronAPI.setHudOverlayCompactWidth(measuredWidth); + }; + + const scheduleWidthReport = () => { + if (frameId !== 0) { + cancelAnimationFrame(frameId); + } + frameId = requestAnimationFrame(reportWidth); + }; + + scheduleWidthReport(); + + const resizeObserver = new ResizeObserver(() => { + scheduleWidthReport(); + }); + resizeObserver.observe(hudBar); + + return () => { + resizeObserver.disconnect(); + if (frameId !== 0) { + cancelAnimationFrame(frameId); + } + }; + }, [selectedSource, recording, paused, activeDropdown]); + useEffect(() => { const handleClick = (e: MouseEvent) => { if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) { @@ -528,7 +566,9 @@ export function LaunchWindow() { title={selectedSource} > - {selectedSource} + + {selectedSource} + @@ -805,6 +845,7 @@ export function LaunchWindow() {