From 6df02e847d7fa894cf994764bdfc48643523abc7 Mon Sep 17 00:00:00 2001 From: Alan Trebugeais Date: Wed, 6 May 2026 23:12:03 +0200 Subject: [PATCH] fix: overflow, add window in fullscreen mode to avoid jittering when opening up select... add: launchTheme.css for consistent styling accross the whole components spectrum related to LaunchWindow add: correct styles accross the popovers! with unstyled parameters in Popover.tsx because it was needed to make sure its the same --- electron/windows.ts | 135 ++-------- src/App.tsx | 7 +- src/components/launch/LaunchHudPopovers.tsx | 4 +- src/components/launch/LaunchWindow.module.css | 24 +- src/components/launch/LaunchWindow.tsx | 235 +++++------------- src/components/launch/SourceSelector.css | 58 +++++ src/components/launch/SourceSelector.tsx | 42 ++-- src/components/launch/launchTheme.css | 13 + src/components/ui/popover.tsx | 6 +- src/i18n/locales/en/launch.json | 2 + src/i18n/locales/es/launch.json | 2 + src/i18n/locales/fr/launch.json | 2 + src/i18n/locales/ko/launch.json | 2 + src/i18n/locales/nl/launch.json | 2 + src/i18n/locales/pt-BR/launch.json | 2 + src/i18n/locales/zh-CN/launch.json | 2 + src/i18n/locales/zh-TW/launch.json | 4 +- src/index.css | 7 +- 18 files changed, 226 insertions(+), 323 deletions(-) create mode 100644 src/components/launch/launchTheme.css diff --git a/electron/windows.ts b/electron/windows.ts index 6f51f742..37f4853b 100644 --- a/electron/windows.ts +++ b/electron/windows.ts @@ -26,23 +26,11 @@ let countdownWindow: BrowserWindow | null = null; let updateToastWindow: BrowserWindow | null = null; const HUD_OVERLAY_SETTINGS_FILE = path.join(USER_DATA_PATH, "hud-overlay-settings.json"); -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 = 36; -const HUD_MIN_WINDOW_WIDTH = 560; -const HUD_COMPACT_HEIGHT = 96; -const HUD_MIN_EXPANDED_HEIGHT = 520 + HUD_SHADOW_BLEED_DIP; const UPDATE_TOAST_WIDTH = 456; const UPDATE_TOAST_HEIGHT = 252; const UPDATE_TOAST_GAP_DIP = 18; -let hudOverlayExpanded = false; -let hudOverlayCompactWidth = HUD_MIN_WINDOW_WIDTH; -let hudOverlayCompactHeight = HUD_COMPACT_HEIGHT; -let hudOverlayExpandedHeight = HUD_MIN_EXPANDED_HEIGHT; - function getEditorWindowQuery(): Record { const query: Record = { windowType: "editor", @@ -182,61 +170,21 @@ function getHudOverlayDisplay() { return getScreen().getPrimaryDisplay(); } -function getHudOverlayBounds(expanded: boolean) { - const { bounds, workArea } = getHudOverlayDisplay(); - 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 maxWindowHeight = Math.max(HUD_COMPACT_HEIGHT, workArea.height - HUD_EDGE_MARGIN_DIP * 2); - const desiredHeight = expanded - ? Math.max(HUD_MIN_EXPANDED_HEIGHT, Math.round(hudOverlayExpandedHeight)) - : Math.max(HUD_COMPACT_HEIGHT, Math.round(hudOverlayCompactHeight)); - const windowHeight = Math.min(maxWindowHeight, desiredHeight); - const bottomClearanceDip = Math.round((HUD_BOTTOM_CLEARANCE_CM / CM_PER_INCH) * DIP_PER_INCH); - const screenBottom = bounds.y + bounds.height; - const workAreaBottom = workArea.y + workArea.height; - const preferredBottom = screenBottom - bottomClearanceDip; - const maximumSafeBottom = workAreaBottom - HUD_EDGE_MARGIN_DIP; - const windowBottom = Math.min(preferredBottom, maximumSafeBottom); - - const x = Math.floor(workArea.x + (workArea.width - windowWidth) / 2); - const y = Math.max(workArea.y + HUD_EDGE_MARGIN_DIP, Math.floor(windowBottom - windowHeight)); - +function getHudOverlayBounds() { + const { workArea } = getHudOverlayDisplay(); return { - x, - y, - width: windowWidth, - height: windowHeight, + x: workArea.x, + y: workArea.y, + width: workArea.width, + height: workArea.height, }; } -function applyHudOverlayBounds(expanded: boolean) { +function applyHudOverlayBounds() { if (!hudOverlayWindow || hudOverlayWindow.isDestroyed()) { return; } - - hudOverlayExpanded = expanded; - - const computed = getHudOverlayBounds(expanded); - - if (hudUserPosition) { - // Resize in-place at the user's dragged position, clamped so the - // window stays fully within the current display's work area. - const { workArea } = getHudOverlayDisplay(); - const x = Math.max( - workArea.x, - Math.min(hudUserPosition.x, workArea.x + workArea.width - computed.width), - ); - const y = Math.max( - workArea.y, - Math.min(hudUserPosition.y, workArea.y + workArea.height - computed.height), - ); - hudOverlayWindow.setBounds({ x, y, width: computed.width, height: computed.height }, false); - } else { - hudOverlayWindow.setBounds(computed, false); - } + hudOverlayWindow.setBounds(getHudOverlayBounds(), false); positionUpdateToastWindow(); if (!hudOverlayWindow.isVisible()) { @@ -299,9 +247,7 @@ ipcMain.on("hud-overlay-set-ignore-mouse", (_event, ignore: boolean) => { } }); -// When the user drags the HUD, remember their chosen position so that -// subsequent size changes (e.g. idle → recording UI swap) resize in-place -// instead of snapping back to the default centered location. +// Keep compatibility with existing drag IPC/state. let hudUserPosition: { x: number; y: number } | null = null; let hudDragOffset: { x: number; y: number } | null = null; let hudDragLastCursor: { x: number; y: number } | null = null; @@ -315,7 +261,7 @@ ipcMain.on("hud-overlay-drag", (_event, phase: string, screenX: number, screenY: // the HUD appears "stuck". The renderer marks the drag handle as // -webkit-app-region: drag on Linux, letting the OS move the window for us. // The resulting position is captured by the win.on("moved", ...) listener - // below so `hudUserPosition` stays in sync for in-place resize. + // below so `hudUserPosition` stays in sync. if (process.platform === "linux") { return; } @@ -365,52 +311,19 @@ ipcMain.on("hud-overlay-hide", () => { }); ipcMain.on("set-hud-overlay-expanded", (_event, expanded: boolean) => { - applyHudOverlayBounds(Boolean(expanded)); + // Compatibility no-op for launcher HUD: window bounds are now fixed to workArea. + void expanded; }); ipcMain.on("set-hud-overlay-compact-width", (_event, width: number) => { - if (!Number.isFinite(width)) { - return; - } - - const maxWindowWidth = Math.max( - HUD_MIN_WINDOW_WIDTH, - getHudOverlayDisplay().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); + // Compatibility no-op for launcher HUD: window bounds are now fixed to workArea. + void width; }); ipcMain.on("set-hud-overlay-measured-height", (_event, height: number, expanded: boolean) => { - if (!Number.isFinite(height)) { - return; - } - - const maxWindowHeight = Math.max( - HUD_COMPACT_HEIGHT, - getHudOverlayDisplay().workArea.height - HUD_EDGE_MARGIN_DIP * 2, - ); - const nextHeight = Math.min(maxWindowHeight, Math.max(HUD_COMPACT_HEIGHT, Math.round(height))); - - if (expanded) { - if (nextHeight === hudOverlayExpandedHeight) { - return; - } - hudOverlayExpandedHeight = Math.max(HUD_MIN_EXPANDED_HEIGHT, nextHeight); - } else { - if (nextHeight === hudOverlayCompactHeight) { - return; - } - hudOverlayCompactHeight = nextHeight; - } - - applyHudOverlayBounds(hudOverlayExpanded); + // Compatibility no-op for launcher HUD: window bounds are now fixed to workArea. + void height; + void expanded; }); ipcMain.handle("get-hud-overlay-capture-protection", () => { @@ -450,17 +363,11 @@ ipcMain.handle("set-hud-overlay-capture-protection", (_event, enabled: boolean) export function createHudOverlayWindow(): BrowserWindow { loadHudOverlayCaptureProtectionSetting(); - const initialBounds = getHudOverlayBounds(false); + const initialBounds = getHudOverlayBounds(); const win = new BrowserWindow({ width: initialBounds.width, height: initialBounds.height, - minWidth: HUD_MIN_WINDOW_WIDTH, - minHeight: HUD_COMPACT_HEIGHT, - maxHeight: Math.max( - HUD_COMPACT_HEIGHT, - getHudOverlayDisplay().workArea.height - HUD_EDGE_MARGIN_DIP * 2, - ), x: initialBounds.x, y: initialBounds.y, frame: false, @@ -538,9 +445,7 @@ export function createHudOverlayWindow(): BrowserWindow { hudOverlayWindow = win; // On Linux the HUD is dragged by the OS via -webkit-app-region (Wayland - // forbids client-side positioning). Mirror the resulting bounds into - // hudUserPosition so subsequent expand/collapse resizes stay in place - // instead of snapping back to the default centered spot. + // forbids client-side positioning). Mirror moved bounds into drag state. if (process.platform === "linux") { win.on("moved", () => { if (win.isDestroyed()) return; @@ -569,7 +474,7 @@ export function createHudOverlayWindow(): BrowserWindow { hudUserPosition = null; } } - applyHudOverlayBounds(hudOverlayExpanded); + applyHudOverlayBounds(); }; screen.on("display-removed", handleDisplayRemoved); screen.on("display-metrics-changed", handleDisplayMetricsChanged); diff --git a/src/App.tsx b/src/App.tsx index fa6e9d52..8c590dca 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -19,6 +19,7 @@ export default function App() { const type = params.get("windowType") || ""; const isMacOS = /mac/i.test(navigator.platform); setWindowType(type); + document.documentElement.dataset.windowType = type; if ( type === "hud-overlay" || @@ -31,7 +32,11 @@ export default function App() { document.getElementById("root")?.style.setProperty("background", "transparent"); } - if (type === "hud-overlay" || type === "update-toast") { + if (type === "hud-overlay") { + document.documentElement.classList.add("hud-overlay-window"); + document.body.classList.add("hud-overlay-window"); + document.getElementById("root")?.classList.add("hud-overlay-window"); + } else if (type === "update-toast") { document.documentElement.style.overflow = "visible"; document.body.style.overflow = "visible"; document.getElementById("root")?.style.setProperty("overflow", "visible"); diff --git a/src/components/launch/LaunchHudPopovers.tsx b/src/components/launch/LaunchHudPopovers.tsx index 27bf745c..007fea2c 100644 --- a/src/components/launch/LaunchHudPopovers.tsx +++ b/src/components/launch/LaunchHudPopovers.tsx @@ -21,6 +21,7 @@ import { SUPPORTED_LOCALES } from "@/i18n/config"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { useScopedT } from "../../contexts/I18nContext"; import { AudioLevelMeter } from "../ui/audio-level-meter"; +import "./launchTheme.css"; import { SourceSelector } from "./SourceSelector"; import styles from "./LaunchWindow.module.css"; @@ -123,7 +124,8 @@ function HudPopover({ {trigger} (null); @@ -113,6 +113,8 @@ export function LaunchWindow() { const [appVersion, setAppVersion] = useState(null); const hudContentRef = useRef(null); const hudBarRef = useRef(null); + const hudBarTransformRef = useRef(null); + const recordingHudOffsetRef = useRef(DEFAULT_RECORDING_HUD_OFFSET); const webcamPreviewRef = useRef(null); const recordingWebcamPreviewRef = useRef(null); const recordingWebcamPreviewContainerRef = useRef(null); @@ -132,7 +134,6 @@ export function LaunchWindow() { const hudDragStartRef = useRef< | { pointerId: number; - mode: "webcam-preview"; startX: number; startY: number; originX: number; @@ -142,18 +143,13 @@ export function LaunchWindow() { hudWidth: number; hudHeight: number; } - | { - pointerId: number; - mode: "overlay"; - } | null >(null); const isHudDraggingRef = useRef(false); const isWebcamPreviewDraggingRef = useRef(false); const hudDragMoveRafRef = useRef(null); - const hudDragPendingScreenPositionRef = useRef<{ x: number; y: number } | null>(null); + const hudDragPendingPointerRef = useRef<{ clientX: number; clientY: number } | null>(null); - const [isAtTop, setIsAtTop] = useState(false); const anyPopoverOpen = sourcePopoverOpen || micPopoverOpen || @@ -169,18 +165,6 @@ export function LaunchWindow() { setMorePopoverOpen(false); }, []); - useEffect(() => { - const checkPosition = () => { - const screenHeight = window.screen.availHeight; - const windowY = window.screenY; - setIsAtTop(windowY < screenHeight / 2); - }; - checkPosition(); - if (anyPopoverOpen || projectBrowserOpen) { - checkPosition(); - } - }, [anyPopoverOpen, projectBrowserOpen]); - const showWebcamControls = webcamEnabled && !recording; const showRecordingWebcamPreview = webcamEnabled && @@ -219,7 +203,6 @@ export function LaunchWindow() { useEffect(() => { if (!webcamEnabled) { setWebcamPreviewOffset(DEFAULT_WEBCAM_PREVIEW_OFFSET); - setRecordingHudOffset(DEFAULT_RECORDING_HUD_OFFSET); webcamPreviewDragStartRef.current = null; isWebcamPreviewDraggingRef.current = false; setShowFloatingWebcamPreview(true); @@ -227,10 +210,11 @@ export function LaunchWindow() { }, [webcamEnabled]); useEffect(() => { - if (!showRecordingWebcamPreview) { - setRecordingHudOffset(DEFAULT_RECORDING_HUD_OFFSET); + recordingHudOffsetRef.current = recordingHudOffset; + if (!isHudDraggingRef.current && hudBarTransformRef.current) { + hudBarTransformRef.current.style.transform = `translate3d(${recordingHudOffset.x}px, ${recordingHudOffset.y}px, 0)`; } - }, [showRecordingWebcamPreview]); + }, [recordingHudOffset]); const handleWebcamPreviewPointerDown = (event: React.PointerEvent) => { if (event.button !== 0) { @@ -318,31 +302,23 @@ export function LaunchWindow() { event.preventDefault(); event.currentTarget.setPointerCapture(event.pointerId); isHudDraggingRef.current = true; + setIsHudDragging(true); window.electronAPI?.hudOverlaySetIgnoreMouse?.(false); - - if (showRecordingWebcamPreview && hudBarRef.current) { - const hudRect = hudBarRef.current.getBoundingClientRect(); - hudDragStartRef.current = { - pointerId: event.pointerId, - mode: "webcam-preview", - startX: event.clientX, - startY: event.clientY, - originX: recordingHudOffset.x, - originY: recordingHudOffset.y, - initialLeft: hudRect.left, - initialTop: hudRect.top, - hudWidth: hudRect.width, - hudHeight: hudRect.height, - }; + if (!hudBarRef.current) { return; } - + const hudRect = hudBarRef.current.getBoundingClientRect(); hudDragStartRef.current = { pointerId: event.pointerId, - mode: "overlay", + startX: event.clientX, + startY: event.clientY, + originX: recordingHudOffsetRef.current.x, + originY: recordingHudOffsetRef.current.y, + initialLeft: hudRect.left, + initialTop: hudRect.top, + hudWidth: hudRect.width, + hudHeight: hudRect.height, }; - hudDragPendingScreenPositionRef.current = null; - window.electronAPI?.hudOverlayDrag?.("start", event.screenX, event.screenY); }; const handleHudBarPointerMove = (event: React.PointerEvent) => { @@ -351,42 +327,42 @@ export function LaunchWindow() { return; } - if (dragState.mode === "webcam-preview") { - const deltaX = event.clientX - dragState.startX; - const deltaY = event.clientY - dragState.startY; - const viewportWidth = Math.max(window.innerWidth, window.screen?.width ?? 0); - const viewportHeight = Math.max(window.innerHeight, window.screen?.height ?? 0); - const unclampedLeft = dragState.initialLeft + deltaX; - const unclampedTop = dragState.initialTop + deltaY; - const clampedLeft = Math.min( - Math.max(0, unclampedLeft), - Math.max(0, viewportWidth - dragState.hudWidth), - ); - const clampedTop = Math.min( - Math.max(0, unclampedTop), - Math.max(0, viewportHeight - dragState.hudHeight), - ); - - setRecordingHudOffset({ - x: dragState.originX + (clampedLeft - dragState.initialLeft), - y: dragState.originY + (clampedTop - dragState.initialTop), - }); - return; - } - - hudDragPendingScreenPositionRef.current = { x: event.screenX, y: event.screenY }; + hudDragPendingPointerRef.current = { clientX: event.clientX, clientY: event.clientY }; if (hudDragMoveRafRef.current !== null) { return; } hudDragMoveRafRef.current = requestAnimationFrame(() => { hudDragMoveRafRef.current = null; - const position = hudDragPendingScreenPositionRef.current; - if (!position) { + const latestDragState = hudDragStartRef.current; + const pointer = hudDragPendingPointerRef.current; + if (!latestDragState || !pointer) { return; } - window.electronAPI?.hudOverlayDrag?.("move", position.x, position.y); + const deltaX = pointer.clientX - latestDragState.startX; + const deltaY = pointer.clientY - latestDragState.startY; + const viewportWidth = Math.max(window.innerWidth, window.screen?.width ?? 0); + const viewportHeight = Math.max(window.innerHeight, window.screen?.height ?? 0); + const unclampedLeft = latestDragState.initialLeft + deltaX; + const unclampedTop = latestDragState.initialTop + deltaY; + const clampedLeft = Math.min( + Math.max(0, unclampedLeft), + Math.max(0, viewportWidth - latestDragState.hudWidth), + ); + const clampedTop = Math.min( + Math.max(0, unclampedTop), + Math.max(0, viewportHeight - latestDragState.hudHeight), + ); + + const nextOffset = { + x: latestDragState.originX + (clampedLeft - latestDragState.initialLeft), + y: latestDragState.originY + (clampedTop - latestDragState.initialTop), + }; + recordingHudOffsetRef.current = nextOffset; + if (hudBarTransformRef.current) { + hudBarTransformRef.current.style.transform = `translate3d(${nextOffset.x}px, ${nextOffset.y}px, 0)`; + } }); }; @@ -396,22 +372,17 @@ export function LaunchWindow() { return; } - if (dragState.mode === "overlay") { - if (hudDragMoveRafRef.current !== null) { - cancelAnimationFrame(hudDragMoveRafRef.current); - hudDragMoveRafRef.current = null; - } - const position = hudDragPendingScreenPositionRef.current; - if (position) { - window.electronAPI?.hudOverlayDrag?.("move", position.x, position.y); - } - hudDragPendingScreenPositionRef.current = null; - window.electronAPI?.hudOverlayDrag?.("end", 0, 0); + if (hudDragMoveRafRef.current !== null) { + cancelAnimationFrame(hudDragMoveRafRef.current); + hudDragMoveRafRef.current = null; } + hudDragPendingPointerRef.current = null; hudDragStartRef.current = null; const wasDragging = isHudDraggingRef.current; isHudDraggingRef.current = false; + setIsHudDragging(false); + setRecordingHudOffset({ ...recordingHudOffsetRef.current }); if (event.currentTarget.hasPointerCapture(event.pointerId)) { event.currentTarget.releasePointerCapture(event.pointerId); } @@ -460,7 +431,8 @@ export function LaunchWindow() { cancelAnimationFrame(hudDragMoveRafRef.current); } hudDragMoveRafRef.current = null; - hudDragPendingScreenPositionRef.current = null; + hudDragPendingPointerRef.current = null; + hudDragStartRef.current = null; }; }, []); @@ -692,86 +664,6 @@ export function LaunchWindow() { }; }, []); - useEffect(() => { - const expanded = - anyPopoverOpen || projectBrowserOpen || showRecordingWebcamPreview; - window.electronAPI.setHudOverlayExpanded(expanded); - - return () => { - window.electronAPI.setHudOverlayExpanded(false); - }; - }, [anyPopoverOpen, projectBrowserOpen, showRecordingWebcamPreview]); - - const reportHudSize = useCallback(() => { - const hudContent = hudContentRef.current; - const hudBar = hudBarRef.current; - if (!hudContent || !hudBar) { - return; - } - - if (showRecordingWebcamPreview) { - const viewportWidth = Math.max(window.innerWidth, window.screen?.width ?? 0); - const viewportHeight = Math.max(window.innerHeight, window.screen?.height ?? 0); - window.electronAPI.setHudOverlayCompactWidth(Math.ceil(viewportWidth)); - window.electronAPI.setHudOverlayMeasuredHeight(Math.ceil(viewportHeight), true); - return; - } - - const hudContentRect = hudContent.getBoundingClientRect(); - const hudBarRect = hudBar.getBoundingClientRect(); - const standardWidth = Math.max( - hudBarRect.width, - hudBar.scrollWidth, - hudContentRect.width, - hudContent.scrollWidth, - ); - const standardHeight = Math.max(hudContentRect.height, hudContent.scrollHeight); - - window.electronAPI.setHudOverlayCompactWidth(Math.ceil(standardWidth + 24)); - window.electronAPI.setHudOverlayMeasuredHeight( - Math.ceil(standardHeight + 24), - anyPopoverOpen || projectBrowserOpen, - ); - }, [anyPopoverOpen, projectBrowserOpen, showRecordingWebcamPreview]); - - useEffect(() => { - const hudContent = hudContentRef.current; - const hudBar = hudBarRef.current; - const previewContainer = recordingWebcamPreviewContainerRef.current; - if (!hudContent || !hudBar || typeof ResizeObserver === "undefined") { - return; - } - - let frameId = 0; - const scheduleHudSizeReport = () => { - if (frameId !== 0) { - cancelAnimationFrame(frameId); - } - frameId = requestAnimationFrame(() => { - frameId = 0; - reportHudSize(); - }); - }; - - scheduleHudSizeReport(); - - const resizeObserver = new ResizeObserver(() => { - scheduleHudSizeReport(); - }); - resizeObserver.observe(hudContent); - resizeObserver.observe(hudBar); - if (previewContainer) { - resizeObserver.observe(previewContainer); - } - - return () => { - resizeObserver.disconnect(); - if (frameId !== 0) { - cancelAnimationFrame(frameId); - } - }; - }, [reportHudSize]); - const fetchSources = useCallback(async () => { if (!window.electronAPI) return; setSourcesLoading(true); @@ -1174,16 +1066,12 @@ export function LaunchWindow() { return (
window.electronAPI?.hudOverlaySetIgnoreMouse?.(false)} onMouseLeave={() => { if ( @@ -1199,15 +1087,16 @@ export function LaunchWindow() { >
( - + ), [onSourceSelect, t], ); @@ -196,7 +197,7 @@ export const SourceSelectorContent = ({ return (
-
+
{t("recording.screens")}
@@ -218,7 +219,7 @@ export const SourceSelectorContent = ({ return (
-
+
{t("recording.windows")}
@@ -237,7 +238,7 @@ export const SourceSelectorContent = ({ if (loading) { return (
-
+
); } @@ -250,7 +251,7 @@ export const SourceSelectorContent = ({ {windowSection} ) : ( -
+
{t("recording.noSourcesFound")}
)} @@ -312,7 +313,8 @@ export const SourceSelector = React.memo(function SourceSelector({ )} & { animated?: boolean; usePortal?: boolean; + unstyled?: boolean; }) { const content = (