fix(overlay): Linux X11/XWayland stacking, stale pause state, multi-copy selector (#101)

This commit is contained in:
2026-05-31 20:59:18 -07:00
committed by GitHub
parent b46b8dfa41
commit e1ea464bc9
103 changed files with 6314 additions and 353 deletions
@@ -16,13 +16,19 @@ test('linux mpv fullscreen overlay refresh burst schedules overlay refresh work
const calls: string[] = [];
try {
scheduleLinuxVisibleOverlayFullscreenRefreshBurst({
scheduleLinuxVisibleOverlayFullscreenRefreshBurst(true, {
overlayManager: {
getMainWindow: () =>
({
hide: () => calls.push('hide'),
isFullScreen: () => false,
isDestroyed: () => false,
isVisible: () => true,
setFullScreen: (fullscreen: boolean) => calls.push(`fullscreen:${fullscreen}`),
setIgnoreMouseEvents: (ignore: boolean, options?: { forward?: boolean }) =>
calls.push(
`mouse-ignore:${ignore}:${options?.forward === true ? 'forward' : 'plain'}`,
),
showInactive: () => calls.push('showInactive'),
}) as never,
getVisibleOverlayVisible: () => true,
@@ -30,6 +36,8 @@ test('linux mpv fullscreen overlay refresh burst schedules overlay refresh work
overlayVisibilityRuntime: {
updateVisibleOverlayVisibility: () => calls.push('updateVisibleOverlayVisibility'),
},
syncVisibleOverlayMpvFullscreenMode: (fullscreen: boolean) =>
calls.push(`sync-overlay-mode:${fullscreen}`),
ensureOverlayWindowLevel: () => calls.push('ensureOverlayWindowLevel'),
});
@@ -39,8 +47,11 @@ test('linux mpv fullscreen overlay refresh burst schedules overlay refresh work
}
assert.ok(calls.includes('updateVisibleOverlayVisibility'));
assert.ok(calls.includes('sync-overlay-mode:true'));
assert.ok(!calls.includes('fullscreen:true'));
assert.ok(calls.includes('hide'));
assert.ok(calls.includes('showInactive'));
assert.ok(calls.includes('mouse-ignore:true:forward'));
assert.ok(calls.includes('ensureOverlayWindowLevel'));
} finally {
clearLinuxMpvFullscreenOverlayRefreshTimeouts();
@@ -50,7 +61,46 @@ test('linux mpv fullscreen overlay refresh burst schedules overlay refresh work
}
});
test('linux mpv fullscreen overlay refresh update schedules a fresh burst when fullscreen exits', async () => {
test('linux mpv fullscreen overlay refresh remembers mode even when overlay is hidden', async () => {
const originalPlatformDescriptor = Object.getOwnPropertyDescriptor(process, 'platform');
Object.defineProperty(process, 'platform', {
configurable: true,
value: 'linux',
});
const calls: string[] = [];
try {
scheduleLinuxVisibleOverlayFullscreenRefreshBurst(true, {
overlayManager: {
getMainWindow: () => null,
getVisibleOverlayVisible: () => false,
},
overlayVisibilityRuntime: {
updateVisibleOverlayVisibility: () => calls.push('updateVisibleOverlayVisibility'),
},
syncVisibleOverlayMpvFullscreenMode: (fullscreen: boolean) =>
calls.push(`sync-overlay-mode:${fullscreen}`),
ensureOverlayWindowLevel: () => calls.push('ensureOverlayWindowLevel'),
});
const deadline = Date.now() + 200;
while (!calls.includes('sync-overlay-mode:true') && Date.now() < deadline) {
await new Promise((resolve) => setTimeout(resolve, 5));
}
assert.ok(calls.includes('sync-overlay-mode:true'));
assert.ok(!calls.includes('updateVisibleOverlayVisibility'));
assert.ok(!calls.includes('ensureOverlayWindowLevel'));
} finally {
clearLinuxMpvFullscreenOverlayRefreshTimeouts();
if (originalPlatformDescriptor) {
Object.defineProperty(process, 'platform', originalPlatformDescriptor);
}
}
});
test('linux mpv fullscreen overlay refresh updates mode without hide/show when fullscreen exits', async () => {
const originalPlatformDescriptor = Object.getOwnPropertyDescriptor(process, 'platform');
Object.defineProperty(process, 'platform', {
configurable: true,
@@ -65,8 +115,14 @@ test('linux mpv fullscreen overlay refresh update schedules a fresh burst when f
getMainWindow: () =>
({
hide: () => calls.push('hide'),
isFullScreen: () => true,
isDestroyed: () => false,
isVisible: () => true,
setFullScreen: (fullscreen: boolean) => calls.push(`fullscreen:${fullscreen}`),
setIgnoreMouseEvents: (ignore: boolean, options?: { forward?: boolean }) =>
calls.push(
`mouse-ignore:${ignore}:${options?.forward === true ? 'forward' : 'plain'}`,
),
showInactive: () => calls.push('showInactive'),
}) as never,
getVisibleOverlayVisible: () => true,
@@ -74,6 +130,8 @@ test('linux mpv fullscreen overlay refresh update schedules a fresh burst when f
overlayVisibilityRuntime: {
updateVisibleOverlayVisibility: () => calls.push('updateVisibleOverlayVisibility'),
},
syncVisibleOverlayMpvFullscreenMode: (fullscreen: boolean) =>
calls.push(`sync-overlay-mode:${fullscreen}`),
ensureOverlayWindowLevel: () => calls.push('ensureOverlayWindowLevel'),
};
@@ -84,9 +142,125 @@ test('linux mpv fullscreen overlay refresh update schedules a fresh burst when f
assert.equal(typeof nextCancel, 'function');
assert.ok(calls.includes('updateVisibleOverlayVisibility'));
assert.ok(calls.includes('hide'));
assert.ok(calls.includes('showInactive'));
assert.ok(calls.includes('ensureOverlayWindowLevel'));
assert.ok(calls.includes('sync-overlay-mode:false'));
assert.ok(!calls.includes('fullscreen:false'));
assert.equal(calls.includes('hide'), false);
assert.equal(calls.includes('showInactive'), false);
assert.equal(calls.includes('mouse-ignore:true:forward'), false);
assert.equal(calls.includes('ensureOverlayWindowLevel'), false);
} finally {
clearLinuxMpvFullscreenOverlayRefreshTimeouts();
if (originalPlatformDescriptor) {
Object.defineProperty(process, 'platform', originalPlatformDescriptor);
}
}
});
test('linux mpv fullscreen overlay refresh restores click-through after restacking', async () => {
const originalPlatformDescriptor = Object.getOwnPropertyDescriptor(process, 'platform');
Object.defineProperty(process, 'platform', {
configurable: true,
value: 'linux',
});
const calls: string[] = [];
try {
scheduleLinuxVisibleOverlayFullscreenRefreshBurst(true, {
overlayManager: {
getMainWindow: () =>
({
hide: () => calls.push('hide'),
isFullScreen: () => false,
isDestroyed: () => false,
isVisible: () => true,
setFullScreen: (fullscreen: boolean) => calls.push(`fullscreen:${fullscreen}`),
setIgnoreMouseEvents: (ignore: boolean, options?: { forward?: boolean }) =>
calls.push(
`mouse-ignore:${ignore}:${options?.forward === true ? 'forward' : 'plain'}`,
),
showInactive: () => calls.push('showInactive'),
}) as never,
getVisibleOverlayVisible: () => true,
},
overlayVisibilityRuntime: {
updateVisibleOverlayVisibility: () => calls.push('updateVisibleOverlayVisibility'),
},
syncVisibleOverlayMpvFullscreenMode: (fullscreen: boolean) =>
calls.push(`sync-overlay-mode:${fullscreen}`),
ensureOverlayWindowLevel: () => calls.push('ensureOverlayWindowLevel'),
});
const deadline = Date.now() + 200;
while (!calls.includes('mouse-ignore:true:forward') && Date.now() < deadline) {
await new Promise((resolve) => setTimeout(resolve, 5));
}
const showIndex = calls.indexOf('showInactive');
const passthroughIndex = calls.indexOf('mouse-ignore:true:forward');
const levelIndex = calls.indexOf('ensureOverlayWindowLevel');
const syncIndex = calls.indexOf('sync-overlay-mode:true');
assert.ok(syncIndex >= 0);
assert.ok(showIndex >= 0);
assert.ok(syncIndex < showIndex);
assert.ok(passthroughIndex > showIndex);
assert.ok(levelIndex > passthroughIndex);
} finally {
clearLinuxMpvFullscreenOverlayRefreshTimeouts();
if (originalPlatformDescriptor) {
Object.defineProperty(process, 'platform', originalPlatformDescriptor);
}
}
});
test('linux mpv fullscreen overlay refresh preserves active subtitle interaction after restacking', async () => {
const originalPlatformDescriptor = Object.getOwnPropertyDescriptor(process, 'platform');
Object.defineProperty(process, 'platform', {
configurable: true,
value: 'linux',
});
const calls: string[] = [];
try {
scheduleLinuxVisibleOverlayFullscreenRefreshBurst(true, {
overlayManager: {
getMainWindow: () =>
({
hide: () => calls.push('hide'),
isFullScreen: () => false,
isDestroyed: () => false,
isVisible: () => true,
setFullScreen: (fullscreen: boolean) => calls.push(`fullscreen:${fullscreen}`),
setIgnoreMouseEvents: (ignore: boolean, options?: { forward?: boolean }) =>
calls.push(
`mouse-ignore:${ignore}:${options?.forward === true ? 'forward' : 'plain'}`,
),
showInactive: () => calls.push('showInactive'),
}) as never,
getVisibleOverlayVisible: () => true,
},
overlayVisibilityRuntime: {
updateVisibleOverlayVisibility: () => calls.push('updateVisibleOverlayVisibility'),
},
syncVisibleOverlayMpvFullscreenMode: (fullscreen: boolean) =>
calls.push(`sync-overlay-mode:${fullscreen}`),
getOverlayInteractionActive: () => true,
ensureOverlayWindowLevel: () => calls.push('ensureOverlayWindowLevel'),
});
const deadline = Date.now() + 200;
while (!calls.includes('mouse-ignore:false:plain') && Date.now() < deadline) {
await new Promise((resolve) => setTimeout(resolve, 5));
}
const showIndex = calls.indexOf('showInactive');
const interactiveIndex = calls.indexOf('mouse-ignore:false:plain');
assert.ok(showIndex >= 0);
assert.ok(interactiveIndex > showIndex);
assert.equal(calls.includes('mouse-ignore:true:forward'), false);
} finally {
clearLinuxMpvFullscreenOverlayRefreshTimeouts();
if (originalPlatformDescriptor) {