From 126025aa416c73f47330815ee67cfe48bfa2811a Mon Sep 17 00:00:00 2001 From: 4gray Date: Tue, 5 May 2026 10:16:33 +0200 Subject: [PATCH] fix(embedded-mpv): expose popover area by shrinking MPV view, not just hiding Same root cause as the modal-dialog occlusion: control popovers (volume, audio, subtitle, speed, aspect) extend upward from the controls strip into the area covered by the MPV NSView. They render in DOM but the native view paints over them. Replace the simple boolean overlayActiveProvider with a richer boundsProvider closure on the session controller. The component drives it from both the modal overlay state and the popover menu state: - Modal dialog open (command palette, MatDialog) -> HIDDEN_BOUNDS, MPV fully off-screen so the dialog has the whole window. - Popover menu open -> shrink MPV from the bottom by 300 px so the popover region lives in DOM-receiving space; video keeps playing in the upper region instead of disappearing entirely. - Otherwise -> full host bounds. Bounds-resync effect now tracks menus.anyOpen() in addition to overlayActive() so opening or closing a popover triggers an immediate re-sync. Co-Authored-By: Claude Opus 4.7 (1M context) Entire-Checkpoint: f957cd9849e0 --- .../embedded-mpv-format.utils.ts | 10 ++++++++ .../embedded-mpv-player.component.ts | 23 ++++++++++++++++--- .../embedded-mpv-session-controller.ts | 16 +++++++------ 3 files changed, 39 insertions(+), 10 deletions(-) diff --git a/libs/ui/playback/src/lib/embedded-mpv-player/embedded-mpv-format.utils.ts b/libs/ui/playback/src/lib/embedded-mpv-player/embedded-mpv-format.utils.ts index 0cfe5d023..d583bdcf3 100644 --- a/libs/ui/playback/src/lib/embedded-mpv-player/embedded-mpv-format.utils.ts +++ b/libs/ui/playback/src/lib/embedded-mpv-player/embedded-mpv-format.utils.ts @@ -7,6 +7,16 @@ export const HIDDEN_BOUNDS: EmbeddedMpvBounds = Object.freeze({ height: 1, }) as EmbeddedMpvBounds; +/** + * Vertical pixels to subtract from the MPV view's height when a control + * popover (volume, audio, subtitle, speed, aspect) is open above the + * controls strip. The native NSView paints over the WebContents, so we + * shrink it from the bottom to expose the popover region in DOM. Sized to + * cover the tallest popover (audio/subtitle list capped at ~240 px plus + * title + padding); video keeps playing in the upper region. + */ +export const MENU_OPEN_BOTTOM_CUTOUT_PX = 300; + export const SPEED_PRESETS: ReadonlyArray<{ value: number; label: string }> = [ { value: 0.5, label: '0.5×' }, { value: 0.75, label: '0.75×' }, diff --git a/libs/ui/playback/src/lib/embedded-mpv-player/embedded-mpv-player.component.ts b/libs/ui/playback/src/lib/embedded-mpv-player/embedded-mpv-player.component.ts index 41db2bd0b..b04b2f501 100644 --- a/libs/ui/playback/src/lib/embedded-mpv-player/embedded-mpv-player.component.ts +++ b/libs/ui/playback/src/lib/embedded-mpv-player/embedded-mpv-player.component.ts @@ -29,10 +29,13 @@ import { } from './embedded-mpv-ui-state'; import { ASPECT_PRESETS, + HIDDEN_BOUNDS, + MENU_OPEN_BOTTOM_CUTOUT_PX, SPEED_PRESETS, aspectLabel, audioTrackLabel, formatTime, + measureBounds, persistVolume, readStoredVolume, speedLabel, @@ -226,9 +229,22 @@ export class EmbeddedMpvPlayerComponent implements OnDestroy { ); } - this.controller.setOverlayActiveProvider(() => - this.overlayVisibility.overlayActive() - ); + this.controller.setBoundsProvider((host) => { + if (this.overlayVisibility.overlayActive()) { + return HIDDEN_BOUNDS; + } + const rect = measureBounds(host); + if (this.menus.anyOpen()) { + return { + ...rect, + height: Math.max( + 1, + rect.height - MENU_OPEN_BOTTOM_CUTOUT_PX + ), + }; + } + return rect; + }); this.shortcuts.attach({ isAvailable: () => !this.overlayVisibility.overlayActive(), @@ -265,6 +281,7 @@ export class EmbeddedMpvPlayerComponent implements OnDestroy { effect(() => { this.overlayVisibility.overlayActive(); + this.menus.anyOpen(); this.controller.triggerBoundsSync(); }); diff --git a/libs/ui/playback/src/lib/embedded-mpv-player/embedded-mpv-session-controller.ts b/libs/ui/playback/src/lib/embedded-mpv-player/embedded-mpv-session-controller.ts index 8362a0700..0804bb200 100644 --- a/libs/ui/playback/src/lib/embedded-mpv-player/embedded-mpv-session-controller.ts +++ b/libs/ui/playback/src/lib/embedded-mpv-player/embedded-mpv-session-controller.ts @@ -7,11 +7,14 @@ import { untracked, } from '@angular/core'; import { + EmbeddedMpvBounds, EmbeddedMpvSession, EmbeddedMpvSupport, ResolvedPortalPlayback, } from 'shared-interfaces'; -import { HIDDEN_BOUNDS, measureBounds } from './embedded-mpv-format.utils'; +import { measureBounds } from './embedded-mpv-format.utils'; + +export type EmbeddedMpvBoundsProvider = (host: HTMLElement) => EmbeddedMpvBounds; const STALLED_TIMEOUT_MS = 30_000; @@ -26,7 +29,8 @@ export class EmbeddedMpvSessionController { private readonly destroyRef = inject(DestroyRef); private readonly unsubscribeSessionUpdate?: () => void; - private overlayActiveProvider: () => boolean = () => false; + private boundsProvider: EmbeddedMpvBoundsProvider = (host) => + measureBounds(host); private activeBoundsSync: (() => void) | null = null; private boundsAnimationFrame: number | null = null; private stalledTimer: number | null = null; @@ -65,8 +69,8 @@ export class EmbeddedMpvSessionController { }); } - setOverlayActiveProvider(provider: () => boolean): void { - this.overlayActiveProvider = provider; + setBoundsProvider(provider: EmbeddedMpvBoundsProvider): void { + this.boundsProvider = provider; } triggerBoundsSync(): void { @@ -98,9 +102,7 @@ export class EmbeddedMpvSessionController { if (!activeSessionId) { return; } - const bounds = this.overlayActiveProvider() - ? HIDDEN_BOUNDS - : measureBounds(host); + const bounds = this.boundsProvider(host); void window.electron ?.setEmbeddedMpvBounds(activeSessionId, bounds) .catch(() => undefined);