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

* 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>

* fix(embedded-mpv): address native-view dock review feedback

Resolves the actionable P2 review comments on the dock rework:

- Inline volume no longer clips the dock actions. At sidebar-constrained
  player widths (~480-660px, viewport wider than the 720px breakpoint) the
  new in-flow volume slider widened the non-shrinking actions column and,
  under overflow:hidden, clipped the fullscreen button. Add min-width:0 to
  .embedded-mpv-player__actions and __volume-group so the inline volume (a
  scroll container) compresses its own slider instead of pushing neighbors
  off-edge. Verified in Chromium: fullscreen stays visible down to 480px.
- Space now selects a focused chip. onPanelKeydown stops Space/Enter from
  bubbling to the global shortcut handler (whose Space case preventDefault'd
  the button's native activation and toggled playback) without calling
  preventDefault itself, so the menuitemradio chip activates and emits
  chipSelected. Matches the WAI-ARIA menu activation-key expectation.
- Simplify dock-panel opener tracking: always remember the toggled kind so
  focus restoration is correct if in-panel switching ever becomes reachable
  (currently unreachable — the toggle buttons are removed from the DOM while
  a panel is open); restoreOpenerFocus still no-ops unless focus fell to body.

Not changed: the "closePanels no-ops when unavailable" comment — verified
unreachable (chip selection closes via menus.close() directly, not through
closePopovers; isAvailable() is engine-bound and the native dock only renders
while it is true, with engine handoff calling menus.closeAll()).

Regression test added for Space/Enter chip activation. The volume-overflow
fix is CSS layout (no jsdom layout engine) and was validated in a real
browser.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Fable 5 authored and GitHub committed 2026-07-19 18:39:59 +02:00
1 parent c707af1334
commit aa1941cf2c
35 files changed
+1666 -622

No files matched your search

+40 -10
View File
@@ -246,7 +246,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,
@@ -533,7 +533,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
@@ -554,7 +554,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
@@ -562,9 +564,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.
@@ -578,21 +592,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