refactor(main): extract overlay geometry runtime from main.ts

This commit is contained in:
2026-06-11 23:28:55 -07:00
parent 1fc83a842d
commit eb1af727bb
3 changed files with 373 additions and 233 deletions
+52 -231
View File
@@ -49,7 +49,6 @@ import {
} from './main/runtime/linux-mpv-fullscreen-overlay-refresh'; } from './main/runtime/linux-mpv-fullscreen-overlay-refresh';
import { import {
resolveLinuxVisibleOverlayWindowModeAction, resolveLinuxVisibleOverlayWindowModeAction,
shouldExitFullscreenOverrideForTrackedGeometry,
type LinuxVisibleOverlayWindowMode, type LinuxVisibleOverlayWindowMode,
} from './main/runtime/linux-visible-overlay-window-mode'; } from './main/runtime/linux-visible-overlay-window-mode';
import { import {
@@ -218,9 +217,6 @@ import {
createBuildRestorePreviousSecondarySubVisibilityMainDepsHandler, createBuildRestorePreviousSecondarySubVisibilityMainDepsHandler,
createBuildSendToActiveOverlayWindowMainDepsHandler, createBuildSendToActiveOverlayWindowMainDepsHandler,
createBuildSetOverlayDebugVisualizationEnabledMainDepsHandler, createBuildSetOverlayDebugVisualizationEnabledMainDepsHandler,
createBuildEnforceOverlayLayerOrderMainDepsHandler,
createBuildEnsureOverlayWindowLevelMainDepsHandler,
createBuildUpdateVisibleOverlayBoundsMainDepsHandler,
createTrayRuntimeHandlers, createTrayRuntimeHandlers,
createOverlayVisibilityRuntime, createOverlayVisibilityRuntime,
createBroadcastRuntimeOptionsChangedHandler, createBroadcastRuntimeOptionsChangedHandler,
@@ -231,10 +227,6 @@ import {
createRestorePreviousSecondarySubVisibilityHandler, createRestorePreviousSecondarySubVisibilityHandler,
createSendToActiveOverlayWindowHandler, createSendToActiveOverlayWindowHandler,
createSetOverlayDebugVisualizationEnabledHandler, createSetOverlayDebugVisualizationEnabledHandler,
createEnforceOverlayLayerOrderHandler,
createEnsureOverlayWindowLevelHandler,
createUpdateVisibleOverlayBoundsHandler,
hasLiveOverlayWindowBoundsMismatch,
createLoadSubtitlePositionHandler, createLoadSubtitlePositionHandler,
createSaveSubtitlePositionHandler, createSaveSubtitlePositionHandler,
createAppendClipboardVideoToQueueHandler, createAppendClipboardVideoToQueueHandler,
@@ -322,8 +314,6 @@ import {
cycleSecondarySubMode as cycleSecondarySubModeCore, cycleSecondarySubMode as cycleSecondarySubModeCore,
deleteYomitanDictionaryByTitle, deleteYomitanDictionaryByTitle,
destroyYomitanSettingsWindow, destroyYomitanSettingsWindow,
enforceOverlayLayerOrder as enforceOverlayLayerOrderCore,
ensureOverlayWindowLevel as ensureOverlayWindowLevelCore,
getYomitanDictionaryInfo, getYomitanDictionaryInfo,
handleMineSentenceDigit as handleMineSentenceDigitCore, handleMineSentenceDigit as handleMineSentenceDigitCore,
handleMultiCopyDigit as handleMultiCopyDigitCore, handleMultiCopyDigit as handleMultiCopyDigitCore,
@@ -355,7 +345,6 @@ import {
sendMpvCommandRuntime, sendMpvCommandRuntime,
setMpvSubVisibilityRuntime, setMpvSubVisibilityRuntime,
setOverlayDebugVisualizationEnabledRuntime, setOverlayDebugVisualizationEnabledRuntime,
syncOverlayWindowLayer,
setVisibleOverlayVisible as setVisibleOverlayVisibleCore, setVisibleOverlayVisible as setVisibleOverlayVisibleCore,
showMpvOsdRuntime, showMpvOsdRuntime,
startOverlayWindowTracker as startOverlayWindowTrackerCore, startOverlayWindowTracker as startOverlayWindowTrackerCore,
@@ -368,13 +357,10 @@ import {
acquireYoutubeSubtitleTrack, acquireYoutubeSubtitleTrack,
acquireYoutubeSubtitleTracks, acquireYoutubeSubtitleTracks,
} from './core/services/youtube/generate'; } from './core/services/youtube/generate';
import { hasHyprlandWindowPlacementBoundsMismatch } from './core/services/hyprland-window-placement';
import { normalizeOverlayWindowBoundsForPlatform } from './core/services/overlay-window-bounds';
import { resolveYoutubePlaybackUrl } from './core/services/youtube/playback-resolve'; import { resolveYoutubePlaybackUrl } from './core/services/youtube/playback-resolve';
import { probeYoutubeTracks } from './core/services/youtube/track-probe'; import { probeYoutubeTracks } from './core/services/youtube/track-probe';
import { import {
destroyStatsWindow, destroyStatsWindow,
promoteStatsOverlayAbovePlayback,
registerStatsOverlayToggle, registerStatsOverlayToggle,
toggleStatsOverlay as toggleStatsOverlayWindow, toggleStatsOverlay as toggleStatsOverlayWindow,
withStatsWindowLayerSuspendedForNativeDialog, withStatsWindowLayerSuspendedForNativeDialog,
@@ -538,6 +524,7 @@ import {
resolveCurrentSubtitleForRenderer, resolveCurrentSubtitleForRenderer,
} from './main/runtime/current-subtitle-snapshot'; } from './main/runtime/current-subtitle-snapshot';
import { restoreLinuxOverlayWindowShape } from './main/runtime/linux-overlay-window-shape'; import { restoreLinuxOverlayWindowShape } from './main/runtime/linux-overlay-window-shape';
import { createOverlayGeometryRuntime } from './main/runtime/overlay-geometry-runtime';
import { createJellyfinSubtitleCacheIo } from './main/runtime/jellyfin-subtitle-cache-io'; import { createJellyfinSubtitleCacheIo } from './main/runtime/jellyfin-subtitle-cache-io';
import { createStartupOsdSequencer } from './main/runtime/startup-osd-sequencer'; import { createStartupOsdSequencer } from './main/runtime/startup-osd-sequencer';
import { import {
@@ -1157,7 +1144,10 @@ const youtubeFlowRuntime = createYoutubeFlowRuntime({
while (Date.now() < deadline) { while (Date.now() < deadline) {
const tracker = appState.windowTracker; const tracker = appState.windowTracker;
const trackerGeometry = tracker?.getGeometry() ?? null; const trackerGeometry = tracker?.getGeometry() ?? null;
if (trackerGeometry && geometryMatches(lastOverlayWindowGeometry, trackerGeometry)) { if (
trackerGeometry &&
geometryMatches(overlayGeometryRuntime.getLastOverlayWindowGeometry(), trackerGeometry)
) {
return; return;
} }
await new Promise((resolve) => setTimeout(resolve, 50)); await new Promise((resolve) => setTimeout(resolve, 50));
@@ -2580,7 +2570,6 @@ const WINDOWS_VISIBLE_OVERLAY_Z_ORDER_RETRY_DELAYS_MS = [0, 48, 120, 240, 480] a
const WINDOWS_VISIBLE_OVERLAY_FOREGROUND_POLL_INTERVAL_MS = 75; const WINDOWS_VISIBLE_OVERLAY_FOREGROUND_POLL_INTERVAL_MS = 75;
const WINDOWS_VISIBLE_OVERLAY_FOCUS_HANDOFF_GRACE_MS = 200; const WINDOWS_VISIBLE_OVERLAY_FOCUS_HANDOFF_GRACE_MS = 200;
const LINUX_VISIBLE_OVERLAY_FOCUS_HANDOFF_GRACE_MS = 1_500; const LINUX_VISIBLE_OVERLAY_FOCUS_HANDOFF_GRACE_MS = 1_500;
const LINUX_VISIBLE_OVERLAY_FULLSCREEN_GEOMETRY_GRACE_MS = 1_200;
// Ignore transient "neither mpv nor overlay is the active window" blips before suppressing // Ignore transient "neither mpv nor overlay is the active window" blips before suppressing
// subtitle pointer interaction. Right after playback starts the overlay can briefly become the // subtitle pointer interaction. Right after playback starts the overlay can briefly become the
// X11 active window, which would otherwise leave subtitles inert for a poll cycle (~1s). // X11 active window, which would otherwise leave subtitles inert for a poll cycle (~1s).
@@ -2877,7 +2866,7 @@ function retargetOverlayWindowTrackerForMpvSocket(
releaseVisibleOverlayOwner(); releaseVisibleOverlayOwner();
appState.windowTracker = null; appState.windowTracker = null;
appState.trackerNotReadyWarningShown = false; appState.trackerNotReadyWarningShown = false;
lastOverlayWindowGeometry = null; overlayGeometryRuntime.resetLastOverlayWindowGeometry();
startOverlayWindowTrackerForCurrentSocket(); startOverlayWindowTrackerForCurrentSocket();
overlayVisibilityRuntime.updateVisibleOverlayVisibility(); overlayVisibilityRuntime.updateVisibleOverlayVisibility();
overlayShortcutsRuntime.syncOverlayShortcuts(); overlayShortcutsRuntime.syncOverlayShortcuts();
@@ -5372,236 +5361,68 @@ function updateMpvSubtitleRenderMetrics(patch: Partial<MpvSubtitleRenderMetrics>
updateMpvSubtitleRenderMetricsHandler(patch); updateMpvSubtitleRenderMetricsHandler(patch);
} }
let lastOverlayWindowGeometry: WindowGeometry | null = null; const overlayGeometryRuntime = createOverlayGeometryRuntime({
overlayManager: {
function getOverlayGeometryFallback(): WindowGeometry { getMainWindow: () => overlayManager.getMainWindow(),
const cursorPoint = screen.getCursorScreenPoint(); getModalWindow: () => overlayManager.getModalWindow(),
const display = screen.getDisplayNearestPoint(cursorPoint); getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
const bounds = display.workArea; setOverlayWindowBounds: (geometry) => overlayManager.setOverlayWindowBounds(geometry),
return { setModalWindowBounds: (geometry) => overlayManager.setModalWindowBounds(geometry),
x: bounds.x, },
y: bounds.y, getTrackedWindowGeometry: () => appState.windowTracker?.getGeometry() ?? null,
width: bounds.width, getTrackedWindowMediaSourceId: () => appState.windowTracker?.getTargetWindowMediaSourceId?.(),
height: bounds.height, getTrackedWindowNativeId: () => appState.windowTracker?.getTargetWindowNativeId?.(),
}; getStatsOverlayVisible: () => appState.statsOverlayVisible,
} getOverlayForegroundSeparateWindows: () => getOverlayForegroundSeparateWindows(),
getLinuxVisibleOverlayWindowMode: () => linuxVisibleOverlayWindowMode,
getLinuxTrackedMpvFullscreen: () => linuxTrackedMpvFullscreen,
getLinuxTrackedMpvFullscreenChangedAtMs: () => linuxTrackedMpvFullscreenChangedAtMs,
syncLinuxVisibleOverlayMpvFullscreenMode: (fullscreen) =>
syncLinuxVisibleOverlayMpvFullscreenMode(fullscreen),
getLinuxVisibleOverlayOwnerBindingKey: () => linuxVisibleOverlayOwnerBindingKey,
setLinuxVisibleOverlayOwnerBindingKey: (key) => {
linuxVisibleOverlayOwnerBindingKey = key;
},
clearVisibleOverlayX11OwnerBinding: (window) => clearVisibleOverlayX11OwnerBinding(window),
getNativeWindowHandleDecimal: (window) => getNativeWindowHandleDecimal(window),
enqueueVisibleOverlayX11OwnerBindingOperation: (window, args, onError) =>
enqueueVisibleOverlayX11OwnerBindingOperation(window, args, onError),
scheduleWindowsVisibleOverlayZOrderSyncBurst: () =>
scheduleWindowsVisibleOverlayZOrderSyncBurst(),
logDebug: (message, ...args) => logger.debug(message, ...args),
});
function getCurrentOverlayGeometry(): WindowGeometry { function getCurrentOverlayGeometry(): WindowGeometry {
if (lastOverlayWindowGeometry) return lastOverlayWindowGeometry; return overlayGeometryRuntime.getCurrentOverlayGeometry();
const trackerGeometry = appState.windowTracker?.getGeometry();
if (trackerGeometry) return trackerGeometry;
return getOverlayGeometryFallback();
} }
function getCurrentTrackedOverlayGeometry(): WindowGeometry | null { function getCurrentTrackedOverlayGeometry(): WindowGeometry | null {
return appState.windowTracker?.getGeometry() ?? null; return overlayGeometryRuntime.getCurrentTrackedOverlayGeometry();
} }
function geometryMatches(a: WindowGeometry | null, b: WindowGeometry | null): boolean { function geometryMatches(a: WindowGeometry | null, b: WindowGeometry | null): boolean {
if (!a || !b) return false; return overlayGeometryRuntime.geometryMatches(a, b);
return a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height;
} }
function applyOverlayRegions(geometry: WindowGeometry): void {
lastOverlayWindowGeometry = geometry;
maybeExitLinuxFullscreenOverrideForTrackedGeometry(geometry);
overlayManager.setOverlayWindowBounds(geometry);
overlayManager.setModalWindowBounds(geometry);
}
function shouldExitLinuxFullscreenOverrideForGeometry(geometry: WindowGeometry): boolean {
if (!shouldRunLinuxOverlayZOrderKeepAlive()) {
return false;
}
if (
linuxTrackedMpvFullscreenChangedAtMs > 0 &&
Date.now() - linuxTrackedMpvFullscreenChangedAtMs <
LINUX_VISIBLE_OVERLAY_FULLSCREEN_GEOMETRY_GRACE_MS
) {
return false;
}
const displayBounds = screen.getDisplayMatching(geometry).bounds;
return shouldExitFullscreenOverrideForTrackedGeometry({
currentMode: linuxVisibleOverlayWindowMode,
trackedFullscreen: linuxTrackedMpvFullscreen,
geometry,
displayBounds,
});
}
function maybeExitLinuxFullscreenOverrideForTrackedGeometry(geometry: WindowGeometry): void {
if (!shouldExitLinuxFullscreenOverrideForGeometry(geometry)) {
return;
}
logger.debug(
'Tracked mpv geometry no longer covers its display; exiting Linux fullscreen overlay override',
);
syncLinuxVisibleOverlayMpvFullscreenMode(false);
}
function hasHyprlandOverlayWindowPlacementMismatch(geometry: WindowGeometry): boolean {
if (process.platform !== 'linux') {
return false;
}
return [overlayManager.getMainWindow(), overlayManager.getModalWindow()].some((window) => {
if (!window || window.isDestroyed()) {
return false;
}
return hasHyprlandWindowPlacementBoundsMismatch({
title: window.getTitle(),
bounds: normalizeOverlayWindowBoundsForPlatform(geometry, process.platform, screen, window),
});
});
}
const buildUpdateVisibleOverlayBoundsMainDepsHandler =
createBuildUpdateVisibleOverlayBoundsMainDepsHandler({
getCurrentOverlayWindowBounds: () => lastOverlayWindowGeometry,
shouldRefreshUnchangedGeometry: (geometry) =>
shouldExitLinuxFullscreenOverrideForGeometry(geometry) ||
(process.platform === 'linux' &&
(hasLiveOverlayWindowBoundsMismatch(
[overlayManager.getMainWindow(), overlayManager.getModalWindow()],
geometry,
) ||
hasHyprlandOverlayWindowPlacementMismatch(geometry))),
setOverlayWindowBounds: (geometry) => applyOverlayRegions(geometry),
afterSetOverlayWindowBounds: () => {
if (!overlayManager.getVisibleOverlayVisible()) {
return;
}
if (process.platform === 'win32') {
scheduleWindowsVisibleOverlayZOrderSyncBurst();
return;
}
const mainWindow = overlayManager.getMainWindow();
if (!mainWindow || mainWindow.isDestroyed()) {
return;
}
if (process.platform === 'linux') {
restoreLinuxOverlayWindowShape(mainWindow);
}
ensureOverlayWindowLevel(mainWindow);
},
});
const updateVisibleOverlayBoundsMainDeps = buildUpdateVisibleOverlayBoundsMainDepsHandler();
const updateVisibleOverlayBounds = createUpdateVisibleOverlayBoundsHandler(
updateVisibleOverlayBoundsMainDeps,
);
const buildEnsureOverlayWindowLevelMainDepsHandler =
createBuildEnsureOverlayWindowLevelMainDepsHandler({
shouldSuppressOverlayWindowLevel: (window) => {
const mainWindow = overlayManager.getMainWindow();
return (
(appState.statsOverlayVisible && window === mainWindow) ||
shouldSuppressVisibleOverlayRaiseForSeparateWindow({
window,
mainWindow,
separateWindows: getOverlayForegroundSeparateWindows(),
})
);
},
ensureOverlayWindowLevelCore: (window) => ensureOverlayWindowLevelCore(window as BrowserWindow),
afterEnsureOverlayWindowLevel: () => {
const mainWindow = overlayManager.getMainWindow();
if (mainWindow && !mainWindow.isDestroyed()) {
moveVisibleOverlayAboveTrackedPlaybackWindow(mainWindow);
}
promoteStatsOverlayAbovePlayback();
},
});
const ensureOverlayWindowLevelMainDeps = buildEnsureOverlayWindowLevelMainDepsHandler();
const ensureOverlayWindowLevel = createEnsureOverlayWindowLevelHandler(
ensureOverlayWindowLevelMainDeps,
);
function syncPrimaryOverlayWindowLayer(layer: 'visible'): void { function syncPrimaryOverlayWindowLayer(layer: 'visible'): void {
const mainWindow = overlayManager.getMainWindow(); overlayGeometryRuntime.syncPrimaryOverlayWindowLayer(layer);
if (!mainWindow || mainWindow.isDestroyed()) return;
syncOverlayWindowLayer(mainWindow, layer);
}
function moveVisibleOverlayAboveTrackedPlaybackWindow(window: BrowserWindow): void {
if (process.platform !== 'linux') return;
if (window !== overlayManager.getMainWindow()) return;
bindVisibleOverlayToTrackedX11Window(window);
const mediaSourceId = appState.windowTracker?.getTargetWindowMediaSourceId?.();
if (!mediaSourceId) return;
try {
window.moveAbove(mediaSourceId);
} catch (error) {
logger.debug(
'Failed to move visible overlay above tracked playback window:',
error instanceof Error ? error.message : String(error),
);
}
} }
function bindVisibleOverlayToTrackedX11Window(window: BrowserWindow): void { function bindVisibleOverlayToTrackedX11Window(window: BrowserWindow): void {
const targetWindowId = appState.windowTracker?.getTargetWindowNativeId?.(); overlayGeometryRuntime.bindVisibleOverlayToTrackedX11Window(window);
if (!targetWindowId) {
if (linuxVisibleOverlayOwnerBindingKey !== null) {
clearVisibleOverlayX11OwnerBinding(window);
}
linuxVisibleOverlayOwnerBindingKey = null;
return;
}
const overlayWindowId = getNativeWindowHandleDecimal(window);
const bindingKey = `${overlayWindowId}:${targetWindowId}`;
if (linuxVisibleOverlayOwnerBindingKey === bindingKey) {
return;
}
linuxVisibleOverlayOwnerBindingKey = bindingKey;
enqueueVisibleOverlayX11OwnerBindingOperation(
window,
[
'-id',
overlayWindowId,
'-f',
'WM_TRANSIENT_FOR',
'32x',
'-set',
'WM_TRANSIENT_FOR',
targetWindowId,
],
(error) => {
if (linuxVisibleOverlayOwnerBindingKey === bindingKey) {
linuxVisibleOverlayOwnerBindingKey = null;
}
logger.debug(
'Failed to bind visible overlay as transient for tracked X11 playback window:',
error instanceof Error ? error.message : String(error),
);
},
);
} }
const buildEnforceOverlayLayerOrderMainDepsHandler = function updateVisibleOverlayBounds(geometry: WindowGeometry): void {
createBuildEnforceOverlayLayerOrderMainDepsHandler({ overlayGeometryRuntime.updateVisibleOverlayBounds(geometry);
enforceOverlayLayerOrderCore: (params) => }
enforceOverlayLayerOrderCore({
visibleOverlayVisible: params.visibleOverlayVisible, function ensureOverlayWindowLevel(window: unknown): void {
mainWindow: params.mainWindow as BrowserWindow | null, overlayGeometryRuntime.ensureOverlayWindowLevel(window);
ensureOverlayWindowLevel: (window) => }
params.ensureOverlayWindowLevel(window as BrowserWindow),
}), function enforceOverlayLayerOrder(): void {
getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(), overlayGeometryRuntime.enforceOverlayLayerOrder();
getMainWindow: () => overlayManager.getMainWindow(), }
ensureOverlayWindowLevel: (window) => ensureOverlayWindowLevel(window as BrowserWindow),
});
const enforceOverlayLayerOrderMainDeps = buildEnforceOverlayLayerOrderMainDepsHandler();
const enforceOverlayLayerOrder = createEnforceOverlayLayerOrderHandler(
enforceOverlayLayerOrderMainDeps,
);
async function loadYomitanExtension(): Promise<Extension | null> { async function loadYomitanExtension(): Promise<Extension | null> {
const extension = await yomitanExtensionRuntime.loadYomitanExtension(); const extension = await yomitanExtensionRuntime.loadYomitanExtension();
+2 -2
View File
@@ -524,9 +524,9 @@ test('Linux visible overlay show/reset does not leave an empty X11 window shape'
}); });
test('Linux visible overlay bounds refresh restores X11 shape after applying mpv geometry', () => { test('Linux visible overlay bounds refresh restores X11 shape after applying mpv geometry', () => {
const source = readMainSource(); const source = readSource('src/main/runtime/overlay-geometry-runtime.ts');
const afterBoundsBlock = source.match( const afterBoundsBlock = source.match(
/afterSetOverlayWindowBounds:\s*\(\) => \{(?<body>[\s\S]*?)\n \},/, /afterSetOverlayWindowBounds:\s*\(\) => \{(?<body>[\s\S]*?)\n \},/,
)?.groups?.body; )?.groups?.body;
assert.ok(afterBoundsBlock); assert.ok(afterBoundsBlock);
@@ -0,0 +1,319 @@
import { type BrowserWindow, screen } from 'electron';
import type { WindowGeometry } from '../../types';
import { hasHyprlandWindowPlacementBoundsMismatch } from '../../core/services/hyprland-window-placement';
import { normalizeOverlayWindowBoundsForPlatform } from '../../core/services/overlay-window-bounds';
import {
enforceOverlayLayerOrder as enforceOverlayLayerOrderCore,
ensureOverlayWindowLevel as ensureOverlayWindowLevelCore,
syncOverlayWindowLayer,
} from '../../core/services/overlay-window';
import { promoteStatsOverlayAbovePlayback } from '../../core/services/stats-window.js';
import { restoreLinuxOverlayWindowShape } from './linux-overlay-window-shape';
import { shouldRunLinuxOverlayZOrderKeepAlive } from './linux-overlay-zorder-keepalive';
import {
shouldExitFullscreenOverrideForTrackedGeometry,
type LinuxVisibleOverlayWindowMode,
} from './linux-visible-overlay-window-mode';
import {
createEnforceOverlayLayerOrderHandler,
createEnsureOverlayWindowLevelHandler,
createUpdateVisibleOverlayBoundsHandler,
hasLiveOverlayWindowBoundsMismatch,
} from './overlay-window-layout';
import {
createBuildEnforceOverlayLayerOrderMainDepsHandler,
createBuildEnsureOverlayWindowLevelMainDepsHandler,
createBuildUpdateVisibleOverlayBoundsMainDepsHandler,
} from './overlay-window-layout-main-deps';
import { shouldSuppressVisibleOverlayRaiseForSeparateWindow } from './settings-window-z-order';
const LINUX_VISIBLE_OVERLAY_FULLSCREEN_GEOMETRY_GRACE_MS = 1_200;
export interface OverlayGeometryRuntimeDeps {
overlayManager: {
getMainWindow: () => BrowserWindow | null;
getModalWindow: () => BrowserWindow | null;
getVisibleOverlayVisible: () => boolean;
setOverlayWindowBounds: (geometry: WindowGeometry) => void;
setModalWindowBounds: (geometry: WindowGeometry) => void;
};
getTrackedWindowGeometry: () => WindowGeometry | null;
getTrackedWindowMediaSourceId: () => string | null | undefined;
getTrackedWindowNativeId: () => string | null | undefined;
getStatsOverlayVisible: () => boolean;
getOverlayForegroundSeparateWindows: () => BrowserWindow[];
getLinuxVisibleOverlayWindowMode: () => LinuxVisibleOverlayWindowMode;
getLinuxTrackedMpvFullscreen: () => boolean;
getLinuxTrackedMpvFullscreenChangedAtMs: () => number;
syncLinuxVisibleOverlayMpvFullscreenMode: (fullscreen: boolean) => void;
getLinuxVisibleOverlayOwnerBindingKey: () => string | null;
setLinuxVisibleOverlayOwnerBindingKey: (key: string | null) => void;
clearVisibleOverlayX11OwnerBinding: (window: BrowserWindow) => void;
getNativeWindowHandleDecimal: (window: BrowserWindow) => string;
enqueueVisibleOverlayX11OwnerBindingOperation: (
window: BrowserWindow,
args: string[],
onError?: (error: Error) => void,
) => void;
scheduleWindowsVisibleOverlayZOrderSyncBurst: () => void;
logDebug: (message: string, ...args: unknown[]) => void;
}
export function createOverlayGeometryRuntime(deps: OverlayGeometryRuntimeDeps) {
const { overlayManager } = deps;
let lastOverlayWindowGeometry: WindowGeometry | null = null;
function getOverlayGeometryFallback(): WindowGeometry {
const cursorPoint = screen.getCursorScreenPoint();
const display = screen.getDisplayNearestPoint(cursorPoint);
const bounds = display.workArea;
return {
x: bounds.x,
y: bounds.y,
width: bounds.width,
height: bounds.height,
};
}
function getCurrentOverlayGeometry(): WindowGeometry {
if (lastOverlayWindowGeometry) return lastOverlayWindowGeometry;
const trackerGeometry = deps.getTrackedWindowGeometry();
if (trackerGeometry) return trackerGeometry;
return getOverlayGeometryFallback();
}
function getCurrentTrackedOverlayGeometry(): WindowGeometry | null {
return deps.getTrackedWindowGeometry();
}
function geometryMatches(a: WindowGeometry | null, b: WindowGeometry | null): boolean {
if (!a || !b) return false;
return a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height;
}
function applyOverlayRegions(geometry: WindowGeometry): void {
lastOverlayWindowGeometry = geometry;
maybeExitLinuxFullscreenOverrideForTrackedGeometry(geometry);
overlayManager.setOverlayWindowBounds(geometry);
overlayManager.setModalWindowBounds(geometry);
}
function shouldExitLinuxFullscreenOverrideForGeometry(geometry: WindowGeometry): boolean {
if (!shouldRunLinuxOverlayZOrderKeepAlive()) {
return false;
}
if (
deps.getLinuxTrackedMpvFullscreenChangedAtMs() > 0 &&
Date.now() - deps.getLinuxTrackedMpvFullscreenChangedAtMs() <
LINUX_VISIBLE_OVERLAY_FULLSCREEN_GEOMETRY_GRACE_MS
) {
return false;
}
const displayBounds = screen.getDisplayMatching(geometry).bounds;
return shouldExitFullscreenOverrideForTrackedGeometry({
currentMode: deps.getLinuxVisibleOverlayWindowMode(),
trackedFullscreen: deps.getLinuxTrackedMpvFullscreen(),
geometry,
displayBounds,
});
}
function maybeExitLinuxFullscreenOverrideForTrackedGeometry(geometry: WindowGeometry): void {
if (!shouldExitLinuxFullscreenOverrideForGeometry(geometry)) {
return;
}
deps.logDebug(
'Tracked mpv geometry no longer covers its display; exiting Linux fullscreen overlay override',
);
deps.syncLinuxVisibleOverlayMpvFullscreenMode(false);
}
function hasHyprlandOverlayWindowPlacementMismatch(geometry: WindowGeometry): boolean {
if (process.platform !== 'linux') {
return false;
}
return [overlayManager.getMainWindow(), overlayManager.getModalWindow()].some((window) => {
if (!window || window.isDestroyed()) {
return false;
}
return hasHyprlandWindowPlacementBoundsMismatch({
title: window.getTitle(),
bounds: normalizeOverlayWindowBoundsForPlatform(geometry, process.platform, screen, window),
});
});
}
const buildUpdateVisibleOverlayBoundsMainDepsHandler =
createBuildUpdateVisibleOverlayBoundsMainDepsHandler({
getCurrentOverlayWindowBounds: () => lastOverlayWindowGeometry,
shouldRefreshUnchangedGeometry: (geometry) =>
shouldExitLinuxFullscreenOverrideForGeometry(geometry) ||
(process.platform === 'linux' &&
(hasLiveOverlayWindowBoundsMismatch(
[overlayManager.getMainWindow(), overlayManager.getModalWindow()],
geometry,
) ||
hasHyprlandOverlayWindowPlacementMismatch(geometry))),
setOverlayWindowBounds: (geometry) => applyOverlayRegions(geometry),
afterSetOverlayWindowBounds: () => {
if (!overlayManager.getVisibleOverlayVisible()) {
return;
}
if (process.platform === 'win32') {
deps.scheduleWindowsVisibleOverlayZOrderSyncBurst();
return;
}
const mainWindow = overlayManager.getMainWindow();
if (!mainWindow || mainWindow.isDestroyed()) {
return;
}
if (process.platform === 'linux') {
restoreLinuxOverlayWindowShape(mainWindow);
}
ensureOverlayWindowLevel(mainWindow);
},
});
const updateVisibleOverlayBoundsMainDeps = buildUpdateVisibleOverlayBoundsMainDepsHandler();
const updateVisibleOverlayBounds = createUpdateVisibleOverlayBoundsHandler(
updateVisibleOverlayBoundsMainDeps,
);
const buildEnsureOverlayWindowLevelMainDepsHandler =
createBuildEnsureOverlayWindowLevelMainDepsHandler({
shouldSuppressOverlayWindowLevel: (window) => {
const mainWindow = overlayManager.getMainWindow();
return (
(deps.getStatsOverlayVisible() && window === mainWindow) ||
shouldSuppressVisibleOverlayRaiseForSeparateWindow({
window,
mainWindow,
separateWindows: deps.getOverlayForegroundSeparateWindows(),
})
);
},
ensureOverlayWindowLevelCore: (window) =>
ensureOverlayWindowLevelCore(window as BrowserWindow),
afterEnsureOverlayWindowLevel: () => {
const mainWindow = overlayManager.getMainWindow();
if (mainWindow && !mainWindow.isDestroyed()) {
moveVisibleOverlayAboveTrackedPlaybackWindow(mainWindow);
}
promoteStatsOverlayAbovePlayback();
},
});
const ensureOverlayWindowLevelMainDeps = buildEnsureOverlayWindowLevelMainDepsHandler();
const ensureOverlayWindowLevel = createEnsureOverlayWindowLevelHandler(
ensureOverlayWindowLevelMainDeps,
);
function syncPrimaryOverlayWindowLayer(layer: 'visible'): void {
const mainWindow = overlayManager.getMainWindow();
if (!mainWindow || mainWindow.isDestroyed()) return;
syncOverlayWindowLayer(mainWindow, layer);
}
function moveVisibleOverlayAboveTrackedPlaybackWindow(window: BrowserWindow): void {
if (process.platform !== 'linux') return;
if (window !== overlayManager.getMainWindow()) return;
bindVisibleOverlayToTrackedX11Window(window);
const mediaSourceId = deps.getTrackedWindowMediaSourceId();
if (!mediaSourceId) return;
try {
window.moveAbove(mediaSourceId);
} catch (error) {
deps.logDebug(
'Failed to move visible overlay above tracked playback window:',
error instanceof Error ? error.message : String(error),
);
}
}
function bindVisibleOverlayToTrackedX11Window(window: BrowserWindow): void {
const targetWindowId = deps.getTrackedWindowNativeId();
if (!targetWindowId) {
if (deps.getLinuxVisibleOverlayOwnerBindingKey() !== null) {
deps.clearVisibleOverlayX11OwnerBinding(window);
}
deps.setLinuxVisibleOverlayOwnerBindingKey(null);
return;
}
const overlayWindowId = deps.getNativeWindowHandleDecimal(window);
const bindingKey = `${overlayWindowId}:${targetWindowId}`;
if (deps.getLinuxVisibleOverlayOwnerBindingKey() === bindingKey) {
return;
}
deps.setLinuxVisibleOverlayOwnerBindingKey(bindingKey);
deps.enqueueVisibleOverlayX11OwnerBindingOperation(
window,
[
'-id',
overlayWindowId,
'-f',
'WM_TRANSIENT_FOR',
'32x',
'-set',
'WM_TRANSIENT_FOR',
targetWindowId,
],
(error) => {
if (deps.getLinuxVisibleOverlayOwnerBindingKey() === bindingKey) {
deps.setLinuxVisibleOverlayOwnerBindingKey(null);
}
deps.logDebug(
'Failed to bind visible overlay as transient for tracked X11 playback window:',
error instanceof Error ? error.message : String(error),
);
},
);
}
const buildEnforceOverlayLayerOrderMainDepsHandler =
createBuildEnforceOverlayLayerOrderMainDepsHandler({
enforceOverlayLayerOrderCore: (params) =>
enforceOverlayLayerOrderCore({
visibleOverlayVisible: params.visibleOverlayVisible,
mainWindow: params.mainWindow as BrowserWindow | null,
ensureOverlayWindowLevel: (window) =>
params.ensureOverlayWindowLevel(window as BrowserWindow),
}),
getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
getMainWindow: () => overlayManager.getMainWindow(),
ensureOverlayWindowLevel: (window) => ensureOverlayWindowLevel(window as BrowserWindow),
});
const enforceOverlayLayerOrderMainDeps = buildEnforceOverlayLayerOrderMainDepsHandler();
const enforceOverlayLayerOrder = createEnforceOverlayLayerOrderHandler(
enforceOverlayLayerOrderMainDeps,
);
return {
getLastOverlayWindowGeometry: () => lastOverlayWindowGeometry,
resetLastOverlayWindowGeometry: () => {
lastOverlayWindowGeometry = null;
},
getOverlayGeometryFallback,
getCurrentOverlayGeometry,
getCurrentTrackedOverlayGeometry,
geometryMatches,
applyOverlayRegions,
shouldExitLinuxFullscreenOverrideForGeometry,
maybeExitLinuxFullscreenOverrideForTrackedGeometry,
hasHyprlandOverlayWindowPlacementMismatch,
moveVisibleOverlayAboveTrackedPlaybackWindow,
bindVisibleOverlayToTrackedX11Window,
syncPrimaryOverlayWindowLayer,
updateVisibleOverlayBounds,
ensureOverlayWindowLevel,
enforceOverlayLayerOrder,
};
}
export type OverlayGeometryRuntime = ReturnType<typeof createOverlayGeometryRuntime>;