upgrade Electron 39→42 and fix Hyprland overlay z-order/placement (#79)

This commit is contained in:
2026-05-22 23:22:51 -07:00
committed by GitHub
parent c6328eef09
commit c4f99fec2f
30 changed files with 557 additions and 126 deletions
@@ -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;
+13
View File
@@ -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();
+18
View File
@@ -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,
+42
View File
@@ -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[] = [];
+15 -2
View File
@@ -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
View File
@@ -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(),
+3
View File
@@ -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),
});
}
+20 -1
View File
@@ -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']);
});
+7
View File
@@ -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);
}