feat: bind overlay state to secondary subtitle mpv visibility

This commit is contained in:
2026-02-26 16:40:51 -08:00
parent 74554a30f0
commit 75442a4648
48 changed files with 1231 additions and 1070 deletions

View File

@@ -23,7 +23,6 @@ export {
export { createAppLifecycleDepsRuntime, startAppLifecycle } from './app-lifecycle';
export { cycleSecondarySubMode } from './subtitle-position';
export {
getInitialInvisibleOverlayVisibility,
isAutoUpdateEnabledRuntime,
shouldAutoInitializeOverlayRuntimeFromConfig,
shouldBindVisibleOverlayToMpvSubVisibility,
@@ -59,14 +58,12 @@ export {
createOverlayWindow,
enforceOverlayLayerOrder,
ensureOverlayWindowLevel,
syncOverlayWindowLayer,
updateOverlayWindowBounds,
} from './overlay-window';
export { initializeOverlayRuntime } from './overlay-runtime-init';
export {
setInvisibleOverlayVisible,
setVisibleOverlayVisible,
syncInvisibleOverlayMousePassthrough,
updateInvisibleOverlayVisibility,
updateVisibleOverlayVisibility,
} from './overlay-visibility';
export {
@@ -76,6 +73,7 @@ export {
replayCurrentSubtitleRuntime,
resolveCurrentAudioStreamIndex,
sendMpvCommandRuntime,
setMpvSecondarySubVisibilityRuntime,
setMpvSubVisibilityRuntime,
showMpvOsdRuntime,
} from './mpv';

View File

@@ -60,6 +60,8 @@ const MPV_SUBTITLE_PROPERTY_OBSERVATIONS: string[] = [
'sub-use-margins',
'pause',
'media-title',
'secondary-sub-visibility',
'sub-visibility',
];
const MPV_INITIAL_PROPERTY_REQUESTS: Array<MpvProtocolCommand> = [

View File

@@ -119,6 +119,38 @@ test('dispatchMpvProtocolMessage emits subtitle text on property change', async
assert.deepEqual(state.events, [{ text: '字幕', isOverlayVisible: false }]);
});
test('dispatchMpvProtocolMessage enforces sub-visibility hidden when overlay suppression is enabled', async () => {
const { deps, state } = createDeps({
shouldBindVisibleOverlayToMpvSubVisibility: () => true,
isVisibleOverlayVisible: () => true,
});
await dispatchMpvProtocolMessage(
{ event: 'property-change', name: 'sub-visibility', data: 'yes' },
deps,
);
assert.deepEqual(state.commands.pop(), {
command: ['set_property', 'sub-visibility', 'no'],
});
});
test('dispatchMpvProtocolMessage enforces secondary sub-visibility hidden when overlay suppression is enabled', async () => {
const { deps, state } = createDeps({
shouldBindVisibleOverlayToMpvSubVisibility: () => true,
isVisibleOverlayVisible: () => true,
});
await dispatchMpvProtocolMessage(
{ event: 'property-change', name: 'secondary-sub-visibility', data: 'yes' },
deps,
);
assert.deepEqual(state.commands.pop(), {
command: ['set_property', 'secondary-sub-visibility', 'no'],
});
});
test('dispatchMpvProtocolMessage sets secondary subtitle track based on track list response', async () => {
const { deps, state } = createDeps();

View File

@@ -48,6 +48,7 @@ export interface MpvProtocolHandleMessageDeps {
};
getSubtitleMetrics: () => MpvSubtitleRenderMetrics;
isVisibleOverlayVisible: () => boolean;
shouldBindVisibleOverlayToMpvSubVisibility?: () => boolean;
emitSubtitleChange: (payload: { text: string; isOverlayVisible: boolean }) => void;
emitSubtitleAssChange: (payload: { text: string }) => void;
emitSubtitleTiming: (payload: { text: string; start: number; end: number }) => void;
@@ -216,6 +217,22 @@ export async function dispatchMpvProtocolMessage(
deps.emitSubtitleMetricsChange({
subScaleByWindow: asBoolean(msg.data, deps.getSubtitleMetrics().subScaleByWindow),
});
} else if (msg.name === 'sub-visibility') {
if (
deps.shouldBindVisibleOverlayToMpvSubVisibility?.() &&
deps.isVisibleOverlayVisible() &&
asBoolean(msg.data, false)
) {
deps.sendCommand({ command: ['set_property', 'sub-visibility', 'no'] });
}
} else if (msg.name === 'secondary-sub-visibility') {
if (
deps.shouldBindVisibleOverlayToMpvSubVisibility?.() &&
deps.isVisibleOverlayVisible() &&
asBoolean(msg.data, false)
) {
deps.sendCommand({ command: ['set_property', 'secondary-sub-visibility', 'no'] });
}
} else if (msg.name === 'sub-use-margins') {
deps.emitSubtitleMetricsChange({
subUseMargins: asBoolean(msg.data, deps.getSubtitleMetrics().subUseMargins),

View File

@@ -306,6 +306,32 @@ test('MpvIpcClient reconnect replays property subscriptions and initial state re
assert.equal(hasPathRequest, true);
});
test('MpvIpcClient connect does not force primary subtitle visibility from binding path', () => {
const commands: unknown[] = [];
const client = new MpvIpcClient(
'/tmp/mpv.sock',
makeDeps({
shouldBindVisibleOverlayToMpvSubVisibility: () => true,
isVisibleOverlayVisible: () => true,
}),
);
(client as any).send = (command: unknown) => {
commands.push(command);
return true;
};
const callbacks = (client as any).transport.callbacks;
callbacks.onConnect();
const hasPrimaryVisibilityMutation = commands.some(
(command) =>
Array.isArray((command as { command: unknown[] }).command) &&
(command as { command: unknown[] }).command[0] === 'set_property' &&
(command as { command: unknown[] }).command[1] === 'sub-visibility',
);
assert.equal(hasPrimaryVisibilityMutation, false);
});
test('MpvIpcClient captures and disables secondary subtitle visibility on request', async () => {
const commands: unknown[] = [];
const client = new MpvIpcClient('/tmp/mpv.sock', makeDeps());

View File

@@ -44,6 +44,7 @@ export interface MpvRuntimeClientLike {
replayCurrentSubtitle?: () => void;
playNextSubtitle?: () => void;
setSubVisibility?: (visible: boolean) => void;
setSecondarySubVisibility?: (visible: boolean) => void;
}
export function showMpvOsdRuntime(
@@ -84,6 +85,14 @@ export function setMpvSubVisibilityRuntime(
mpvClient.setSubVisibility(visible);
}
export function setMpvSecondarySubVisibilityRuntime(
mpvClient: MpvRuntimeClientLike | null,
visible: boolean,
): void {
if (!mpvClient?.setSecondarySubVisibility) return;
mpvClient.setSecondarySubVisibility(visible);
}
export { MPV_REQUEST_ID_SECONDARY_SUB_VISIBILITY } from './mpv-protocol';
export interface MpvIpcClientProtocolDeps {
@@ -181,8 +190,6 @@ export class MpvIpcClient implements MpvClient {
setTimeout(() => {
this.deps.setOverlayVisible(true);
}, 100);
} else if (this.deps.shouldBindVisibleOverlayToMpvSubVisibility()) {
this.setSubVisibility(!this.deps.isVisibleOverlayVisible());
}
this.firstConnection = false;
@@ -290,6 +297,8 @@ export class MpvIpcClient implements MpvClient {
getResolvedConfig: () => this.deps.getResolvedConfig(),
getSubtitleMetrics: () => this.mpvSubtitleRenderMetrics,
isVisibleOverlayVisible: () => this.deps.isVisibleOverlayVisible(),
shouldBindVisibleOverlayToMpvSubVisibility: () =>
this.deps.shouldBindVisibleOverlayToMpvSubVisibility(),
emitSubtitleChange: (payload) => {
this.emit('subtitle-change', payload);
},
@@ -488,7 +497,7 @@ export class MpvIpcClient implements MpvClient {
this.previousSecondarySubVisibility = null;
}
private setSecondarySubVisibility(visible: boolean): void {
setSecondarySubVisibility(visible: boolean): void {
this.send({
command: ['set_property', 'secondary-sub-visibility', visible ? 'yes' : 'no'],
});

View File

@@ -9,11 +9,8 @@ import {
test('overlay manager initializes with empty windows and hidden overlays', () => {
const manager = createOverlayManager();
assert.equal(manager.getMainWindow(), null);
assert.equal(manager.getInvisibleWindow(), null);
assert.equal(manager.getSecondaryWindow(), null);
assert.equal(manager.getModalWindow(), null);
assert.equal(manager.getVisibleOverlayVisible(), false);
assert.equal(manager.getInvisibleOverlayVisible(), false);
assert.deepEqual(manager.getOverlayWindows(), []);
});
@@ -22,28 +19,17 @@ test('overlay manager stores window references and returns stable window order',
const visibleWindow = {
isDestroyed: () => false,
} as unknown as Electron.BrowserWindow;
const invisibleWindow = {
isDestroyed: () => false,
} as unknown as Electron.BrowserWindow;
const secondaryWindow = {
isDestroyed: () => false,
} as unknown as Electron.BrowserWindow;
const modalWindow = {
isDestroyed: () => false,
} as unknown as Electron.BrowserWindow;
manager.setMainWindow(visibleWindow);
manager.setInvisibleWindow(invisibleWindow);
manager.setSecondaryWindow(secondaryWindow);
manager.setModalWindow(modalWindow);
assert.equal(manager.getMainWindow(), visibleWindow);
assert.equal(manager.getInvisibleWindow(), invisibleWindow);
assert.equal(manager.getSecondaryWindow(), secondaryWindow);
assert.equal(manager.getModalWindow(), modalWindow);
assert.equal(manager.getOverlayWindow('visible'), visibleWindow);
assert.equal(manager.getOverlayWindow('invisible'), invisibleWindow);
assert.deepEqual(manager.getOverlayWindows(), [visibleWindow, invisibleWindow, secondaryWindow]);
assert.equal(manager.getOverlayWindow(), visibleWindow);
assert.deepEqual(manager.getOverlayWindows(), [visibleWindow]);
});
test('overlay manager excludes destroyed windows', () => {
@@ -51,26 +37,18 @@ test('overlay manager excludes destroyed windows', () => {
manager.setMainWindow({
isDestroyed: () => true,
} as unknown as Electron.BrowserWindow);
manager.setInvisibleWindow({
isDestroyed: () => false,
} as unknown as Electron.BrowserWindow);
manager.setSecondaryWindow({
isDestroyed: () => true,
} as unknown as Electron.BrowserWindow);
manager.setModalWindow({
isDestroyed: () => false,
} as unknown as Electron.BrowserWindow);
assert.equal(manager.getOverlayWindows().length, 1);
assert.equal(manager.getOverlayWindows().length, 0);
});
test('overlay manager stores visibility state', () => {
const manager = createOverlayManager();
manager.setVisibleOverlayVisible(true);
manager.setInvisibleOverlayVisible(true);
assert.equal(manager.getVisibleOverlayVisible(), true);
assert.equal(manager.getInvisibleOverlayVisible(), true);
});
test('overlay manager broadcasts to non-destroyed windows', () => {
@@ -84,58 +62,25 @@ test('overlay manager broadcasts to non-destroyed windows', () => {
},
},
} as unknown as Electron.BrowserWindow;
const deadWindow = {
isDestroyed: () => true,
webContents: {
send: (..._args: unknown[]) => {},
},
} as unknown as Electron.BrowserWindow;
const secondaryWindow = {
isDestroyed: () => false,
webContents: {
send: (...args: unknown[]) => {
calls.push(args);
},
},
} as unknown as Electron.BrowserWindow;
manager.setMainWindow(aliveWindow);
manager.setInvisibleWindow(deadWindow);
manager.setSecondaryWindow(secondaryWindow);
manager.setModalWindow({
isDestroyed: () => false,
webContents: { send: () => {} },
} as unknown as Electron.BrowserWindow);
manager.broadcastToOverlayWindows('x', 1, 'a');
assert.deepEqual(calls, [
['x', 1, 'a'],
['x', 1, 'a'],
]);
assert.deepEqual(calls, [['x', 1, 'a']]);
});
test('overlay manager applies bounds by layer', () => {
test('overlay manager applies bounds for main and modal windows', () => {
const manager = createOverlayManager();
const visibleCalls: Electron.Rectangle[] = [];
const invisibleCalls: Electron.Rectangle[] = [];
const visibleWindow = {
isDestroyed: () => false,
setBounds: (bounds: Electron.Rectangle) => {
visibleCalls.push(bounds);
},
} as unknown as Electron.BrowserWindow;
const invisibleWindow = {
isDestroyed: () => false,
setBounds: (bounds: Electron.Rectangle) => {
invisibleCalls.push(bounds);
},
} as unknown as Electron.BrowserWindow;
const secondaryWindow = {
isDestroyed: () => false,
setBounds: (bounds: Electron.Rectangle) => {
invisibleCalls.push(bounds);
},
} as unknown as Electron.BrowserWindow;
const modalCalls: Electron.Rectangle[] = [];
const modalWindow = {
isDestroyed: () => false,
@@ -144,28 +89,14 @@ test('overlay manager applies bounds by layer', () => {
},
} as unknown as Electron.BrowserWindow;
manager.setMainWindow(visibleWindow);
manager.setInvisibleWindow(invisibleWindow);
manager.setSecondaryWindow(secondaryWindow);
manager.setModalWindow(modalWindow);
manager.setOverlayWindowBounds('visible', {
manager.setOverlayWindowBounds({
x: 10,
y: 20,
width: 30,
height: 40,
});
manager.setOverlayWindowBounds('invisible', {
x: 1,
y: 2,
width: 3,
height: 4,
});
manager.setSecondaryWindowBounds({
x: 8,
y: 9,
width: 10,
height: 11,
});
manager.setModalWindowBounds({
x: 80,
y: 90,
@@ -174,14 +105,10 @@ test('overlay manager applies bounds by layer', () => {
});
assert.deepEqual(visibleCalls, [{ x: 10, y: 20, width: 30, height: 40 }]);
assert.deepEqual(invisibleCalls, [
{ x: 1, y: 2, width: 3, height: 4 },
{ x: 8, y: 9, width: 10, height: 11 },
]);
assert.deepEqual(modalCalls, [{ x: 80, y: 90, width: 100, height: 110 }]);
});
test('runtime-option and debug broadcasts use expected channels', () => {
test('runtime-option broadcast still uses expected channel', () => {
const broadcasts: unknown[][] = [];
broadcastRuntimeOptionsChangedRuntime(
() => [],
@@ -196,14 +123,8 @@ test('runtime-option and debug broadcasts use expected channels', () => {
(enabled) => {
state = enabled;
},
(channel, ...args) => {
broadcasts.push([channel, ...args]);
},
);
assert.equal(changed, true);
assert.equal(state, true);
assert.deepEqual(broadcasts, [
['runtime-options:changed', []],
['overlay-debug-visualization:set', true],
]);
assert.deepEqual(broadcasts, [['runtime-options:changed', []]]);
});

View File

@@ -2,60 +2,37 @@ import type { BrowserWindow } from 'electron';
import { RuntimeOptionState, WindowGeometry } from '../../types';
import { updateOverlayWindowBounds } from './overlay-window';
type OverlayLayer = 'visible' | 'invisible';
export interface OverlayManager {
getMainWindow: () => BrowserWindow | null;
setMainWindow: (window: BrowserWindow | null) => void;
getInvisibleWindow: () => BrowserWindow | null;
setInvisibleWindow: (window: BrowserWindow | null) => void;
getSecondaryWindow: () => BrowserWindow | null;
setSecondaryWindow: (window: BrowserWindow | null) => void;
getModalWindow: () => BrowserWindow | null;
setModalWindow: (window: BrowserWindow | null) => void;
getOverlayWindow: (layer: OverlayLayer) => BrowserWindow | null;
setOverlayWindowBounds: (layer: OverlayLayer, geometry: WindowGeometry) => void;
setSecondaryWindowBounds: (geometry: WindowGeometry) => void;
getOverlayWindow: () => BrowserWindow | null;
setOverlayWindowBounds: (geometry: WindowGeometry) => void;
setModalWindowBounds: (geometry: WindowGeometry) => void;
getVisibleOverlayVisible: () => boolean;
setVisibleOverlayVisible: (visible: boolean) => void;
getInvisibleOverlayVisible: () => boolean;
setInvisibleOverlayVisible: (visible: boolean) => void;
getOverlayWindows: () => BrowserWindow[];
broadcastToOverlayWindows: (channel: string, ...args: unknown[]) => void;
}
export function createOverlayManager(): OverlayManager {
let mainWindow: BrowserWindow | null = null;
let invisibleWindow: BrowserWindow | null = null;
let secondaryWindow: BrowserWindow | null = null;
let modalWindow: BrowserWindow | null = null;
let visibleOverlayVisible = false;
let invisibleOverlayVisible = false;
return {
getMainWindow: () => mainWindow,
setMainWindow: (window) => {
mainWindow = window;
},
getInvisibleWindow: () => invisibleWindow,
setInvisibleWindow: (window) => {
invisibleWindow = window;
},
getSecondaryWindow: () => secondaryWindow,
setSecondaryWindow: (window) => {
secondaryWindow = window;
},
getModalWindow: () => modalWindow,
setModalWindow: (window) => {
modalWindow = window;
},
getOverlayWindow: (layer) => (layer === 'visible' ? mainWindow : invisibleWindow),
setOverlayWindowBounds: (layer, geometry) => {
updateOverlayWindowBounds(geometry, layer === 'visible' ? mainWindow : invisibleWindow);
},
setSecondaryWindowBounds: (geometry) => {
updateOverlayWindowBounds(geometry, secondaryWindow);
getOverlayWindow: () => mainWindow,
setOverlayWindowBounds: (geometry) => {
updateOverlayWindowBounds(geometry, mainWindow);
},
setModalWindowBounds: (geometry) => {
updateOverlayWindowBounds(geometry, modalWindow);
@@ -64,36 +41,12 @@ export function createOverlayManager(): OverlayManager {
setVisibleOverlayVisible: (visible) => {
visibleOverlayVisible = visible;
},
getInvisibleOverlayVisible: () => invisibleOverlayVisible,
setInvisibleOverlayVisible: (visible) => {
invisibleOverlayVisible = visible;
},
getOverlayWindows: () => {
const windows: BrowserWindow[] = [];
if (mainWindow && !mainWindow.isDestroyed()) {
windows.push(mainWindow);
}
if (invisibleWindow && !invisibleWindow.isDestroyed()) {
windows.push(invisibleWindow);
}
if (secondaryWindow && !secondaryWindow.isDestroyed()) {
windows.push(secondaryWindow);
}
return windows;
return mainWindow && !mainWindow.isDestroyed() ? [mainWindow] : [];
},
broadcastToOverlayWindows: (channel, ...args) => {
const windows: BrowserWindow[] = [];
if (mainWindow && !mainWindow.isDestroyed()) {
windows.push(mainWindow);
}
if (invisibleWindow && !invisibleWindow.isDestroyed()) {
windows.push(invisibleWindow);
}
if (secondaryWindow && !secondaryWindow.isDestroyed()) {
windows.push(secondaryWindow);
}
for (const window of windows) {
window.webContents.send(channel, ...args);
mainWindow.webContents.send(channel, ...args);
}
},
};
@@ -110,10 +63,8 @@ export function setOverlayDebugVisualizationEnabledRuntime(
currentEnabled: boolean,
nextEnabled: boolean,
setState: (enabled: boolean) => void,
broadcastToOverlayWindows: (channel: string, ...args: unknown[]) => void,
): boolean {
if (currentEnabled === nextEnabled) return false;
setState(nextEnabled);
broadcastToOverlayWindows('overlay-debug-visualization:set', nextEnabled);
return true;
}

View File

@@ -1,41 +0,0 @@
import type { WindowGeometry } from '../../types';
export const SECONDARY_OVERLAY_MAX_HEIGHT_RATIO = 0.2;
function toInteger(value: number): number {
return Number.isFinite(value) ? Math.round(value) : 0;
}
function clampPositive(value: number): number {
return Math.max(1, toInteger(value));
}
export function splitOverlayGeometryForSecondaryBar(geometry: WindowGeometry): {
secondary: WindowGeometry;
primary: WindowGeometry;
} {
const x = toInteger(geometry.x);
const y = toInteger(geometry.y);
const width = clampPositive(geometry.width);
const totalHeight = clampPositive(geometry.height);
const secondaryHeight = clampPositive(
Math.min(totalHeight, Math.round(totalHeight * SECONDARY_OVERLAY_MAX_HEIGHT_RATIO)),
);
const primaryHeight = clampPositive(totalHeight - secondaryHeight);
return {
secondary: {
x,
y,
width,
height: secondaryHeight,
},
primary: {
x,
y: y + secondaryHeight,
width,
height: primaryHeight,
},
};
}

View File

@@ -1,37 +0,0 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { splitOverlayGeometryForSecondaryBar } from './overlay-window-geometry';
test('splitOverlayGeometryForSecondaryBar returns 20/80 top-bottom regions', () => {
const regions = splitOverlayGeometryForSecondaryBar({
x: 100,
y: 50,
width: 1200,
height: 900,
});
assert.deepEqual(regions.secondary, {
x: 100,
y: 50,
width: 1200,
height: 180,
});
assert.deepEqual(regions.primary, {
x: 100,
y: 230,
width: 1200,
height: 720,
});
});
test('splitOverlayGeometryForSecondaryBar keeps positive sizes for tiny heights', () => {
const regions = splitOverlayGeometryForSecondaryBar({
x: 0,
y: 0,
width: 300,
height: 1,
});
assert.ok(regions.secondary.height >= 1);
assert.ok(regions.primary.height >= 1);
});

View File

@@ -4,8 +4,25 @@ import { WindowGeometry } from '../../types';
import { createLogger } from '../../logger';
const logger = createLogger('main:overlay-window');
const overlayWindowLayerByInstance = new WeakMap<BrowserWindow, OverlayWindowKind>();
export type OverlayWindowKind = 'visible' | 'invisible' | 'secondary' | 'modal';
function getOverlayWindowHtmlPath(): string {
return path.join(__dirname, '..', '..', 'renderer', 'index.html');
}
function loadOverlayWindowLayer(window: BrowserWindow, layer: OverlayWindowKind): void {
overlayWindowLayerByInstance.set(window, layer);
const htmlPath = getOverlayWindowHtmlPath();
window
.loadFile(htmlPath, {
query: { layer },
})
.catch((err) => {
logger.error('Failed to load HTML file:', err);
});
}
export type OverlayWindowKind = 'visible' | 'modal';
export function updateOverlayWindowBounds(
geometry: WindowGeometry,
@@ -32,14 +49,11 @@ export function ensureOverlayWindowLevel(window: BrowserWindow): void {
export function enforceOverlayLayerOrder(options: {
visibleOverlayVisible: boolean;
invisibleOverlayVisible: boolean;
mainWindow: BrowserWindow | null;
invisibleWindow: BrowserWindow | null;
ensureOverlayWindowLevel: (window: BrowserWindow) => void;
}): void {
if (!options.visibleOverlayVisible || !options.invisibleOverlayVisible) return;
if (!options.visibleOverlayVisible) return;
if (!options.mainWindow || options.mainWindow.isDestroyed()) return;
if (!options.invisibleWindow || options.invisibleWindow.isDestroyed()) return;
options.ensureOverlayWindowLevel(options.mainWindow);
options.mainWindow.moveTop();
@@ -49,7 +63,6 @@ export function createOverlayWindow(
kind: OverlayWindowKind,
options: {
isDev: boolean;
overlayDebugVisualizationEnabled: boolean;
ensureOverlayWindowLevel: (window: BrowserWindow) => void;
onRuntimeOptionsChanged: () => void;
setOverlayDebugVisualizationEnabled: (enabled: boolean) => void;
@@ -83,16 +96,7 @@ export function createOverlayWindow(
});
options.ensureOverlayWindowLevel(window);
const htmlPath = path.join(__dirname, '..', '..', 'renderer', 'index.html');
window
.loadFile(htmlPath, {
query: { layer: kind },
})
.catch((err) => {
logger.error('Failed to load HTML file:', err);
});
loadOverlayWindowLayer(window, kind);
window.webContents.on('did-fail-load', (_event, errorCode, errorDescription, validatedURL) => {
logger.error('Page failed to load:', errorCode, errorDescription, validatedURL);
@@ -100,10 +104,6 @@ export function createOverlayWindow(
window.webContents.on('did-finish-load', () => {
options.onRuntimeOptionsChanged();
window.webContents.send(
'overlay-debug-visualization:set',
options.overlayDebugVisualizationEnabled,
);
});
if (kind === 'visible') {
@@ -140,3 +140,9 @@ export function createOverlayWindow(
return window;
}
export function syncOverlayWindowLayer(window: BrowserWindow, layer: 'visible'): void {
if (window.isDestroyed()) return;
if (overlayWindowLayerByInstance.get(window) === layer) return;
loadOverlayWindowLayer(window, layer);
}