import { type BrowserWindow, screen } from 'electron'; import { execFile } from 'node:child_process'; import { startOverlayWindowTracker as startOverlayWindowTrackerCore } from '../../core/services'; import { isHeadlessInitialCommand, type CliArgs } from '../../cli/args'; import type { OverlayContentMeasurement, WindowGeometry } from '../../types'; import { createWindowTracker as createWindowTrackerCore } from '../../window-trackers'; import type { BaseWindowTracker } from '../../window-trackers'; import { bindWindowsOverlayAboveMpv, clearWindowsOverlayOwner, findWindowsMpvTargetWindowHandle, getWindowsForegroundProcessName, setWindowsOverlayOwner, } from '../../window-trackers/windows-helper'; import { applyLinuxOverlayInputShape, applyLinuxOverlayPointerInteractionMousePassthrough, ensureLinuxOverlayPointerInteractionLoop, type ForegroundSuppressionGraceState, mapOverlayMeasurementForPointerInteraction, resolveForegroundSuppressionWithGrace, shouldPrimeLinuxOverlayInteractionFromMeasurement, tickLinuxOverlayPointerInteraction, } from './linux-overlay-pointer-interaction'; import { restoreLinuxOverlayWindowShape } from './linux-overlay-window-shape'; import { ensureLinuxOverlayZOrderKeepAliveLoop, shouldRunLinuxOverlayZOrderKeepAlive, tickLinuxOverlayZOrderKeepAlive, } from './linux-overlay-zorder-keepalive'; import { createLinuxX11CursorPointReader } from './linux-x11-cursor-point'; import type { LinuxVisibleOverlayWindowMode } from './linux-visible-overlay-window-mode'; import { createStatsOverlayVisibilityChangeHandler } from './stats-overlay-visibility'; import { hasLiveSeparateWindow } from './settings-window-z-order'; import { tickWindowsOverlayPointerInteraction } from './windows-overlay-pointer-interaction'; export interface VisibleOverlayInteractionRuntimeDeps { overlayManager: { getMainWindow: () => BrowserWindow | null; getVisibleOverlayVisible: () => boolean; }; overlayContentMeasurementStore: { clear: (layer: 'visible') => void; getLatestByLayer: (layer: 'visible') => OverlayContentMeasurement | null; }; logger: { info: (message: string, ...args: unknown[]) => void; warn: (message: string, ...args: unknown[]) => void; debug: (message: string, ...args: unknown[]) => void; }; updateVisibleOverlayVisibility: () => void; getModalInputExclusive: () => boolean; getStatsOverlayVisible: () => boolean; setStatsOverlayVisible: (visible: boolean) => void; getWindowTracker: () => BaseWindowTracker | null; setWindowTracker: (tracker: BaseWindowTracker | null) => void; setTrackerNotReadyWarningShown: (shown: boolean) => void; getMpvSocketPath: () => string; getBackendOverride: () => string | null; getInitialArgs: () => CliArgs | null; getOverlayRuntimeInitialized: () => boolean; getLinuxVisibleOverlayWindowMode: () => LinuxVisibleOverlayWindowMode; setLinuxVisibleOverlayOwnerBindingKey: (key: string | null) => void; bindVisibleOverlayToTrackedX11Window: (window: BrowserWindow) => void; updateVisibleOverlayBounds: (geometry: WindowGeometry) => void; refreshCurrentSubtitle: () => void; getOverlayWindows: () => BrowserWindow[]; syncOverlayShortcuts: () => void; resetLastOverlayWindowGeometry: () => void; enforceOverlayLayerOrder: () => void; getOverlayForegroundSeparateWindows: () => BrowserWindow[]; } export function createVisibleOverlayInteractionRuntime(deps: VisibleOverlayInteractionRuntimeDeps) { const { overlayManager, overlayContentMeasurementStore, logger } = deps; const VISIBLE_OVERLAY_BLUR_REFRESH_DELAYS_MS = [0, 25, 100, 250] as const; const WINDOWS_VISIBLE_OVERLAY_Z_ORDER_RETRY_DELAYS_MS = [0, 48, 120, 240, 480] as const; const WINDOWS_VISIBLE_OVERLAY_FOREGROUND_POLL_INTERVAL_MS = 75; const WINDOWS_VISIBLE_OVERLAY_FOCUS_HANDOFF_GRACE_MS = 200; const LINUX_VISIBLE_OVERLAY_FOCUS_HANDOFF_GRACE_MS = 1_500; // 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 // X11 active window, which would otherwise leave subtitles inert for a poll cycle (~1s). const LINUX_POINTER_FOREGROUND_SUPPRESS_GRACE_MS = 500; const LINUX_VISIBLE_OVERLAY_STARTUP_INPUT_GRACE_MS = 1_500; const MACOS_VISIBLE_OVERLAY_FOREGROUND_PROBE_TIMEOUT_MS = 1_200; let visibleOverlayBlurRefreshTimeouts: Array> = []; let windowsVisibleOverlayZOrderRetryTimeouts: Array> = []; let windowsVisibleOverlayZOrderSyncInFlight = false; let windowsVisibleOverlayZOrderSyncQueued = false; let windowsVisibleOverlayForegroundPollInterval: ReturnType | null = null; let windowsOverlayPointerInteractionActive = false; let lastWindowsVisibleOverlayForegroundProcessName: string | null = null; let lastWindowsVisibleOverlayBlurredAtMs = 0; let lastLinuxVisibleOverlayFollowedMpvAtMs = 0; const linuxPointerForegroundSuppressionGrace: ForegroundSuppressionGraceState = { lossSinceMs: null, }; let visibleOverlayInteractionActive = false; let linuxOverlayInputShapeActive = false; let linuxOverlayPointerInteractionStateApplied = process.platform !== 'linux'; let linuxVisibleOverlayStartupInputPrimed = false; let linuxVisibleOverlayStartupInputGraceUntilMs = 0; // Renderer-reported interactive hint (Linux only): true while a Yomitan popup/modal // region is interactive, so the cursor poll keeps the overlay interactive even when the cursor // moves off measured subtitle/sidebar rects onto the popup. let linuxOverlayInteractiveHint = false; let macOSVisibleOverlayForegroundProbeActive = false; let macOSVisibleOverlayForegroundProbeToken = 0; let macOSVisibleOverlayForegroundProbeTimeout: ReturnType | null = null; const linuxVisibleOverlayOwnerBindingQueues = new WeakMap>(); const handleStatsOverlayVisibilityChanged = createStatsOverlayVisibilityChangeHandler({ setStatsOverlayVisibleState: (visible) => { deps.setStatsOverlayVisible(visible); }, resetVisibleOverlayInteraction: () => { visibleOverlayInteractionActive = false; }, getMainWindow: () => overlayManager.getMainWindow(), updateVisibleOverlayVisibility: () => deps.updateVisibleOverlayVisibility(), }); function resetVisibleOverlayInputState(): void { visibleOverlayInteractionActive = false; windowsOverlayPointerInteractionActive = false; linuxOverlayInputShapeActive = false; linuxOverlayPointerInteractionStateApplied = false; resetLinuxVisibleOverlayStartupInputPrimer(); linuxOverlayInteractiveHint = false; overlayContentMeasurementStore.clear('visible'); const mainWindow = overlayManager.getMainWindow(); if (process.platform === 'linux' && mainWindow && !mainWindow.isDestroyed()) { restoreLinuxOverlayWindowShape(mainWindow); } } function restoreVisibleOverlayWindowShapeForShow(): void { if (process.platform !== 'linux') { return; } restoreLinuxOverlayWindowShape(overlayManager.getMainWindow()); } function clearVisibleOverlayBlurRefreshTimeouts(): void { for (const timeout of visibleOverlayBlurRefreshTimeouts) { clearTimeout(timeout); } visibleOverlayBlurRefreshTimeouts = []; } function clearWindowsVisibleOverlayZOrderRetryTimeouts(): void { for (const timeout of windowsVisibleOverlayZOrderRetryTimeouts) { clearTimeout(timeout); } windowsVisibleOverlayZOrderRetryTimeouts = []; } function finishMacOSVisibleOverlayForegroundProbe(token: number): void { if (token !== macOSVisibleOverlayForegroundProbeToken) { return; } if (macOSVisibleOverlayForegroundProbeTimeout !== null) { clearTimeout(macOSVisibleOverlayForegroundProbeTimeout); macOSVisibleOverlayForegroundProbeTimeout = null; } if (!macOSVisibleOverlayForegroundProbeActive) { return; } macOSVisibleOverlayForegroundProbeActive = false; deps.updateVisibleOverlayVisibility(); } function startMacOSVisibleOverlayForegroundProbe(): void { if (process.platform !== 'darwin') { return; } const tracker = deps.getWindowTracker(); if (!tracker) { return; } macOSVisibleOverlayForegroundProbeActive = true; const token = ++macOSVisibleOverlayForegroundProbeToken; if (macOSVisibleOverlayForegroundProbeTimeout !== null) { clearTimeout(macOSVisibleOverlayForegroundProbeTimeout); } macOSVisibleOverlayForegroundProbeTimeout = setTimeout(() => { finishMacOSVisibleOverlayForegroundProbe(token); }, MACOS_VISIBLE_OVERLAY_FOREGROUND_PROBE_TIMEOUT_MS); void tracker .refreshNow() .catch((error) => { logger.warn('Failed to refresh macOS frontmost app after overlay blur', error); }) .finally(() => { finishMacOSVisibleOverlayForegroundProbe(token); }); } function getNativeWindowHandleDecimal(window: BrowserWindow): string { const handle = window.getNativeWindowHandle(); return handle.length >= 8 ? handle.readBigUInt64LE(0).toString() : BigInt(handle.readUInt32LE(0)).toString(); } function getWindowsNativeWindowHandle(window: BrowserWindow): string { return getNativeWindowHandleDecimal(window); } function getWindowsNativeWindowHandleNumber(window: BrowserWindow): number { const handle = window.getNativeWindowHandle(); return handle.length >= 8 ? Number(handle.readBigUInt64LE(0)) : handle.readUInt32LE(0); } function enqueueVisibleOverlayX11OwnerBindingOperation( window: BrowserWindow, args: string[], onError?: (error: Error) => void, ): void { const previous = linuxVisibleOverlayOwnerBindingQueues.get(window) ?? Promise.resolve(); const operation = previous .catch(() => {}) .then( () => new Promise((resolve) => { if (window.isDestroyed()) { resolve(); return; } execFile('xprop', args, { timeout: 1500 }, (error) => { if (error) { onError?.(error); } resolve(); }); }), ); const queued = operation.finally(() => { if (linuxVisibleOverlayOwnerBindingQueues.get(window) === queued) { linuxVisibleOverlayOwnerBindingQueues.delete(window); } }); linuxVisibleOverlayOwnerBindingQueues.set(window, queued); } function clearVisibleOverlayX11OwnerBinding(window: BrowserWindow): void { if (window.isDestroyed()) return; enqueueVisibleOverlayX11OwnerBindingOperation(window, [ '-id', getNativeWindowHandleDecimal(window), '-remove', 'WM_TRANSIENT_FOR', ]); } function resolveWindowsOverlayBindTargetHandle( targetMpvSocketPath?: string | null, ): number | null { if (process.platform !== 'win32') { return null; } try { if (targetMpvSocketPath) { const windowTracker = deps.getWindowTracker() as { getTargetWindowHandle?: () => number | null; } | null; const trackedHandle = windowTracker?.getTargetWindowHandle?.(); if (typeof trackedHandle === 'number' && Number.isFinite(trackedHandle)) { return trackedHandle; } return null; } return findWindowsMpvTargetWindowHandle(); } catch { return null; } } function createOverlayWindowTracker( override?: string | null, targetMpvSocketPath?: string | null, ) { const initialArgs = deps.getInitialArgs(); if (initialArgs && isHeadlessInitialCommand(initialArgs)) { return null; } return createWindowTrackerCore(override, targetMpvSocketPath); } function bindVisibleOverlayOwner(): void { const mainWindow = overlayManager.getMainWindow(); if (!mainWindow || mainWindow.isDestroyed()) return; if (process.platform === 'linux') { deps.bindVisibleOverlayToTrackedX11Window(mainWindow); return; } if (process.platform !== 'win32') return; const overlayHwnd = getWindowsNativeWindowHandleNumber(mainWindow); const targetSocketPath = deps.getMpvSocketPath(); const targetWindowHwnd = resolveWindowsOverlayBindTargetHandle(targetSocketPath); if (targetWindowHwnd !== null && bindWindowsOverlayAboveMpv(overlayHwnd, targetWindowHwnd)) { return; } if (targetSocketPath) { return; } const tracker = deps.getWindowTracker(); const mpvResult = tracker ? (() => { try { const win32 = require('../../window-trackers/win32') as typeof import('../../window-trackers/win32'); const poll = win32.findMpvWindows(); const focused = poll.matches.find((m) => m.isForeground); return focused ?? [...poll.matches].sort((a, b) => b.area - a.area)[0] ?? null; } catch { return null; } })() : null; if (!mpvResult) return; if (!setWindowsOverlayOwner(overlayHwnd, mpvResult.hwnd)) { logger.warn('Failed to set overlay owner via koffi'); } } function releaseVisibleOverlayOwner(): void { const mainWindow = overlayManager.getMainWindow(); if (process.platform === 'linux') { deps.setLinuxVisibleOverlayOwnerBindingKey(null); if (mainWindow && !mainWindow.isDestroyed()) { clearVisibleOverlayX11OwnerBinding(mainWindow); } return; } if (process.platform !== 'win32' || !mainWindow || mainWindow.isDestroyed()) return; const overlayHwnd = getWindowsNativeWindowHandleNumber(mainWindow); if (!clearWindowsOverlayOwner(overlayHwnd)) { logger.warn('Failed to clear overlay owner via koffi'); } } function startOverlayWindowTrackerForCurrentSocket(): void { startOverlayWindowTrackerCore({ backendOverride: deps.getBackendOverride(), getMpvSocketPath: () => deps.getMpvSocketPath(), createWindowTracker: createOverlayWindowTracker, setWindowTracker: (tracker) => { deps.setWindowTracker(tracker); }, updateVisibleOverlayBounds: (geometry: WindowGeometry) => deps.updateVisibleOverlayBounds(geometry), isVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(), updateVisibleOverlayVisibility: () => deps.updateVisibleOverlayVisibility(), refreshCurrentSubtitle: () => { deps.refreshCurrentSubtitle(); }, getOverlayWindows: () => deps.getOverlayWindows(), syncOverlayShortcuts: () => deps.syncOverlayShortcuts(), bindOverlayOwner: () => bindVisibleOverlayOwner(), releaseOverlayOwner: () => releaseVisibleOverlayOwner(), }); } function retargetOverlayWindowTrackerForMpvSocket( nextSocketPath: string, previousSocketPath: string, ): void { if (nextSocketPath === previousSocketPath || !deps.getOverlayRuntimeInitialized()) { return; } const previousTracker = deps.getWindowTracker(); if (previousTracker) { try { previousTracker.stop(); } catch (error) { logger.warn('Failed to stop previous overlay window tracker before retargeting', error); } } releaseVisibleOverlayOwner(); deps.setWindowTracker(null); deps.setTrackerNotReadyWarningShown(false); deps.resetLastOverlayWindowGeometry(); startOverlayWindowTrackerForCurrentSocket(); deps.updateVisibleOverlayVisibility(); deps.syncOverlayShortcuts(); logger.info( `Retargeted overlay window tracker for MPV socket: ${previousSocketPath} -> ${nextSocketPath}`, ); } async function syncWindowsVisibleOverlayToMpvZOrder(): Promise { if (process.platform !== 'win32') { return false; } const mainWindow = overlayManager.getMainWindow(); if ( !mainWindow || mainWindow.isDestroyed() || !mainWindow.isVisible() || !overlayManager.getVisibleOverlayVisible() ) { return false; } const windowTracker = deps.getWindowTracker(); if (!windowTracker) { return false; } if ( typeof windowTracker.isTargetWindowMinimized === 'function' && windowTracker.isTargetWindowMinimized() ) { return false; } if (!windowTracker.isTracking() && windowTracker.getGeometry() === null) { return false; } const overlayHwnd = getWindowsNativeWindowHandleNumber(mainWindow); const targetWindowHwnd = resolveWindowsOverlayBindTargetHandle(deps.getMpvSocketPath()); if (targetWindowHwnd !== null && bindWindowsOverlayAboveMpv(overlayHwnd, targetWindowHwnd)) { (mainWindow as BrowserWindow & { setOpacity?: (opacity: number) => void }).setOpacity?.(1); return true; } return false; } function requestWindowsVisibleOverlayZOrderSync(): void { if (process.platform !== 'win32') { return; } if (windowsVisibleOverlayZOrderSyncInFlight) { windowsVisibleOverlayZOrderSyncQueued = true; return; } windowsVisibleOverlayZOrderSyncInFlight = true; void syncWindowsVisibleOverlayToMpvZOrder() .catch((error) => { logger.warn('Failed to bind Windows overlay z-order to mpv', error); }) .finally(() => { windowsVisibleOverlayZOrderSyncInFlight = false; if (!windowsVisibleOverlayZOrderSyncQueued) { return; } windowsVisibleOverlayZOrderSyncQueued = false; requestWindowsVisibleOverlayZOrderSync(); }); } function scheduleWindowsVisibleOverlayZOrderSyncBurst(): void { if (process.platform !== 'win32') { return; } clearWindowsVisibleOverlayZOrderRetryTimeouts(); for (const delayMs of WINDOWS_VISIBLE_OVERLAY_Z_ORDER_RETRY_DELAYS_MS) { const retryTimeout = setTimeout(() => { windowsVisibleOverlayZOrderRetryTimeouts = windowsVisibleOverlayZOrderRetryTimeouts.filter( (timeout) => timeout !== retryTimeout, ); requestWindowsVisibleOverlayZOrderSync(); }, delayMs); windowsVisibleOverlayZOrderRetryTimeouts.push(retryTimeout); } } function hasWindowsVisibleOverlayFocusHandoffGrace(): boolean { return ( process.platform === 'win32' && lastWindowsVisibleOverlayBlurredAtMs > 0 && Date.now() - lastWindowsVisibleOverlayBlurredAtMs <= WINDOWS_VISIBLE_OVERLAY_FOCUS_HANDOFF_GRACE_MS ); } function shouldPollWindowsVisibleOverlayForegroundProcess(): boolean { if (process.platform !== 'win32' || !overlayManager.getVisibleOverlayVisible()) { return false; } const mainWindow = overlayManager.getMainWindow(); if (!mainWindow || mainWindow.isDestroyed()) { return false; } const windowTracker = deps.getWindowTracker(); if (!windowTracker) { return false; } if ( typeof windowTracker.isTargetWindowMinimized === 'function' && windowTracker.isTargetWindowMinimized() ) { return false; } const overlayFocused = mainWindow.isFocused(); const trackerFocused = windowTracker.isTargetWindowFocused?.() ?? false; return !overlayFocused && !trackerFocused; } function maybePollWindowsVisibleOverlayForegroundProcess(): void { if (!shouldPollWindowsVisibleOverlayForegroundProcess()) { lastWindowsVisibleOverlayForegroundProcessName = null; return; } const processName = getWindowsForegroundProcessName(); const normalizedProcessName = processName?.trim().toLowerCase() ?? null; const previousProcessName = lastWindowsVisibleOverlayForegroundProcessName; lastWindowsVisibleOverlayForegroundProcessName = normalizedProcessName; if (normalizedProcessName !== previousProcessName) { deps.updateVisibleOverlayVisibility(); } if (normalizedProcessName === 'mpv' && previousProcessName !== 'mpv') { requestWindowsVisibleOverlayZOrderSync(); } } function ensureWindowsVisibleOverlayForegroundPollLoop(): void { if (process.platform !== 'win32' || windowsVisibleOverlayForegroundPollInterval !== null) { return; } windowsVisibleOverlayForegroundPollInterval = setInterval(() => { maybePollWindowsVisibleOverlayForegroundProcess(); tickWindowsOverlayPointerInteractionNow(); }, WINDOWS_VISIBLE_OVERLAY_FOREGROUND_POLL_INTERVAL_MS); } function clearWindowsVisibleOverlayForegroundPollLoop(): void { if (windowsVisibleOverlayForegroundPollInterval === null) { return; } clearInterval(windowsVisibleOverlayForegroundPollInterval); windowsVisibleOverlayForegroundPollInterval = null; } function scheduleVisibleOverlayBlurRefresh(): void { if (process.platform !== 'win32' && process.platform !== 'darwin') { return; } if (process.platform === 'win32') { lastWindowsVisibleOverlayBlurredAtMs = Date.now(); } startMacOSVisibleOverlayForegroundProbe(); clearVisibleOverlayBlurRefreshTimeouts(); for (const delayMs of VISIBLE_OVERLAY_BLUR_REFRESH_DELAYS_MS) { const refreshTimeout = setTimeout(() => { visibleOverlayBlurRefreshTimeouts = visibleOverlayBlurRefreshTimeouts.filter( (timeout) => timeout !== refreshTimeout, ); deps.updateVisibleOverlayVisibility(); }, delayMs); visibleOverlayBlurRefreshTimeouts.push(refreshTimeout); } } function shouldSuspendWindowsOverlayPointerInteraction(): boolean { return ( deps.getModalInputExclusive() || deps.getStatsOverlayVisible() || hasLiveSeparateWindow(deps.getOverlayForegroundSeparateWindows()) ); } function updateWindowsOverlayPointerInteractionActive(active: boolean): void { windowsOverlayPointerInteractionActive = active; visibleOverlayInteractionActive = active; const mainWindow = overlayManager.getMainWindow(); if ( process.platform !== 'win32' || !mainWindow || mainWindow.isDestroyed() || !mainWindow.isVisible() ) { deps.updateVisibleOverlayVisibility(); return; } if (active) { mainWindow.setIgnoreMouseEvents(false); } else { mainWindow.setIgnoreMouseEvents(true, { forward: true }); } } const windowsOverlayPointerInteractionDeps = { getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(), getMainWindow: () => overlayManager.getMainWindow(), getCursorScreenPoint: () => screen.getCursorScreenPoint(), getSubtitleMeasurement: () => overlayContentMeasurementStore.getLatestByLayer('visible'), shouldSuspend: shouldSuspendWindowsOverlayPointerInteraction, getInteractionActive: () => windowsOverlayPointerInteractionActive, setInteractionActive: updateWindowsOverlayPointerInteractionActive, }; function tickWindowsOverlayPointerInteractionNow(): void { if (process.platform !== 'win32') { return; } if (!windowsOverlayPointerInteractionActive && visibleOverlayInteractionActive) { return; } tickWindowsOverlayPointerInteraction(windowsOverlayPointerInteractionDeps); } ensureWindowsVisibleOverlayForegroundPollLoop(); const linuxX11CursorPointReader = createLinuxX11CursorPointReader(); function getLinuxOverlayPointerMeasurement() { const measurement = overlayContentMeasurementStore.getLatestByLayer('visible'); return mapOverlayMeasurementForPointerInteraction(measurement); } function shouldSuspendLinuxOverlayPointerInteraction(): boolean { return deps.getModalInputExclusive() || deps.getStatsOverlayVisible(); } function shouldSuppressLinuxOverlayPointerInteraction(): boolean { return resolveForegroundSuppressionWithGrace({ hasForegroundSeparateWindow: hasLiveSeparateWindow( deps.getOverlayForegroundSeparateWindows(), ), isTrackingMpvWindow: Boolean(deps.getWindowTracker()?.isTracking()), isMpvWindowFocused: deps.getWindowTracker()?.isTargetWindowFocused?.() !== false, isOverlayWindowFocused: overlayManager.getMainWindow()?.isFocused() === true, nowMs: Date.now(), graceMs: LINUX_POINTER_FOREGROUND_SUPPRESS_GRACE_MS, state: linuxPointerForegroundSuppressionGrace, }); } function shouldUseLinuxOverlayInputShape(): boolean { // Electron's setShape is a *bounding* shape: outside the given rects no pixels are drawn, so // it clips the visible subtitle (and makes a dragged subtitle vanish behind the shaped // region). There is no input-only region API on Linux, so selective hit-testing is handled by // the main-process cursor poll instead. Keep this off to avoid clipping the overlay. return false; } function hasLinuxVisibleOverlayStartupInputGrace(): boolean { return ( process.platform === 'linux' && linuxVisibleOverlayStartupInputGraceUntilMs > 0 && Date.now() < linuxVisibleOverlayStartupInputGraceUntilMs ); } function clearLinuxVisibleOverlayStartupInputGrace(): void { linuxVisibleOverlayStartupInputGraceUntilMs = 0; } function startLinuxVisibleOverlayStartupInputGrace(): void { if (process.platform !== 'linux') { return; } linuxVisibleOverlayStartupInputGraceUntilMs = Date.now() + LINUX_VISIBLE_OVERLAY_STARTUP_INPUT_GRACE_MS; linuxOverlayPointerInteractionStateApplied = false; } function resetLinuxVisibleOverlayStartupInputPrimer(): void { linuxVisibleOverlayStartupInputPrimed = false; clearLinuxVisibleOverlayStartupInputGrace(); if (process.platform === 'linux') { visibleOverlayInteractionActive = false; linuxOverlayInteractiveHint = false; linuxOverlayPointerInteractionStateApplied = false; } } function applyLinuxOverlayInputShapeFromLatestMeasurement(): boolean { if (!shouldUseLinuxOverlayInputShape()) { linuxOverlayInputShapeActive = false; return false; } const result = applyLinuxOverlayInputShape({ getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(), getMainWindow: () => overlayManager.getMainWindow(), getSubtitleMeasurement: getLinuxOverlayPointerMeasurement, getRendererInteractiveHint: () => linuxOverlayInteractiveHint, shouldSuspend: shouldSuspendLinuxOverlayPointerInteraction, shouldSuppressInteraction: shouldSuppressLinuxOverlayPointerInteraction, }); linuxOverlayInputShapeActive = result.active; linuxOverlayPointerInteractionStateApplied = result.handled; return result.handled; } function updateLinuxOverlayPointerInteractionActive(active: boolean): void { visibleOverlayInteractionActive = active; if ( process.platform === 'linux' && applyLinuxOverlayPointerInteractionMousePassthrough({ active, getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(), getMainWindow: () => overlayManager.getMainWindow(), shouldSuspend: shouldSuspendLinuxOverlayPointerInteraction, shouldSuppressInteraction: shouldSuppressLinuxOverlayPointerInteraction, updateVisibleOverlayVisibility: () => deps.updateVisibleOverlayVisibility(), }) ) { linuxOverlayPointerInteractionStateApplied = true; return; } linuxOverlayPointerInteractionStateApplied = true; deps.updateVisibleOverlayVisibility(); } function primeLinuxOverlayPointerInteractionAfterFirstMeasurement(): void { if (process.platform !== 'linux') return; if (linuxVisibleOverlayStartupInputPrimed) return; if (shouldUseLinuxOverlayInputShape()) return; if ( !shouldPrimeLinuxOverlayInteractionFromMeasurement({ getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(), getMainWindow: () => overlayManager.getMainWindow(), getSubtitleMeasurement: getLinuxOverlayPointerMeasurement, shouldSuspend: shouldSuspendLinuxOverlayPointerInteraction, shouldSuppressInteraction: shouldSuppressLinuxOverlayPointerInteraction, }) ) { return; } linuxVisibleOverlayStartupInputPrimed = true; linuxVisibleOverlayStartupInputGraceUntilMs = Date.now() + LINUX_VISIBLE_OVERLAY_STARTUP_INPUT_GRACE_MS; updateLinuxOverlayPointerInteractionActive(true); } const linuxOverlayZOrderKeepAliveDeps = { getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(), getMainWindow: () => overlayManager.getMainWindow(), isTrackingMpvWindow: () => Boolean(deps.getWindowTracker()?.isTracking()), isMpvWindowFocused: () => deps.getWindowTracker()?.isTargetWindowFocused?.() !== false, isOverlayWindowFocused: () => overlayManager.getMainWindow()?.isFocused() === true, shouldSuppressReassert: () => deps.getModalInputExclusive() || deps.getStatsOverlayVisible() || hasLiveSeparateWindow(deps.getOverlayForegroundSeparateWindows()) || (visibleOverlayInteractionActive && overlayManager.getMainWindow()?.isFocused() !== true), raiseMpvWindow: () => { if ( lastLinuxVisibleOverlayFollowedMpvAtMs > 0 && Date.now() - lastLinuxVisibleOverlayFollowedMpvAtMs <= LINUX_VISIBLE_OVERLAY_FOCUS_HANDOFF_GRACE_MS ) { return Promise.resolve(false); } lastLinuxVisibleOverlayFollowedMpvAtMs = Date.now(); return deps.getWindowTracker()?.raiseTargetWindow?.() ?? Promise.resolve(false); }, releaseOverlayLayerOrder: () => { const mainWindow = overlayManager.getMainWindow(); if (!mainWindow || mainWindow.isDestroyed()) return; mainWindow.setAlwaysOnTop(false); mainWindow.setFullScreen?.(false); mainWindow.setVisibleOnAllWorkspaces?.(false, { visibleOnFullScreen: false }); if ( deps.getLinuxVisibleOverlayWindowMode() === 'fullscreen-override' && mainWindow.isVisible() ) { mainWindow.hide(); } }, enforceOverlayLayerOrder: () => { deps.enforceOverlayLayerOrder(); }, focusOverlayWindow: () => { const mainWindow = overlayManager.getMainWindow(); if (!mainWindow || mainWindow.isDestroyed() || mainWindow.isFocused()) return; mainWindow.focus(); }, }; function requestLinuxOverlayZOrderFollow(): void { if (!shouldRunLinuxOverlayZOrderKeepAlive()) return; void tickLinuxOverlayZOrderKeepAlive(linuxOverlayZOrderKeepAliveDeps).catch((error) => { logger.debug( 'Failed to follow tracked mpv behind focused overlay:', error instanceof Error ? error.message : String(error), ); }); } ensureLinuxOverlayZOrderKeepAliveLoop(linuxOverlayZOrderKeepAliveDeps); const linuxOverlayPointerInteractionDeps = { getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(), getMainWindow: () => overlayManager.getMainWindow(), getCursorScreenPoint: () => linuxX11CursorPointReader.getCursorScreenPoint(screen.getCursorScreenPoint()), getSubtitleMeasurement: getLinuxOverlayPointerMeasurement, getRendererInteractiveHint: () => linuxOverlayInteractiveHint || hasLinuxVisibleOverlayStartupInputGrace(), shouldSuspend: shouldSuspendLinuxOverlayPointerInteraction, shouldSuppressInteraction: shouldSuppressLinuxOverlayPointerInteraction, shouldUseInputShape: shouldUseLinuxOverlayInputShape, getInteractionActive: () => visibleOverlayInteractionActive, isInteractionStateApplied: () => linuxOverlayPointerInteractionStateApplied, setInteractionActive: updateLinuxOverlayPointerInteractionActive, }; function tickLinuxOverlayPointerInteractionNow(): void { if (applyLinuxOverlayInputShapeFromLatestMeasurement()) { return; } tickLinuxOverlayPointerInteraction(linuxOverlayPointerInteractionDeps); } ensureLinuxOverlayPointerInteractionLoop(linuxOverlayPointerInteractionDeps); return { handleStatsOverlayVisibilityChanged, resetVisibleOverlayInputState, restoreVisibleOverlayWindowShapeForShow, startMacOSVisibleOverlayForegroundProbe, getNativeWindowHandleDecimal, getWindowsNativeWindowHandle, getWindowsNativeWindowHandleNumber, enqueueVisibleOverlayX11OwnerBindingOperation, clearVisibleOverlayX11OwnerBinding, createOverlayWindowTracker, bindVisibleOverlayOwner, releaseVisibleOverlayOwner, startOverlayWindowTrackerForCurrentSocket, retargetOverlayWindowTrackerForMpvSocket, requestWindowsVisibleOverlayZOrderSync, scheduleWindowsVisibleOverlayZOrderSyncBurst, hasWindowsVisibleOverlayFocusHandoffGrace, ensureWindowsVisibleOverlayForegroundPollLoop, clearWindowsVisibleOverlayForegroundPollLoop, scheduleVisibleOverlayBlurRefresh, getLinuxOverlayPointerMeasurement, hasLinuxVisibleOverlayStartupInputGrace, clearLinuxVisibleOverlayStartupInputGrace, startLinuxVisibleOverlayStartupInputGrace, resetLinuxVisibleOverlayStartupInputPrimer, applyLinuxOverlayInputShapeFromLatestMeasurement, updateLinuxOverlayPointerInteractionActive, primeLinuxOverlayPointerInteractionAfterFirstMeasurement, requestLinuxOverlayZOrderFollow, tickWindowsOverlayPointerInteractionNow, tickLinuxOverlayPointerInteractionNow, getVisibleOverlayInteractionActive: () => visibleOverlayInteractionActive, setVisibleOverlayInteractionActive: (active: boolean) => { visibleOverlayInteractionActive = active; windowsOverlayPointerInteractionActive = false; }, getLinuxOverlayInputShapeActive: () => linuxOverlayInputShapeActive, getLastWindowsVisibleOverlayForegroundProcessName: () => lastWindowsVisibleOverlayForegroundProcessName, getMacOSVisibleOverlayForegroundProbeActive: () => macOSVisibleOverlayForegroundProbeActive, setLinuxOverlayInteractiveHint: (interactive: boolean) => { linuxOverlayInteractiveHint = interactive; }, }; } export type VisibleOverlayInteractionRuntime = ReturnType< typeof createVisibleOverlayInteractionRuntime >;