mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-06-12 03:13:39 -07:00
refactor(main): extract overlay geometry runtime from main.ts
This commit is contained in:
+52
-231
@@ -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();
|
||||||
|
|||||||
@@ -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>;
|
||||||
Reference in New Issue
Block a user