mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-05-27 00:55:16 -07:00
upgrade Electron 39→42 and fix Hyprland overlay z-order/placement (#79)
This commit is contained in:
@@ -60,7 +60,10 @@ test('buildHyprlandPlacementDispatches floats tiled overlay windows without pinn
|
||||
floating: false,
|
||||
pinned: false,
|
||||
}),
|
||||
[['dispatch', 'setfloating', 'address:0xabc']],
|
||||
[
|
||||
['dispatch', 'setfloating', 'address:0xabc'],
|
||||
['dispatch', 'alterzorder', 'top,address:0xabc'],
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
@@ -87,6 +90,7 @@ test('buildHyprlandPlacementDispatches force-aligns floating overlay windows to
|
||||
['dispatch', 'setprop', 'address:0xabc no_shadow 1'],
|
||||
['dispatch', 'setprop', 'address:0xabc no_blur 1'],
|
||||
['dispatch', 'setprop', 'address:0xabc decorate 0'],
|
||||
['dispatch', 'alterzorder', 'top,address:0xabc'],
|
||||
],
|
||||
);
|
||||
});
|
||||
@@ -98,7 +102,7 @@ test('buildHyprlandPlacementDispatches does not pin already floating overlay win
|
||||
floating: true,
|
||||
pinned: false,
|
||||
}),
|
||||
[],
|
||||
[['dispatch', 'alterzorder', 'top,address:0xabc']],
|
||||
);
|
||||
});
|
||||
|
||||
@@ -109,7 +113,10 @@ test('buildHyprlandPlacementDispatches unpins previously pinned overlay windows'
|
||||
floating: true,
|
||||
pinned: true,
|
||||
}),
|
||||
[['dispatch', 'pin', 'address:0xabc']],
|
||||
[
|
||||
['dispatch', 'pin', 'address:0xabc'],
|
||||
['dispatch', 'alterzorder', 'top,address:0xabc'],
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
@@ -146,6 +153,7 @@ test('ensureHyprlandWindowFloatingByTitle dispatches float-only placement for ma
|
||||
[
|
||||
['-j', 'clients'],
|
||||
['dispatch', 'setfloating', 'address:0xmatch'],
|
||||
['dispatch', 'alterzorder', 'top,address:0xmatch'],
|
||||
],
|
||||
);
|
||||
});
|
||||
@@ -195,6 +203,7 @@ test('ensureHyprlandWindowFloatingByTitle dispatches exact Hyprland geometry whe
|
||||
['dispatch', 'setprop', 'address:0xmatch no_shadow 1'],
|
||||
['dispatch', 'setprop', 'address:0xmatch no_blur 1'],
|
||||
['dispatch', 'setprop', 'address:0xmatch decorate 0'],
|
||||
['dispatch', 'alterzorder', 'top,address:0xmatch'],
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
@@ -95,6 +95,7 @@ export function buildHyprlandPlacementDispatches(
|
||||
dispatches.push(['dispatch', 'setprop', `${windowAddress} no_blur 1`]);
|
||||
dispatches.push(['dispatch', 'setprop', `${windowAddress} decorate 0`]);
|
||||
}
|
||||
dispatches.push(['dispatch', 'alterzorder', `top,${windowAddress}`]);
|
||||
return dispatches;
|
||||
}
|
||||
|
||||
|
||||
@@ -197,6 +197,53 @@ test('tracked non-macOS overlay stays hidden while tracker is not ready', () =>
|
||||
assert.ok(!calls.includes('osd'));
|
||||
});
|
||||
|
||||
test('suspended visible overlay hides without refreshing bounds or z-order', () => {
|
||||
const { window, calls } = createMainWindowRecorder();
|
||||
const tracker: WindowTrackerStub = {
|
||||
isTracking: () => true,
|
||||
getGeometry: () => ({ x: 0, y: 0, width: 1280, height: 720 }),
|
||||
};
|
||||
|
||||
window.show();
|
||||
calls.length = 0;
|
||||
|
||||
updateVisibleOverlayVisibility({
|
||||
visibleOverlayVisible: true,
|
||||
suspendVisibleOverlay: true,
|
||||
mainWindow: window as never,
|
||||
windowTracker: tracker as never,
|
||||
trackerNotReadyWarningShown: false,
|
||||
setTrackerNotReadyWarningShown: () => {},
|
||||
updateVisibleOverlayBounds: () => {
|
||||
calls.push('update-bounds');
|
||||
},
|
||||
ensureOverlayWindowLevel: () => {
|
||||
calls.push('ensure-level');
|
||||
},
|
||||
syncPrimaryOverlayWindowLayer: () => {
|
||||
calls.push('sync-layer');
|
||||
},
|
||||
enforceOverlayLayerOrder: () => {
|
||||
calls.push('enforce-order');
|
||||
},
|
||||
syncOverlayShortcuts: () => {
|
||||
calls.push('sync-shortcuts');
|
||||
},
|
||||
isMacOSPlatform: false,
|
||||
isWindowsPlatform: false,
|
||||
} as never);
|
||||
|
||||
assert.ok(calls.includes('mouse-ignore:true:forward'));
|
||||
assert.ok(calls.includes('always-on-top:false'));
|
||||
assert.ok(calls.includes('hide'));
|
||||
assert.ok(calls.includes('sync-shortcuts'));
|
||||
assert.ok(!calls.includes('update-bounds'));
|
||||
assert.ok(!calls.includes('ensure-level'));
|
||||
assert.ok(!calls.includes('enforce-order'));
|
||||
assert.ok(!calls.includes('show'));
|
||||
assert.ok(!calls.includes('focus'));
|
||||
});
|
||||
|
||||
test('untracked non-macOS overlay keeps fallback visible behavior when no tracker exists', () => {
|
||||
const { window, calls } = createMainWindowRecorder();
|
||||
let trackerWarning = false;
|
||||
|
||||
@@ -64,6 +64,7 @@ export function updateVisibleOverlayVisibility(args: {
|
||||
visibleOverlayVisible: boolean;
|
||||
modalActive?: boolean;
|
||||
forceMousePassthrough?: boolean;
|
||||
suspendVisibleOverlay?: boolean;
|
||||
overlayInteractionActive?: boolean;
|
||||
mainWindow: BrowserWindow | null;
|
||||
windowTracker: BaseWindowTracker | null;
|
||||
@@ -103,6 +104,18 @@ export function updateVisibleOverlayVisibility(args: {
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.suspendVisibleOverlay) {
|
||||
if (args.isWindowsPlatform) {
|
||||
clearPendingWindowsOverlayReveal(mainWindow);
|
||||
setOverlayWindowOpacity(mainWindow, 0);
|
||||
}
|
||||
mainWindow.setIgnoreMouseEvents(true, { forward: true });
|
||||
releaseOverlayWindowLevel(mainWindow);
|
||||
mainWindow.hide();
|
||||
args.syncOverlayShortcuts();
|
||||
return;
|
||||
}
|
||||
|
||||
const showPassiveVisibleOverlay = (): boolean => {
|
||||
const forceMousePassthrough = args.forceMousePassthrough === true;
|
||||
const wasVisible = mainWindow.isVisible();
|
||||
|
||||
@@ -7,6 +7,8 @@ export const STATS_WINDOW_TITLE = 'SubMiner Stats';
|
||||
|
||||
type StatsWindowLevelController = Pick<BrowserWindow, 'setAlwaysOnTop' | 'moveTop'> &
|
||||
Partial<Pick<BrowserWindow, 'setVisibleOnAllWorkspaces' | 'setFullScreenable'>>;
|
||||
type VisibleStatsWindowLevelController = StatsWindowLevelController &
|
||||
Pick<BrowserWindow, 'isDestroyed' | 'isVisible'>;
|
||||
|
||||
type StatsWindowBoundsController = Pick<BrowserWindow, 'getBounds' | 'getContentBounds'>;
|
||||
type StatsWindowPresentationController = Pick<BrowserWindow, 'show' | 'focus'> &
|
||||
@@ -106,6 +108,22 @@ export function promoteStatsWindowLevel(
|
||||
window.moveTop();
|
||||
}
|
||||
|
||||
export function promoteVisibleStatsWindowAboveOverlay(
|
||||
window: VisibleStatsWindowLevelController,
|
||||
options: {
|
||||
platform?: NodeJS.Platform;
|
||||
promoteHyprlandWindow?: () => void;
|
||||
} = {},
|
||||
): boolean {
|
||||
if (window.isDestroyed() || !window.isVisible()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
promoteStatsWindowLevel(window, options.platform);
|
||||
options.promoteHyprlandWindow?.();
|
||||
return true;
|
||||
}
|
||||
|
||||
export function presentStatsWindow(
|
||||
window: StatsWindowPresentationController,
|
||||
platform: NodeJS.Platform = process.platform,
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
buildStatsWindowLoadFileOptions,
|
||||
buildStatsWindowOptions,
|
||||
presentStatsWindow,
|
||||
promoteVisibleStatsWindowAboveOverlay,
|
||||
promoteStatsWindowLevel,
|
||||
resolveStatsWindowOuterBoundsForContent,
|
||||
shouldHideStatsWindowForInput,
|
||||
@@ -232,6 +233,47 @@ test('promoteStatsWindowLevel raises stats above overlay level on Windows', () =
|
||||
assert.deepEqual(calls, ['always-on-top:true:screen-saver:2', 'move-top']);
|
||||
});
|
||||
|
||||
test('promoteVisibleStatsWindowAboveOverlay reasserts stats above overlay on Hyprland', () => {
|
||||
const calls: string[] = [];
|
||||
const promoted = promoteVisibleStatsWindowAboveOverlay(
|
||||
{
|
||||
isDestroyed: () => false,
|
||||
isVisible: () => true,
|
||||
setAlwaysOnTop: (flag: boolean, level?: string, relativeLevel?: number) => {
|
||||
calls.push(`always-on-top:${flag}:${level ?? 'none'}:${relativeLevel ?? 0}`);
|
||||
},
|
||||
moveTop: () => {
|
||||
calls.push('move-top');
|
||||
},
|
||||
} as never,
|
||||
{
|
||||
platform: 'linux',
|
||||
promoteHyprlandWindow: () => calls.push('hyprland-top'),
|
||||
},
|
||||
);
|
||||
|
||||
assert.equal(promoted, true);
|
||||
assert.deepEqual(calls, ['always-on-top:true:none:0', 'move-top', 'hyprland-top']);
|
||||
});
|
||||
|
||||
test('promoteVisibleStatsWindowAboveOverlay skips hidden stats windows', () => {
|
||||
const calls: string[] = [];
|
||||
const promoted = promoteVisibleStatsWindowAboveOverlay(
|
||||
{
|
||||
isDestroyed: () => false,
|
||||
isVisible: () => false,
|
||||
setAlwaysOnTop: () => calls.push('always-on-top'),
|
||||
moveTop: () => calls.push('move-top'),
|
||||
} as never,
|
||||
{
|
||||
promoteHyprlandWindow: () => calls.push('hyprland-top'),
|
||||
},
|
||||
);
|
||||
|
||||
assert.equal(promoted, false);
|
||||
assert.deepEqual(calls, []);
|
||||
});
|
||||
|
||||
test('presentStatsWindow shows inactive on macOS to stay on the fullscreen mpv Space', () => {
|
||||
const calls: string[] = [];
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
buildStatsWindowOptions,
|
||||
presentStatsWindow,
|
||||
promoteStatsWindowLevel,
|
||||
promoteVisibleStatsWindowAboveOverlay,
|
||||
resolveStatsWindowOuterBoundsForContent,
|
||||
shouldHideStatsWindowForInput,
|
||||
STATS_WINDOW_TITLE,
|
||||
@@ -58,7 +59,19 @@ function showStatsWindow(window: BrowserWindow, options: StatsWindowOptions): vo
|
||||
placementBounds = syncStatsWindowBounds(window, bounds) ?? placementBounds;
|
||||
}
|
||||
options.onVisibilityChanged?.(true);
|
||||
promoteStatsWindowLevel(window);
|
||||
promoteStatsOverlayAbovePlayback();
|
||||
}
|
||||
|
||||
export function promoteStatsOverlayAbovePlayback(): boolean {
|
||||
if (!statsWindow) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return promoteVisibleStatsWindowAboveOverlay(statsWindow, {
|
||||
promoteHyprlandWindow: () => {
|
||||
ensureHyprlandWindowFloatingByTitle({ title: STATS_WINDOW_TITLE });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -104,7 +117,7 @@ export function toggleStatsOverlay(options: StatsWindowOptions): void {
|
||||
if (!statsWindow || statsWindow.isDestroyed() || !statsWindow.isVisible()) {
|
||||
return;
|
||||
}
|
||||
promoteStatsWindowLevel(statsWindow);
|
||||
promoteStatsOverlayAbovePlayback();
|
||||
});
|
||||
} else if (statsWindow.isVisible()) {
|
||||
statsWindow.hide();
|
||||
|
||||
+26
-6
@@ -351,8 +351,12 @@ import {
|
||||
import { resolveYoutubePlaybackUrl } from './core/services/youtube/playback-resolve';
|
||||
import { probeYoutubeTracks } from './core/services/youtube/track-probe';
|
||||
import { startStatsServer } from './core/services/stats-server';
|
||||
import { registerStatsOverlayToggle, destroyStatsWindow } from './core/services/stats-window.js';
|
||||
import { toggleStatsOverlay as toggleStatsOverlayWindow } from './core/services/stats-window.js';
|
||||
import {
|
||||
destroyStatsWindow,
|
||||
promoteStatsOverlayAbovePlayback,
|
||||
registerStatsOverlayToggle,
|
||||
toggleStatsOverlay as toggleStatsOverlayWindow,
|
||||
} from './core/services/stats-window.js';
|
||||
import {
|
||||
createFirstRunSetupService,
|
||||
getFirstRunSetupCompletionMessage,
|
||||
@@ -495,6 +499,7 @@ import {
|
||||
} from './main/jlpt-runtime';
|
||||
import { createMediaRuntimeService } from './main/media-runtime';
|
||||
import { createOverlayVisibilityRuntimeService } from './main/overlay-visibility-runtime';
|
||||
import { createStatsOverlayVisibilityChangeHandler } from './main/runtime/stats-overlay-visibility';
|
||||
import { createDiscordPresenceRuntime } from './main/runtime/discord-presence-runtime';
|
||||
import { createCharacterDictionaryRuntimeService } from './main/character-dictionary-runtime';
|
||||
import { createCharacterDictionaryAutoSyncRuntimeService } from './main/runtime/character-dictionary-auto-sync';
|
||||
@@ -2232,6 +2237,7 @@ const overlayVisibilityRuntime = createOverlayVisibilityRuntimeService(
|
||||
getModalActive: () => overlayModalInputState.getModalInputExclusive(),
|
||||
getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
|
||||
getForceMousePassthrough: () => appState.statsOverlayVisible,
|
||||
getSuspendVisibleOverlay: () => appState.statsOverlayVisible,
|
||||
getOverlayInteractionActive: () => visibleOverlayInteractionActive,
|
||||
getWindowTracker: () => appState.windowTracker,
|
||||
getLastKnownWindowsForegroundProcessName: () => lastWindowsVisibleOverlayForegroundProcessName,
|
||||
@@ -2289,6 +2295,17 @@ let lastWindowsVisibleOverlayForegroundProcessName: string | null = null;
|
||||
let lastWindowsVisibleOverlayBlurredAtMs = 0;
|
||||
let visibleOverlayInteractionActive = false;
|
||||
|
||||
const handleStatsOverlayVisibilityChanged = createStatsOverlayVisibilityChangeHandler({
|
||||
setStatsOverlayVisibleState: (visible) => {
|
||||
appState.statsOverlayVisible = visible;
|
||||
},
|
||||
resetVisibleOverlayInteraction: () => {
|
||||
visibleOverlayInteractionActive = false;
|
||||
},
|
||||
getMainWindow: () => overlayManager.getMainWindow(),
|
||||
updateVisibleOverlayVisibility: () => overlayVisibilityRuntime.updateVisibleOverlayVisibility(),
|
||||
});
|
||||
|
||||
function clearVisibleOverlayBlurRefreshTimeouts(): void {
|
||||
for (const timeout of visibleOverlayBlurRefreshTimeouts) {
|
||||
clearTimeout(timeout);
|
||||
@@ -3837,8 +3854,7 @@ const immersionTrackerStartupMainDeps: Parameters<
|
||||
getToggleKey: () => getResolvedConfig().stats.toggleKey,
|
||||
resolveBounds: () => getCurrentOverlayGeometry(),
|
||||
onVisibilityChanged: (visible) => {
|
||||
appState.statsOverlayVisible = visible;
|
||||
overlayVisibilityRuntime.updateVisibleOverlayVisibility();
|
||||
handleStatsOverlayVisibilityChanged(visible);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -4628,7 +4644,12 @@ const updateVisibleOverlayBounds = createUpdateVisibleOverlayBoundsHandler(
|
||||
|
||||
const buildEnsureOverlayWindowLevelMainDepsHandler =
|
||||
createBuildEnsureOverlayWindowLevelMainDepsHandler({
|
||||
shouldSuppressOverlayWindowLevel: (window) =>
|
||||
appState.statsOverlayVisible && window === overlayManager.getMainWindow(),
|
||||
ensureOverlayWindowLevelCore: (window) => ensureOverlayWindowLevelCore(window as BrowserWindow),
|
||||
afterEnsureOverlayWindowLevel: () => {
|
||||
promoteStatsOverlayAbovePlayback();
|
||||
},
|
||||
});
|
||||
const ensureOverlayWindowLevelMainDeps = buildEnsureOverlayWindowLevelMainDepsHandler();
|
||||
const ensureOverlayWindowLevel = createEnsureOverlayWindowLevelHandler(
|
||||
@@ -5349,8 +5370,7 @@ async function dispatchSessionAction(request: SessionActionDispatchRequest): Pro
|
||||
getToggleKey: () => getResolvedConfig().stats.toggleKey,
|
||||
resolveBounds: () => getCurrentOverlayGeometry(),
|
||||
onVisibilityChanged: (visible) => {
|
||||
appState.statsOverlayVisible = visible;
|
||||
overlayVisibilityRuntime.updateVisibleOverlayVisibility();
|
||||
handleStatsOverlayVisibilityChanged(visible);
|
||||
},
|
||||
}),
|
||||
toggleVisibleOverlay: () => toggleVisibleOverlay(),
|
||||
|
||||
@@ -11,6 +11,7 @@ export interface OverlayVisibilityRuntimeDeps {
|
||||
getModalActive: () => boolean;
|
||||
getVisibleOverlayVisible: () => boolean;
|
||||
getForceMousePassthrough: () => boolean;
|
||||
getSuspendVisibleOverlay?: () => boolean;
|
||||
getOverlayInteractionActive?: () => boolean;
|
||||
getWindowTracker: () => BaseWindowTracker | null;
|
||||
getLastKnownWindowsForegroundProcessName?: () => string | null;
|
||||
@@ -43,6 +44,7 @@ export function createOverlayVisibilityRuntimeService(
|
||||
updateVisibleOverlayVisibility(): void {
|
||||
const visibleOverlayVisible = deps.getVisibleOverlayVisible();
|
||||
const forceMousePassthrough = deps.getForceMousePassthrough();
|
||||
const suspendVisibleOverlay = deps.getSuspendVisibleOverlay?.() ?? false;
|
||||
const windowTracker = deps.getWindowTracker();
|
||||
const mainWindow = deps.getMainWindow();
|
||||
|
||||
@@ -50,6 +52,7 @@ export function createOverlayVisibilityRuntimeService(
|
||||
visibleOverlayVisible,
|
||||
modalActive: deps.getModalActive(),
|
||||
forceMousePassthrough,
|
||||
suspendVisibleOverlay,
|
||||
overlayInteractionActive: deps.getOverlayInteractionActive?.() ?? false,
|
||||
mainWindow,
|
||||
windowTracker,
|
||||
|
||||
@@ -50,7 +50,7 @@ test('linux mpv fullscreen overlay refresh burst schedules overlay refresh work
|
||||
}
|
||||
});
|
||||
|
||||
test('linux mpv fullscreen overlay refresh update cancels burst when fullscreen exits', async () => {
|
||||
test('linux mpv fullscreen overlay refresh update schedules a fresh burst when fullscreen exits', async () => {
|
||||
const originalPlatformDescriptor = Object.getOwnPropertyDescriptor(process, 'platform');
|
||||
Object.defineProperty(process, 'platform', {
|
||||
configurable: true,
|
||||
@@ -82,8 +82,11 @@ test('linux mpv fullscreen overlay refresh update cancels burst when fullscreen
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 80));
|
||||
|
||||
assert.equal(nextCancel, null);
|
||||
assert.deepEqual(calls, []);
|
||||
assert.equal(typeof nextCancel, 'function');
|
||||
assert.ok(calls.includes('updateVisibleOverlayVisibility'));
|
||||
assert.ok(calls.includes('hide'));
|
||||
assert.ok(calls.includes('showInactive'));
|
||||
assert.ok(calls.includes('ensureOverlayWindowLevel'));
|
||||
} finally {
|
||||
clearLinuxMpvFullscreenOverlayRefreshTimeouts();
|
||||
if (originalPlatformDescriptor) {
|
||||
|
||||
@@ -68,14 +68,11 @@ export function scheduleLinuxVisibleOverlayFullscreenRefreshBurst(
|
||||
}
|
||||
|
||||
export function updateLinuxMpvFullscreenOverlayRefreshBurst(
|
||||
isFullscreen: boolean,
|
||||
_isFullscreen: boolean,
|
||||
deps: LinuxMpvFullscreenOverlayRefreshDeps,
|
||||
cancelCurrentBurst: CancelLinuxMpvFullscreenOverlayRefreshBurst | null,
|
||||
): CancelLinuxMpvFullscreenOverlayRefreshBurst | null {
|
||||
cancelCurrentBurst?.();
|
||||
if (!isFullscreen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return scheduleLinuxVisibleOverlayFullscreenRefreshBurst(deps);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ test('overlay visibility runtime main deps builder maps state and geometry callb
|
||||
getModalActive: () => true,
|
||||
getVisibleOverlayVisible: () => true,
|
||||
getForceMousePassthrough: () => true,
|
||||
getSuspendVisibleOverlay: () => true,
|
||||
getOverlayInteractionActive: () => true,
|
||||
getWindowTracker: () => tracker,
|
||||
getLastKnownWindowsForegroundProcessName: () => 'mpv',
|
||||
@@ -41,6 +42,7 @@ test('overlay visibility runtime main deps builder maps state and geometry callb
|
||||
assert.equal(deps.getModalActive(), true);
|
||||
assert.equal(deps.getVisibleOverlayVisible(), true);
|
||||
assert.equal(deps.getForceMousePassthrough(), true);
|
||||
assert.equal(deps.getSuspendVisibleOverlay?.(), true);
|
||||
assert.equal(deps.getOverlayInteractionActive?.(), true);
|
||||
assert.equal(deps.getLastKnownWindowsForegroundProcessName?.(), 'mpv');
|
||||
assert.equal(deps.getWindowsOverlayProcessName?.(), 'subminer');
|
||||
|
||||
@@ -10,6 +10,7 @@ export function createBuildOverlayVisibilityRuntimeMainDepsHandler(
|
||||
getModalActive: () => deps.getModalActive(),
|
||||
getVisibleOverlayVisible: () => deps.getVisibleOverlayVisible(),
|
||||
getForceMousePassthrough: () => deps.getForceMousePassthrough(),
|
||||
getSuspendVisibleOverlay: () => deps.getSuspendVisibleOverlay?.() ?? false,
|
||||
getOverlayInteractionActive: () => deps.getOverlayInteractionActive?.() ?? false,
|
||||
getWindowTracker: () => deps.getWindowTracker(),
|
||||
getLastKnownWindowsForegroundProcessName: () =>
|
||||
|
||||
@@ -15,9 +15,16 @@ test('overlay window layout main deps builders map callbacks', () => {
|
||||
visible.setOverlayWindowBounds({ x: 0, y: 0, width: 1, height: 1 });
|
||||
|
||||
const level = createBuildEnsureOverlayWindowLevelMainDepsHandler({
|
||||
shouldSuppressOverlayWindowLevel: () => {
|
||||
calls.push('ensure-suppressed-check');
|
||||
return false;
|
||||
},
|
||||
ensureOverlayWindowLevelCore: () => calls.push('ensure'),
|
||||
afterEnsureOverlayWindowLevel: () => calls.push('ensure-after'),
|
||||
})();
|
||||
assert.equal(level.shouldSuppressOverlayWindowLevel?.({}), false);
|
||||
level.ensureOverlayWindowLevelCore({});
|
||||
level.afterEnsureOverlayWindowLevel?.({});
|
||||
|
||||
const order = createBuildEnforceOverlayLayerOrderMainDepsHandler({
|
||||
enforceOverlayLayerOrderCore: () => calls.push('order'),
|
||||
@@ -34,5 +41,12 @@ test('overlay window layout main deps builders map callbacks', () => {
|
||||
assert.deepEqual(order.getMainWindow(), { kind: 'main' });
|
||||
order.ensureOverlayWindowLevel({});
|
||||
|
||||
assert.deepEqual(calls, ['visible', 'ensure', 'order', 'order-level']);
|
||||
assert.deepEqual(calls, [
|
||||
'visible',
|
||||
'ensure-suppressed-check',
|
||||
'ensure',
|
||||
'ensure-after',
|
||||
'order',
|
||||
'order-level',
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -23,7 +23,11 @@ export function createBuildEnsureOverlayWindowLevelMainDepsHandler(
|
||||
deps: EnsureOverlayWindowLevelMainDeps,
|
||||
) {
|
||||
return (): EnsureOverlayWindowLevelMainDeps => ({
|
||||
shouldSuppressOverlayWindowLevel: (window: unknown) =>
|
||||
deps.shouldSuppressOverlayWindowLevel?.(window) ?? false,
|
||||
ensureOverlayWindowLevelCore: (window: unknown) => deps.ensureOverlayWindowLevelCore(window),
|
||||
afterEnsureOverlayWindowLevel: (window: unknown) =>
|
||||
deps.afterEnsureOverlayWindowLevel?.(window),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -36,9 +36,28 @@ test('ensure overlay window level handler delegates to core', () => {
|
||||
const calls: string[] = [];
|
||||
const ensureLevel = createEnsureOverlayWindowLevelHandler({
|
||||
ensureOverlayWindowLevelCore: () => calls.push('core'),
|
||||
afterEnsureOverlayWindowLevel: () => calls.push('after'),
|
||||
});
|
||||
ensureLevel({});
|
||||
assert.deepEqual(calls, ['core']);
|
||||
assert.deepEqual(calls, ['core', 'after']);
|
||||
});
|
||||
|
||||
test('ensure overlay window level handler skips while top reassertion is suppressed', () => {
|
||||
const calls: string[] = [];
|
||||
const window = {};
|
||||
const ensureLevel = createEnsureOverlayWindowLevelHandler({
|
||||
shouldSuppressOverlayWindowLevel: (nextWindow) => {
|
||||
assert.equal(nextWindow, window);
|
||||
calls.push('suppress-check');
|
||||
return true;
|
||||
},
|
||||
ensureOverlayWindowLevelCore: () => calls.push('core'),
|
||||
afterEnsureOverlayWindowLevel: () => calls.push('after'),
|
||||
});
|
||||
|
||||
ensureLevel(window);
|
||||
|
||||
assert.deepEqual(calls, ['suppress-check']);
|
||||
});
|
||||
|
||||
test('enforce overlay layer order handler forwards resolved state', () => {
|
||||
|
||||
@@ -11,10 +11,16 @@ export function createUpdateVisibleOverlayBoundsHandler(deps: {
|
||||
}
|
||||
|
||||
export function createEnsureOverlayWindowLevelHandler(deps: {
|
||||
shouldSuppressOverlayWindowLevel?: (window: unknown) => boolean;
|
||||
ensureOverlayWindowLevelCore: (window: unknown) => void;
|
||||
afterEnsureOverlayWindowLevel?: (window: unknown) => void;
|
||||
}) {
|
||||
return (window: unknown): void => {
|
||||
if (deps.shouldSuppressOverlayWindowLevel?.(window) === true) {
|
||||
return;
|
||||
}
|
||||
deps.ensureOverlayWindowLevelCore(window);
|
||||
deps.afterEnsureOverlayWindowLevel?.(window);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import { createStatsOverlayVisibilityChangeHandler } from './stats-overlay-visibility';
|
||||
|
||||
test('stats overlay visibility handler makes overlay mouse-passive before opening stats', () => {
|
||||
const calls: string[] = [];
|
||||
const handler = createStatsOverlayVisibilityChangeHandler({
|
||||
setStatsOverlayVisibleState: (visible) => calls.push(`state:${visible}`),
|
||||
resetVisibleOverlayInteraction: () => calls.push('reset-interaction'),
|
||||
getMainWindow: () =>
|
||||
({
|
||||
isDestroyed: () => false,
|
||||
isVisible: () => true,
|
||||
setIgnoreMouseEvents: (ignore: boolean, options?: { forward?: boolean }) => {
|
||||
calls.push(`mouse-ignore:${ignore}:${options?.forward === true ? 'forward' : 'plain'}`);
|
||||
},
|
||||
}) as never,
|
||||
updateVisibleOverlayVisibility: () => calls.push('update-visible'),
|
||||
});
|
||||
|
||||
handler(true);
|
||||
|
||||
assert.deepEqual(calls, [
|
||||
'state:true',
|
||||
'reset-interaction',
|
||||
'mouse-ignore:true:forward',
|
||||
'update-visible',
|
||||
]);
|
||||
});
|
||||
|
||||
test('stats overlay visibility handler restores overlay then leaves mpv mouse-responsive after close', () => {
|
||||
const calls: string[] = [];
|
||||
const handler = createStatsOverlayVisibilityChangeHandler({
|
||||
setStatsOverlayVisibleState: (visible) => calls.push(`state:${visible}`),
|
||||
resetVisibleOverlayInteraction: () => calls.push('reset-interaction'),
|
||||
getMainWindow: () =>
|
||||
({
|
||||
isDestroyed: () => false,
|
||||
isVisible: () => true,
|
||||
setIgnoreMouseEvents: (ignore: boolean, options?: { forward?: boolean }) => {
|
||||
calls.push(`mouse-ignore:${ignore}:${options?.forward === true ? 'forward' : 'plain'}`);
|
||||
},
|
||||
}) as never,
|
||||
updateVisibleOverlayVisibility: () => calls.push('update-visible'),
|
||||
});
|
||||
|
||||
handler(false);
|
||||
|
||||
assert.deepEqual(calls, [
|
||||
'state:false',
|
||||
'reset-interaction',
|
||||
'update-visible',
|
||||
'mouse-ignore:true:forward',
|
||||
]);
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
type StatsOverlayVisibilityWindow = {
|
||||
isDestroyed: () => boolean;
|
||||
isVisible: () => boolean;
|
||||
setIgnoreMouseEvents: (ignore: boolean, options?: { forward?: boolean }) => void;
|
||||
};
|
||||
|
||||
function makeOverlayMousePassive(window: StatsOverlayVisibilityWindow | null): void {
|
||||
if (!window || window.isDestroyed() || !window.isVisible()) {
|
||||
return;
|
||||
}
|
||||
window.setIgnoreMouseEvents(true, { forward: true });
|
||||
}
|
||||
|
||||
export function createStatsOverlayVisibilityChangeHandler(deps: {
|
||||
setStatsOverlayVisibleState: (visible: boolean) => void;
|
||||
resetVisibleOverlayInteraction: () => void;
|
||||
getMainWindow: () => StatsOverlayVisibilityWindow | null;
|
||||
updateVisibleOverlayVisibility: () => void;
|
||||
}) {
|
||||
return (visible: boolean): void => {
|
||||
deps.setStatsOverlayVisibleState(visible);
|
||||
deps.resetVisibleOverlayInteraction();
|
||||
|
||||
if (visible) {
|
||||
makeOverlayMousePassive(deps.getMainWindow());
|
||||
deps.updateVisibleOverlayVisibility();
|
||||
return;
|
||||
}
|
||||
|
||||
deps.updateVisibleOverlayVisibility();
|
||||
makeOverlayMousePassive(deps.getMainWindow());
|
||||
};
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
HyprlandWindowTracker,
|
||||
isHyprlandGeometryEvent,
|
||||
parseHyprctlClients,
|
||||
parseHyprctlMonitors,
|
||||
@@ -177,3 +178,22 @@ test('resolveHyprlandWindowGeometry uses monitor bounds for client-requested ful
|
||||
height: 1080,
|
||||
});
|
||||
});
|
||||
|
||||
test('HyprlandWindowTracker re-emits focus callback on active window events for z-order refresh', () => {
|
||||
const calls: string[] = [];
|
||||
const tracker = new HyprlandWindowTracker();
|
||||
const privateTracker = tracker as unknown as {
|
||||
handleSocketEvent: (event: string) => void;
|
||||
pollGeometry: () => void;
|
||||
};
|
||||
privateTracker.pollGeometry = () => {
|
||||
calls.push('poll');
|
||||
};
|
||||
tracker.onWindowFocusChange = (focused) => {
|
||||
calls.push(`focus:${focused}`);
|
||||
};
|
||||
|
||||
privateTracker.handleSocketEvent('activewindowv2>>0xmpv');
|
||||
|
||||
assert.deepEqual(calls, ['poll', 'focus:false']);
|
||||
});
|
||||
|
||||
@@ -295,8 +295,12 @@ export class HyprlandWindowTracker extends BaseWindowTracker {
|
||||
const data = rawData.trim();
|
||||
|
||||
if (name === 'activewindowv2') {
|
||||
const wasFocused = this.isTargetWindowFocused();
|
||||
this.activeWindowAddress = data || null;
|
||||
this.pollGeometry();
|
||||
if (this.isTargetWindowFocused() === wasFocused) {
|
||||
this.onWindowFocusChange?.(this.isTargetWindowFocused());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -336,9 +340,12 @@ export class HyprlandWindowTracker extends BaseWindowTracker {
|
||||
const mpvWindow = this.findTargetWindow(clients);
|
||||
|
||||
if (mpvWindow) {
|
||||
const focused = !this.activeWindowAddress || mpvWindow.address === this.activeWindowAddress;
|
||||
this.updateGeometry(
|
||||
resolveHyprlandWindowGeometry(mpvWindow, this.getHyprlandMonitors(mpvWindow)),
|
||||
focused,
|
||||
);
|
||||
this.updateTargetWindowFocused(focused);
|
||||
} else {
|
||||
this.updateGeometry(null);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user