diff --git a/electron/hudOverlayBounds.test.ts b/electron/hudOverlayBounds.test.ts index db21e92bd..7840ee2e8 100644 --- a/electron/hudOverlayBounds.test.ts +++ b/electron/hudOverlayBounds.test.ts @@ -21,18 +21,26 @@ describe("getHudOverlayWindowBounds", () => { it("uses a bottom-centered compact fallback when mouse passthrough is unavailable", () => { expect(getHudOverlayWindowBounds(workArea, false)).toEqual({ x: 650, - y: 920, + y: 520, width: 860, - height: 160, + height: 560, }); }); + it("reserves enough compact height for a full-height HUD menu", () => { + // The menu card is capped at 400px and sits above roughly 96px of bar and + // padding plus 16px of offsets. If the compact window is shorter than that + // the menu is clipped by the window bounds, which is the bug this guards. + const compact = getHudOverlayWindowBounds(workArea, false); + expect(compact.height).toBeGreaterThanOrEqual(400 + 16 + 96); + }); + it("expands the non-passthrough fallback for HUD menus and hover interaction", () => { expect(getHudOverlayWindowBounds(workArea, false, true)).toEqual({ x: 650, - y: 540, + y: 400, width: 860, - height: 540, + height: 680, }); }); @@ -49,9 +57,9 @@ describe("getHudOverlayWindowBounds", () => { ), ).toEqual({ x: -100, - y: 280, + y: 20, width: 640, - height: 160, + height: 420, }); }); @@ -98,9 +106,9 @@ describe("resizeHudOverlayFallbackBounds", () => { ), ).toEqual({ x: 420, - y: 320, + y: 180, width: 860, - height: 540, + height: 680, }); }); @@ -110,17 +118,17 @@ describe("resizeHudOverlayFallbackBounds", () => { workArea, { x: 420, - y: 320, + y: 180, width: 860, - height: 540, + height: 680, }, false, ), ).toEqual({ x: 420, - y: 700, + y: 300, width: 860, - height: 160, + height: 560, }); }); @@ -138,9 +146,9 @@ describe("resizeHudOverlayFallbackBounds", () => { ), ).toEqual({ x: 1060, - y: 520, + y: 380, width: 860, - height: 540, + height: 680, }); }); }); diff --git a/electron/hudOverlayBounds.ts b/electron/hudOverlayBounds.ts index 8c51b88c7..defb6b3e8 100644 --- a/electron/hudOverlayBounds.ts +++ b/electron/hudOverlayBounds.ts @@ -6,8 +6,18 @@ export interface HudOverlayWorkArea { } const NON_PASSTHROUGH_HUD_WIDTH_DIP = 860; -const NON_PASSTHROUGH_HUD_COMPACT_HEIGHT_DIP = 160; -const NON_PASSTHROUGH_HUD_EXPANDED_HEIGHT_DIP = 540; +// Without mouse passthrough the HUD is an ordinary window, so anything the +// renderer draws outside it is clipped: a menu taller than the window loses its +// top rows. Resizing on demand is not an option either, because Wayland +// compositors ignore client-side moves, so a window that grows keeps its +// top-left pinned and drags the bar down with it. +// +// So reserve the menu headroom once, at creation. The bar renders at the bottom +// edge of the window, which leaves the space above it transparent and free for +// menus to open into without the window ever changing size. +// 400px menu card + 16px offsets + ~96px of bar and padding, rounded up. +const NON_PASSTHROUGH_HUD_COMPACT_HEIGHT_DIP = 560; +const NON_PASSTHROUGH_HUD_EXPANDED_HEIGHT_DIP = 680; function clamp(value: number, min: number, max: number): number { return Math.min(Math.max(value, min), max); diff --git a/electron/windows.ts b/electron/windows.ts index 23e874fa1..1c7a67549 100644 --- a/electron/windows.ts +++ b/electron/windows.ts @@ -5,11 +5,7 @@ import { fileURLToPath } from "node:url"; import { app, BrowserWindow, ipcMain } from "electron"; import { supportsHudCaptureProtection } from "../src/lib/hudCaptureProtection"; import { USER_DATA_PATH } from "./appPaths"; -import { - getHudOverlayWindowBounds, - resizeHudOverlayFallbackBounds, - shouldExpandHudOverlayFallback, -} from "./hudOverlayBounds"; +import { getHudOverlayWindowBounds, shouldExpandHudOverlayFallback } from "./hudOverlayBounds"; import { getHudOverlayTaskbarOptions } from "./hudOverlayWindowOptions"; import { getPackagedRendererBaseUrl } from "./rendererServer"; @@ -265,34 +261,6 @@ function positionUpdateToastWindow() { updateToastWindow.moveTop(); } -function setHudOverlayFallbackExpanded(expanded: boolean) { - if (hudOverlayRecordingActive) { - hudOverlayFallbackExpanded = false; - return; - } - - hudOverlayFallbackExpanded = expanded; - if ( - !hudOverlayWindow || - hudOverlayWindow.isDestroyed() || - isHudOverlayMousePassthroughSupported() - ) { - return; - } - - const { workArea } = getHudOverlayDisplay(); - const nextBounds = resizeHudOverlayFallbackBounds( - workArea, - hudOverlayWindow.getBounds(), - expanded, - ); - hudOverlayWindow.setBounds(nextBounds, false); - positionUpdateToastWindow(); - if (hudOverlayWindow.isVisible()) { - hudOverlayWindow.moveTop(); - } -} - function setHudOverlayMousePassthrough(ignore: boolean) { hudOverlayIgnoringMouse = hudOverlaySourceSelectionActive && !hudOverlayRecordingActive ? true : ignore; @@ -312,9 +280,10 @@ function setHudOverlayMousePassthrough(ignore: boolean) { } if (!isHudOverlayMousePassthroughSupported()) { - if (process.platform !== "linux") { - setHudOverlayFallbackExpanded(!ignore); - } + // Deliberately no resize here. This branch is Linux-only, and growing the + // window on hover moves the bar out from under the pointer, which fires + // mouseleave and shrinks it back, oscillating. The fallback window instead + // reserves menu headroom up front, so menus need no resize at all. hudOverlayWindow.setIgnoreMouseEvents(false); return; } diff --git a/src/components/launch/popovers/MorePopover.tsx b/src/components/launch/popovers/MorePopover.tsx index 9a5a52905..f3e71300c 100644 --- a/src/components/launch/popovers/MorePopover.tsx +++ b/src/components/launch/popovers/MorePopover.tsx @@ -14,25 +14,13 @@ import { useI18n } from "@/contexts/I18nContext"; import { useScopedT } from "@/contexts/I18nContext"; import { useTheme } from "@/contexts/ThemeContext"; import type { AppLocale } from "@/i18n/config"; -import { SUPPORTED_LOCALES } from "@/i18n/config"; +import { LOCALE_LABELS, SUPPORTED_LOCALES } from "@/i18n/config"; import styles from "../LaunchWindow.module.css"; import { useLaunchPopoverCoordinator } from "./LaunchPopoverCoordinator"; import { DropdownItem, HudPopover } from "./PopoverScaffold"; const POPOVER_ID = "more"; -const LOCALE_LABELS: Record = { - en: "English", - es: "Español", - fr: "Français", - it: "Italiano", - nl: "Nederlands", - ko: "한국어", - "pt-BR": "Português", - "zh-CN": "簡體中文", - "zh-TW": "繁體中文", -}; - export function MorePopover({ trigger, supportsHudCaptureProtection, @@ -170,7 +158,7 @@ export function MorePopover({ requestClose(POPOVER_ID); }} > - {LOCALE_LABELS[code] ?? code} + {LOCALE_LABELS[code]} ))} {appVersion && ( diff --git a/src/components/video-editor/SettingsPanel.tsx b/src/components/video-editor/SettingsPanel.tsx index 586e09d2a..e1f3aea58 100644 --- a/src/components/video-editor/SettingsPanel.tsx +++ b/src/components/video-editor/SettingsPanel.tsx @@ -32,7 +32,7 @@ import { import { type AspectRatio } from "@/utils/aspectRatioUtils"; import { useI18n, useScopedT } from "../../contexts/I18nContext"; import type { AppLocale } from "../../i18n/config"; -import { SUPPORTED_LOCALES } from "../../i18n/config"; +import { LOCALE_LABELS, SUPPORTED_LOCALES } from "../../i18n/config"; import { AnnotationSettingsPanel } from "./AnnotationSettingsPanel"; import CaptionListPanel from "./CaptionListPanel"; import type { CaptionRetimeSpan } from "./captionOps"; @@ -707,19 +707,6 @@ const CAPTION_LANGUAGE_OPTIONS = [ { value: "ko", label: "Korean" }, ] as const; -const APP_LANGUAGE_LABELS: Record = { - en: "English", - es: "Español", - fr: "Français", - de: "Deutsch", - it: "Italiano", - nl: "Nederlands", - ko: "한국어", - "pt-BR": "Português", - "zh-CN": "簡體中文", - "zh-TW": "繁體中文", -}; - function loadPreviewImage(url: string) { return new Promise((resolve, reject) => { const image = new Image(); @@ -2575,7 +2562,7 @@ export function SettingsPanel({ {SUPPORTED_LOCALES.map((candidateLocale) => ( - {APP_LANGUAGE_LABELS[candidateLocale]} + {LOCALE_LABELS[candidateLocale]} ))} diff --git a/src/i18n/config.ts b/src/i18n/config.ts index 567d72efe..6c7258afb 100644 --- a/src/i18n/config.ts +++ b/src/i18n/config.ts @@ -25,3 +25,21 @@ export const I18N_NAMESPACES = [ export type AppLocale = (typeof SUPPORTED_LOCALES)[number]; export type I18nNamespace = (typeof I18N_NAMESPACES)[number]; + +/** + * Native display name per locale. Typed against AppLocale so adding a locale to + * SUPPORTED_LOCALES without a label is a compile error rather than a menu row + * that renders the raw locale code. + */ +export const LOCALE_LABELS: Record = { + en: "English", + es: "Español", + fr: "Français", + de: "Deutsch", + it: "Italiano", + nl: "Nederlands", + ko: "한국어", + "pt-BR": "Português", + "zh-CN": "簡體中文", + "zh-TW": "繁體中文", +};