fix(embedded-mpv): stop native-view video jump by moving menus into the dock

Opening any control popover in the native-view embedded MPV dock used to
shrink the MPV view by a 300 px bottom cutout so the popover DOM stayed
clickable, which made mpv re-letterbox the video on every menu open/close.

All five menus now render horizontally inside the fixed-height controls
strip, so menu state never changes the native view bounds:

- volume expands as an inline horizontal slider next to the mute button
- audio/subtitle/speed/aspect morph the dock row into a back button, a
  panel title, and a scrollable chip ribbon (app-embedded-mpv-dock-panel)
  with wheel-to-horizontal-scroll mapping, edge fades, active-chip
  reveal/focus, roving arrow-key navigation, ellipsis + tooltips, and
  RTL-aware scrolling
- boundsProvider loses the menus.anyOpen() cutout branch and the
  MENU_OPEN_BOTTOM_CUTOUT_PX constant is removed; HIDDEN_BOUNDS for modal
  overlays is unchanged
- global arrow shortcuts (seek/volume) are suspended while a chip panel
  is open so arrows walk the chips; Esc, click-outside, and close-on-select
  semantics are preserved
- new EMBEDDED_MPV.PLAYER.BACK i18n key in all 18 languages

Regression coverage: the new dock-panels spec asserts the bounds provider
returns full host bounds while every menu is open (fails against the old
cutout behavior), plus panel morph/a11y/selection specs and a dedicated
dock-panel component spec (keyboard, wheel, tabindex, emits).

Frame-copy shared controls (app-player-controls) are untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Fable 5 committed 2026-07-19 07:55:55 +02:00
1 parent 2f8aee72df
commit 4bc6f7e919
35 files changed
+1627 -622

No files matched your search

+40 -10
View File
@@ -245,7 +245,7 @@ service and the adapter):
`attachEmbeddedMpvFrameView`/`detachEmbeddedMpvFrameView`.
- Renderer: `EmbeddedMpvPlayerComponent` renders the canvas when
`support.engine === 'frame-copy'` and skips the compositor workarounds —
no `HIDDEN_BOUNDS` when dialogs open, no popover bottom cutout; dialogs
no `HIDDEN_BOUNDS` when dialogs open; dialogs
and the shared `app-player-controls` overlay stack above the canvas as
ordinary DOM. The canvas fills the player root; the native dock's reserved
controls height is not applied. Legacy embedded-MPV pointer/click,
@@ -532,7 +532,7 @@ player component stays a view-oriented orchestrator and engine-specific
controls host. The renderer files live under
`libs/ui/playback/src/lib/embedded-mpv-player/`:
- `embedded-mpv-format.utils.ts` — pure helpers (`formatTime`, `audioTrackLabel`, `subtitleTrackLabel`, `speedLabel`, `aspectLabel`, `volumeIcon`, `volumeLabel`, `readStoredVolume`, `persistVolume`, `measureBounds`) and preset constants (`SPEED_PRESETS`, `ASPECT_PRESETS`, `HIDDEN_BOUNDS`, `MENU_OPEN_BOTTOM_CUTOUT_PX`).
- `embedded-mpv-format.utils.ts` — pure helpers (`formatTime`, `audioTrackLabel`, `subtitleTrackLabel`, `speedLabel`, `aspectLabel`, `volumeIcon`, `volumeLabel`, `readStoredVolume`, `persistVolume`, `measureBounds`) and preset constants (`SPEED_PRESETS`, `ASPECT_PRESETS`, `HIDDEN_BOUNDS`).
- `embedded-mpv-controls.adapter.ts` — component-scoped `PlayerController`
adapter for frame-copy. Maps session/support/playback signals to shared
controls state and capabilities, delegates commands to
@@ -553,7 +553,9 @@ controls host. The renderer files live under
- `embedded-mpv-shortcuts.ts` — native-view-only `EmbeddedMpvShortcuts` class
with `attach(handlers)` / `detach()`. Owns the legacy document keydown
listener and routes through a callback interface; the component supplies
callbacks for Space/K, F, arrow keys, M, and Escape.
callbacks for Space/K, F, arrow keys, M, and Escape. The optional
`arrowKeysBlocked` handler suspends the seek/volume arrows while a dock
chip panel owns them for chip navigation.
- `embedded-mpv-overlay-visibility.service.ts` — singleton service that exposes
`overlayActive: signal<boolean>`. Tracks `MatDialog.afterOpened`/
`afterAllClosed` for dialog-shaped overlays and falls back to a
@@ -561,9 +563,21 @@ controls host. The renderer files live under
backdrop-bearing CDK overlays. Native-view uses it to move the platform host
off-screen; frame-copy uses it to gate shared playback shortcuts.
- `embedded-mpv-ui-state.ts` — legacy native-view
`EmbeddedMpvMenuState` (single-open popover state machine) and
`EmbeddedMpvMenuState` (single-open menu state machine, incl. the
`dockPanelOpen` chip-panel signal that suspends arrow shortcuts) and
`EmbeddedMpvFeedback` (transient keypress feedback). They are not the
frame-copy shared-controls state.
- `embedded-mpv-dock-panels.ts` — native-view `EmbeddedMpvDockPanelState`:
builds the active horizontal chip-panel view model (audio, subtitle, speed,
aspect) from the menu state, routes chip selection back to the session
controller, and restores toggle-button focus after a panel closes.
- `embedded-mpv-dock-panel.component.ts` — standalone
`app-embedded-mpv-dock-panel` that morphs the dock row inside the
fixed-height controls strip: back button + title + horizontally scrollable
chip ribbon (`role="menu"` with `aria-orientation="horizontal"`,
`menuitemradio` chips, wheel-to-horizontal-scroll mapping, edge fades,
active-chip reveal/focus, roving arrow keys, RTL-aware). Keeping the panels
inside the strip is what lets menus open without any MPV bounds change.
- `embedded-mpv-command-runner.ts` — transport/track/recording IPC delegation; contains addon-side throws; reconciles a returned snapshot only when the current canonical session id and returned snapshot id both match the captured command session id.
- `embedded-mpv-session-factory.ts` — side-effect-free loading/error placeholder factories plus `waitForStartupPaint`.
- `embedded-mpv-stalled-tracker.ts` — owns the 30-second loading timer and `stalled` signal.
@@ -577,21 +591,37 @@ controls host. The renderer files live under
### Bounds compositing strategy
The following cutout strategy applies only to the native-view engine. Its video
The following strategy applies only to the native-view engine. Its video
host paints outside the normal DOM stacking model, so any DOM region it covers
cannot reliably receive pointer events and any CSS `z-index` competition is
unwinnable. The component compensates with a single `boundsProvider(host)`
closure on the controller that returns one of three bound shapes, evaluated
closure on the controller that returns one of two bound shapes, evaluated
each time the active bounds-sync runs:
- **Modal overlay open** (any MatDialog, including the command palette) → `HIDDEN_BOUNDS`. The MPV video host moves off-screen so the dialog has the full window.
- **Control popover open** (any of the menu states above) → host bounds with `MENU_OPEN_BOTTOM_CUTOUT_PX` (300 px) removed from the bottom. The popover region becomes DOM-receiving while video keeps playing in the upper region.
- **Idle** → full host bounds.
- **Otherwise** → full host bounds.
The viewport DOM element also reserves `--embedded-mpv-controls-height` (64 px) at the bottom when controls are enabled, so the controls strip itself is always DOM and always reachable for hover-to-reveal even before the popover-cutout takes effect.
Control menus never influence bounds: all five (volume, audio, subtitle,
speed, aspect) render horizontally inside the fixed-height controls strip
below the video host. Volume expands as an inline horizontal slider next to
the mute button; the audio/subtitle/speed/aspect menus morph the dock row
into `app-embedded-mpv-dock-panel` — back button, panel title, and a
horizontally scrollable chip ribbon (vertical wheel mapped to horizontal
scroll, edge fades as continuation hints, auto-reveal and focus of the active
chip, roving arrow-key navigation, RTL-aware). Because the strip height never
changes, opening or closing a menu sends no new MPV bounds and the video never
re-letterboxes. The popover-era 300 px bottom cutout
(`MENU_OPEN_BOTTOM_CUTOUT_PX`) is gone; while a chip panel is open, the
global arrow-key shortcuts (seek/volume) are suspended so arrows walk the
chips instead.
The viewport DOM element reserves `--embedded-mpv-controls-height` (64 px;
88 px under the narrow breakpoint) at the bottom when controls are enabled, so
the controls strip — including the in-dock panels — is always DOM and always
reachable for hover-to-reveal.
For frame-copy, `boundsProvider` always returns the measured full host bounds:
there is no `HIDDEN_BOUNDS`, popover cutout, or reserved dock height. Dialogs
there is no `HIDDEN_BOUNDS` or reserved dock height. Dialogs
and controls layer naturally over the canvas, while bounds sync still updates
the helper's render size.
@@ -506,8 +506,9 @@ back to the command's stale baseline.
The native MPV surface paints outside Chromium's DOM stacking model. It keeps
the compositor-safe fixed controls dock below the viewport. Modal overlays hide
the native surface with `HIDDEN_BOUNDS`, and control popovers reserve a bottom
cutout so their DOM region remains interactive.
the native surface with `HIDDEN_BOUNDS`; control menus render as horizontal
panels inside the fixed-height dock strip, so they stay interactive without
any bounds change.
The transparent BrowserWindow / `NSWindowBelow` tunnel-and-backdrop approach is
not the shipped architecture. The shared-controls integration does not add