From 4e43c5c6e19efe569912d108d3f51317e68c766d Mon Sep 17 00:00:00 2001 From: sudacode Date: Tue, 29 Sep 2026 18:39:55 -0700 Subject: [PATCH] fix(overlay): keep overlay mapped during Hyprland/Sway fullscreen (#276) --- changes/hyprland-fullscreen-refresh.md | 4 ++ docs-site/troubleshooting.md | 2 + ...nux-mpv-fullscreen-overlay-refresh.test.ts | 63 ++++++++++++++++++- .../linux-mpv-fullscreen-overlay-refresh.ts | 8 ++- 4 files changed, 74 insertions(+), 3 deletions(-) create mode 100644 changes/hyprland-fullscreen-refresh.md diff --git a/changes/hyprland-fullscreen-refresh.md b/changes/hyprland-fullscreen-refresh.md new file mode 100644 index 00000000..d6d7e97c --- /dev/null +++ b/changes/hyprland-fullscreen-refresh.md @@ -0,0 +1,4 @@ +type: fixed +area: overlay + +- Keep the subtitle overlay mapped during fullscreen transitions on Hyprland, avoiding focus changes from repeated hide/show calls that can cancel mpv fullscreen entry. Preserve the hide/show refresh on Sway. diff --git a/docs-site/troubleshooting.md b/docs-site/troubleshooting.md index 34b6f857..aefd9c1a 100644 --- a/docs-site/troubleshooting.md +++ b/docs-site/troubleshooting.md @@ -225,6 +225,8 @@ bind = ALT SHIFT, Y, pass, class:^(SubMiner)$ If the overlay stays behind fullscreen mpv, check that the mpv socket is connected and that `hyprctl -j clients` works from the environment that launched SubMiner. +SubMiner restacks the existing overlay during fullscreen transitions on Hyprland. It keeps the overlay mapped so repeated hide/show calls do not steal focus and cancel mpv's fullscreen entry. Sway retains the hide/show refresh because SubMiner has no Sway-specific restacking implementation. + See the Hyprland wiki on [global keybinds](https://wiki.hypr.land/Configuring/Binds/#global-keybinds) and [window rules](https://wiki.hypr.land/Configuring/Window-Rules/). ### KDE Plasma and other Wayland compositors diff --git a/src/main/runtime/linux-mpv-fullscreen-overlay-refresh.test.ts b/src/main/runtime/linux-mpv-fullscreen-overlay-refresh.test.ts index 7c01eacc..71f29e9f 100644 --- a/src/main/runtime/linux-mpv-fullscreen-overlay-refresh.test.ts +++ b/src/main/runtime/linux-mpv-fullscreen-overlay-refresh.test.ts @@ -1,11 +1,72 @@ import assert from 'node:assert/strict'; -import test from 'node:test'; +import test, { beforeEach, afterEach } from 'node:test'; import { clearLinuxMpvFullscreenOverlayRefreshTimeouts, updateLinuxMpvFullscreenOverlayRefreshBurst, scheduleLinuxVisibleOverlayFullscreenRefreshBurst, } from './linux-mpv-fullscreen-overlay-refresh'; +const compositorEnvKeys = [ + 'HYPRLAND_INSTANCE_SIGNATURE', + 'SWAYSOCK', + 'XDG_CURRENT_DESKTOP', + 'XDG_SESSION_DESKTOP', +] as const; +const originalCompositorEnv = compositorEnvKeys.map((key) => [key, process.env[key]] as const); +beforeEach(() => { + for (const key of compositorEnvKeys) delete process.env[key]; +}); +afterEach(() => { + for (const [key, value] of originalCompositorEnv) { + if (value === undefined) delete process.env[key]; + else process.env[key] = value; + } +}); + +for (const { compositorKey, expectedRefreshCalls } of [ + { + compositorKey: 'HYPRLAND_INSTANCE_SIGNATURE', + expectedRefreshCalls: ['mode', 'visibility', 'mouse', 'restack'], + }, + { + compositorKey: 'SWAYSOCK', + expectedRefreshCalls: ['mode', 'visibility', 'hide', 'showInactive', 'mouse', 'restack'], + }, +]) { + test(`${compositorKey} fullscreen refresh uses compositor-specific restacking`, async () => { + const originalPlatformDescriptor = Object.getOwnPropertyDescriptor(process, 'platform'); + Object.defineProperty(process, 'platform', { configurable: true, value: 'linux' }); + process.env[compositorKey] = 'fullscreen-refresh-test'; + const calls: string[] = []; + try { + scheduleLinuxVisibleOverlayFullscreenRefreshBurst(true, { + overlayManager: { + getMainWindow: () => ({ + hide: () => calls.push('hide'), + showInactive: () => calls.push('showInactive'), + isDestroyed: () => false, + isVisible: () => true, + setIgnoreMouseEvents: () => calls.push('mouse'), + }), + getVisibleOverlayVisible: () => true, + }, + overlayVisibilityRuntime: { + updateVisibleOverlayVisibility: () => calls.push('visibility'), + }, + syncVisibleOverlayMpvFullscreenMode: () => calls.push('mode'), + ensureOverlayWindowLevel: () => calls.push('restack'), + }); + await new Promise((resolve) => setTimeout(resolve, 700)); + assert.deepEqual(calls, Array.from({ length: 5 }, () => expectedRefreshCalls).flat()); + } finally { + clearLinuxMpvFullscreenOverlayRefreshTimeouts(); + if (originalPlatformDescriptor) { + Object.defineProperty(process, 'platform', originalPlatformDescriptor); + } + } + }); +} + test('linux mpv fullscreen overlay refresh burst schedules overlay refresh work on linux', async () => { const originalPlatformDescriptor = Object.getOwnPropertyDescriptor(process, 'platform'); Object.defineProperty(process, 'platform', { diff --git a/src/main/runtime/linux-mpv-fullscreen-overlay-refresh.ts b/src/main/runtime/linux-mpv-fullscreen-overlay-refresh.ts index 213e6883..9340cf4a 100644 --- a/src/main/runtime/linux-mpv-fullscreen-overlay-refresh.ts +++ b/src/main/runtime/linux-mpv-fullscreen-overlay-refresh.ts @@ -52,8 +52,12 @@ function refreshLinuxVisibleOverlayAfterMpvFullscreenChange( return; } - mainWindow.hide(); - mainWindow.showInactive(); + // Hyprland placement can restack in place. Remapping the overlay can + // take focus from mpv and make Hyprland cancel the fullscreen transition. + if (!process.env.HYPRLAND_INSTANCE_SIGNATURE) { + mainWindow.hide(); + mainWindow.showInactive(); + } if (deps.getOverlayInteractionActive?.() === true) { mainWindow.setIgnoreMouseEvents(false); } else {