fix(overlay): correct Hyprland fullscreen overlay alignment on Linux (#107)

This commit is contained in:
2026-06-01 02:12:16 -07:00
committed by GitHub
parent f1e260e996
commit 76f99e6518
15 changed files with 501 additions and 23 deletions
@@ -0,0 +1,4 @@
type: fixed
area: overlay
- Fixed Hyprland fullscreen overlay alignment by verifying compositor client bounds, preventing modal, stats, and sidebar content from shifting below the mpv window.
@@ -4,6 +4,7 @@ import {
buildHyprlandPlacementDispatches,
ensureHyprlandWindowFloatingByTitle,
findHyprlandWindowForPlacement,
hasHyprlandWindowPlacementBoundsMismatch,
shouldAttemptHyprlandWindowPlacement,
} from './hyprland-window-placement';
@@ -83,8 +84,8 @@ test('buildHyprlandPlacementDispatches force-aligns floating overlay windows to
},
),
[
['dispatch', 'movewindowpixel', 'exact 0 0,address:0xabc'],
['dispatch', 'resizewindowpixel', 'exact 1920 1080,address:0xabc'],
['dispatch', 'movewindowpixel', 'exact 0 0,address:0xabc'],
['dispatch', 'setprop', 'address:0xabc rounding 0'],
['dispatch', 'setprop', 'address:0xabc border_size 0'],
['dispatch', 'setprop', 'address:0xabc no_shadow 1'],
@@ -116,8 +117,8 @@ test('buildHyprlandPlacementDispatches emits Lua dispatchers for Lua-config Hypr
[
['dispatch', 'hl.dsp.window.float({ action = "on", window = "address:0xabc" })'],
['dispatch', 'hl.dsp.window.pin({ action = "off", window = "address:0xabc" })'],
['dispatch', 'hl.dsp.window.move({ x = 0, y = 0, window = "address:0xabc" })'],
['dispatch', 'hl.dsp.window.resize({ x = 1920, y = 1080, window = "address:0xabc" })'],
['dispatch', 'hl.dsp.window.move({ x = 0, y = 0, window = "address:0xabc" })'],
[
'dispatch',
'hl.dsp.window.set_prop({ prop = "rounding", value = "0", window = "address:0xabc" })',
@@ -177,8 +178,8 @@ test('buildHyprlandPlacementDispatches can update placement without raising z-or
{ promote: false },
),
[
['dispatch', 'movewindowpixel', 'exact 0 0,address:0xabc'],
['dispatch', 'resizewindowpixel', 'exact 1920 1080,address:0xabc'],
['dispatch', 'movewindowpixel', 'exact 0 0,address:0xabc'],
['dispatch', 'setprop', 'address:0xabc rounding 0'],
['dispatch', 'setprop', 'address:0xabc border_size 0'],
['dispatch', 'setprop', 'address:0xabc no_shadow 1'],
@@ -286,8 +287,8 @@ test('ensureHyprlandWindowFloatingByTitle dispatches exact Hyprland geometry whe
[
['-j', 'clients'],
['-j', 'status'],
['dispatch', 'movewindowpixel', 'exact 0 0,address:0xmatch'],
['dispatch', 'resizewindowpixel', 'exact 1920 1080,address:0xmatch'],
['dispatch', 'movewindowpixel', 'exact 0 0,address:0xmatch'],
['dispatch', 'setprop', 'address:0xmatch rounding 0'],
['dispatch', 'setprop', 'address:0xmatch border_size 0'],
['dispatch', 'setprop', 'address:0xmatch no_shadow 1'],
@@ -340,8 +341,8 @@ test('ensureHyprlandWindowFloatingByTitle dispatches Lua syntax for Lua-config H
[
['-j', 'clients'],
['-j', 'status'],
['dispatch', 'hl.dsp.window.move({ x = 0, y = 0, window = "address:0xmatch" })'],
['dispatch', 'hl.dsp.window.resize({ x = 1920, y = 1080, window = "address:0xmatch" })'],
['dispatch', 'hl.dsp.window.move({ x = 0, y = 0, window = "address:0xmatch" })'],
[
'dispatch',
'hl.dsp.window.set_prop({ prop = "rounding", value = "0", window = "address:0xmatch" })',
@@ -366,3 +367,97 @@ test('ensureHyprlandWindowFloatingByTitle dispatches Lua syntax for Lua-config H
],
);
});
test('hasHyprlandWindowPlacementBoundsMismatch compares compositor client bounds', () => {
const mismatch = hasHyprlandWindowPlacementBoundsMismatch({
title: 'SubMiner Overlay',
platform: 'linux',
env: {
HYPRLAND_INSTANCE_SIGNATURE: 'abc',
},
pid: 456,
bounds: {
x: 0,
y: 0,
width: 3440,
height: 1440,
},
execFileSync: ((command: string, args: string[]) => {
assert.equal(command, 'hyprctl');
assert.deepEqual(args, ['-j', 'clients']);
return JSON.stringify([
{
address: '0xmatch',
pid: 456,
title: 'SubMiner Overlay',
mapped: true,
floating: true,
at: [0, 14],
size: [3440, 1426],
},
]);
}) as never,
});
assert.equal(mismatch, true);
});
test('ensureHyprlandWindowFloatingByTitle retries when compositor bounds stay misaligned', () => {
let clientReads = 0;
const calls: unknown[][] = [];
const placed = ensureHyprlandWindowFloatingByTitle({
title: 'SubMiner Overlay',
platform: 'linux',
env: {
HYPRLAND_INSTANCE_SIGNATURE: 'abc',
},
pid: 456,
bounds: {
x: 0,
y: 0,
width: 3440,
height: 1440,
},
execFileSync: ((command: string, args: string[], options: unknown) => {
calls.push([command, args, options]);
if (args.join(' ') === '-j clients') {
clientReads += 1;
return JSON.stringify([
{
address: '0xmatch',
pid: 456,
title: 'SubMiner Overlay',
mapped: true,
floating: true,
pinned: false,
at: clientReads === 1 ? [10, 58] : [0, 14],
size: clientReads === 1 ? [3420, 1372] : [3440, 1426],
},
]);
}
if (args.join(' ') === '-j status') {
return JSON.stringify({ configProvider: 'hyprlang' });
}
return '';
}) as never,
});
assert.equal(placed, true);
assert.equal(clientReads, 2);
assert.deepEqual(
calls
.map(([, args]) => args)
.filter(
(args) =>
Array.isArray(args) &&
args[0] === 'dispatch' &&
(args[1] === 'resizewindowpixel' || args[1] === 'movewindowpixel'),
),
[
['dispatch', 'resizewindowpixel', 'exact 3440 1440,address:0xmatch'],
['dispatch', 'movewindowpixel', 'exact 0 0,address:0xmatch'],
['dispatch', 'resizewindowpixel', 'exact 3440 1440,address:0xmatch'],
['dispatch', 'movewindowpixel', 'exact 0 0,address:0xmatch'],
],
);
});
+127 -13
View File
@@ -2,12 +2,14 @@ import { execFileSync } from 'node:child_process';
export interface HyprlandPlacementClient {
address?: string;
at?: [number, number];
floating?: boolean;
hidden?: boolean;
initialTitle?: string;
mapped?: boolean;
pid?: number;
pinned?: boolean;
size?: [number, number];
title?: string;
}
@@ -43,6 +45,10 @@ function parseHyprlandClients(output: string): HyprlandPlacementClient[] {
return Array.isArray(parsed) ? (parsed as HyprlandPlacementClient[]) : [];
}
function readHyprlandPlacementClients(run: ExecFileSync): HyprlandPlacementClient[] {
return parseHyprlandClients(String(run('hyprctl', ['-j', 'clients'], { encoding: 'utf-8' })));
}
export function findHyprlandWindowForPlacement(
clients: HyprlandPlacementClient[],
options: {
@@ -96,18 +102,18 @@ export function buildHyprlandPlacementDispatches(
const roundedBounds = roundPlacementBounds(bounds);
if (roundedBounds) {
if (configProvider === 'lua') {
dispatches.push(
luaWindowDispatch('move', windowAddress, [
`x = ${roundedBounds.x}`,
`y = ${roundedBounds.y}`,
]),
);
dispatches.push(
luaWindowDispatch('resize', windowAddress, [
`x = ${roundedBounds.width}`,
`y = ${roundedBounds.height}`,
]),
);
dispatches.push(
luaWindowDispatch('move', windowAddress, [
`x = ${roundedBounds.x}`,
`y = ${roundedBounds.y}`,
]),
);
dispatches.push(luaWindowSetProp(windowAddress, 'rounding', '0'));
dispatches.push(luaWindowSetProp(windowAddress, 'border_size', '0'));
dispatches.push(luaWindowSetProp(windowAddress, 'no_shadow', '1'));
@@ -116,13 +122,13 @@ export function buildHyprlandPlacementDispatches(
} else {
dispatches.push([
'dispatch',
'movewindowpixel',
`exact ${roundedBounds.x} ${roundedBounds.y},${windowAddress}`,
'resizewindowpixel',
`exact ${roundedBounds.width} ${roundedBounds.height},${windowAddress}`,
]);
dispatches.push([
'dispatch',
'resizewindowpixel',
`exact ${roundedBounds.width} ${roundedBounds.height},${windowAddress}`,
'movewindowpixel',
`exact ${roundedBounds.x} ${roundedBounds.y},${windowAddress}`,
]);
dispatches.push(['dispatch', 'setprop', `${windowAddress} rounding 0`]);
dispatches.push(['dispatch', 'setprop', `${windowAddress} border_size 0`]);
@@ -181,6 +187,91 @@ function roundPlacementBounds(
: null;
}
function isFiniteTuple(value: unknown): value is [number, number] {
return (
Array.isArray(value) &&
value.length >= 2 &&
typeof value[0] === 'number' &&
typeof value[1] === 'number' &&
Number.isFinite(value[0]) &&
Number.isFinite(value[1])
);
}
export function getHyprlandClientPlacementBounds(
client: HyprlandPlacementClient,
): HyprlandPlacementBounds | null {
if (!isFiniteTuple(client.at) || !isFiniteTuple(client.size)) {
return null;
}
return roundPlacementBounds({
x: client.at[0],
y: client.at[1],
width: client.size[0],
height: client.size[1],
});
}
export function hyprlandPlacementBoundsMatch(
actual: HyprlandPlacementBounds | null,
target: HyprlandPlacementBounds | null,
tolerancePx = 1,
): boolean {
const roundedActual = roundPlacementBounds(actual);
const roundedTarget = roundPlacementBounds(target);
if (!roundedActual || !roundedTarget) {
return false;
}
return (
Math.abs(roundedActual.x - roundedTarget.x) <= tolerancePx &&
Math.abs(roundedActual.y - roundedTarget.y) <= tolerancePx &&
Math.abs(roundedActual.width - roundedTarget.width) <= tolerancePx &&
Math.abs(roundedActual.height - roundedTarget.height) <= tolerancePx
);
}
function clientMatchesPlacementBounds(
client: HyprlandPlacementClient,
bounds: HyprlandPlacementBounds,
): boolean | null {
const actual = getHyprlandClientPlacementBounds(client);
return actual ? hyprlandPlacementBoundsMatch(actual, bounds) : null;
}
export function hasHyprlandWindowPlacementBoundsMismatch(options: {
title: string;
bounds?: HyprlandPlacementBounds | null;
platform?: NodeJS.Platform;
env?: NodeJS.ProcessEnv;
pid?: number;
execFileSync?: ExecFileSync;
}): boolean {
if (!shouldAttemptHyprlandWindowPlacement(options.platform, options.env)) {
return false;
}
const targetBounds = roundPlacementBounds(options.bounds);
if (!targetBounds) {
return false;
}
const run = options.execFileSync ?? execFileSync;
try {
const client = findHyprlandWindowForPlacement(readHyprlandPlacementClients(run), {
pid: options.pid ?? process.pid,
title: options.title,
});
if (!client) {
return false;
}
return clientMatchesPlacementBounds(client, targetBounds) === false;
} catch {
return false;
}
}
export function ensureHyprlandWindowFloatingByTitle(options: {
title: string;
bounds?: HyprlandPlacementBounds | null;
@@ -196,9 +287,7 @@ export function ensureHyprlandWindowFloatingByTitle(options: {
const run = options.execFileSync ?? execFileSync;
try {
const clients = parseHyprlandClients(
String(run('hyprctl', ['-j', 'clients'], { encoding: 'utf-8' })),
);
const clients = readHyprlandPlacementClients(run);
const client = findHyprlandWindowForPlacement(clients, {
pid: options.pid ?? process.pid,
title: options.title,
@@ -208,6 +297,9 @@ export function ensureHyprlandWindowFloatingByTitle(options: {
}
const configProvider = detectHyprlandConfigProvider(run);
const targetBounds = roundPlacementBounds(options.bounds);
const shouldVerifyBounds =
targetBounds !== null && clientMatchesPlacementBounds(client, targetBounds) === false;
const dispatches = buildHyprlandPlacementDispatches(client, options.bounds, {
configProvider,
promote: options.promote,
@@ -215,6 +307,28 @@ export function ensureHyprlandWindowFloatingByTitle(options: {
for (const args of dispatches) {
run('hyprctl', args, { stdio: 'ignore' });
}
if (shouldVerifyBounds) {
try {
const refreshedClient = findHyprlandWindowForPlacement(readHyprlandPlacementClients(run), {
pid: options.pid ?? process.pid,
title: options.title,
});
if (
refreshedClient &&
targetBounds &&
clientMatchesPlacementBounds(refreshedClient, targetBounds) === false
) {
for (const args of buildHyprlandPlacementDispatches(refreshedClient, targetBounds, {
configProvider,
promote: options.promote,
})) {
run('hyprctl', args, { stdio: 'ignore' });
}
}
} catch {
// Best-effort reconciliation: the initial placement dispatches already ran.
}
}
return dispatches.length > 0;
} catch {
return false;
@@ -7,6 +7,22 @@ test('normalizeOverlayWindowBoundsForPlatform returns original geometry outside
assert.deepEqual(normalizeOverlayWindowBoundsForPlatform(geometry, 'linux', null), geometry);
});
test('normalizeOverlayWindowBoundsForPlatform compensates Linux content insets', () => {
assert.deepEqual(
normalizeOverlayWindowBoundsForPlatform(
{ x: 0, y: 0, width: 3440, height: 1440 },
'linux',
null,
{
isDestroyed: () => false,
getBounds: () => ({ x: 0, y: 0, width: 3440, height: 1440 }),
getContentBounds: () => ({ x: 0, y: 14, width: 3440, height: 1426 }),
},
),
{ x: 0, y: -14, width: 3440, height: 1454 },
);
});
test('normalizeOverlayWindowBoundsForPlatform returns original geometry on Windows when screen is unavailable', () => {
const geometry = { x: 150, y: 90, width: 1200, height: 675 };
assert.deepEqual(normalizeOverlayWindowBoundsForPlatform(geometry, 'win32', null), geometry);
@@ -7,11 +7,56 @@ type ScreenDipConverter = {
) => Electron.Rectangle;
};
type ContentBoundsWindow = {
isDestroyed: () => boolean;
getBounds: () => Electron.Rectangle;
getContentBounds: () => Electron.Rectangle;
};
function resolveContentAlignedBounds(
geometry: WindowGeometry,
window?: ContentBoundsWindow | null,
): WindowGeometry {
if (!window || window.isDestroyed()) {
return geometry;
}
let outer: Electron.Rectangle;
let content: Electron.Rectangle;
try {
outer = window.getBounds();
content = window.getContentBounds();
} catch {
return geometry;
}
const leftInset = content.x - outer.x;
const topInset = content.y - outer.y;
const rightInset = outer.x + outer.width - (content.x + content.width);
const bottomInset = outer.y + outer.height - (content.y + content.height);
const insets = [leftInset, topInset, rightInset, bottomInset];
if (insets.some((inset) => !Number.isFinite(inset) || inset < 0)) {
return geometry;
}
return {
x: geometry.x - leftInset,
y: geometry.y - topInset,
width: geometry.width + leftInset + rightInset,
height: geometry.height + topInset + bottomInset,
};
}
export function normalizeOverlayWindowBoundsForPlatform(
geometry: WindowGeometry,
platform: NodeJS.Platform,
screen: ScreenDipConverter | null,
window?: ContentBoundsWindow | null,
): WindowGeometry {
if (platform === 'linux') {
return resolveContentAlignedBounds(geometry, window);
}
if (platform !== 'win32' || !screen) {
return geometry;
}
+35 -1
View File
@@ -1,6 +1,6 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { ensureOverlayWindowLevel } from './overlay-window';
import { ensureOverlayWindowLevel, updateOverlayWindowBounds } from './overlay-window';
import {
handleOverlayWindowBeforeInputEvent,
handleOverlayWindowBlurred,
@@ -288,3 +288,37 @@ test('ensureOverlayWindowLevel promotes Linux overlay above fullscreen mpv witho
'move-top',
]);
});
test('updateOverlayWindowBounds aligns Linux overlay content bounds to mpv geometry', () => {
const originalPlatformDescriptor = Object.getOwnPropertyDescriptor(process, 'platform');
const originalHyprlandSignature = process.env.HYPRLAND_INSTANCE_SIGNATURE;
Object.defineProperty(process, 'platform', {
configurable: true,
value: 'linux',
});
delete process.env.HYPRLAND_INSTANCE_SIGNATURE;
const calls: Array<{ x: number; y: number; width: number; height: number }> = [];
try {
updateOverlayWindowBounds({ x: 0, y: 0, width: 3440, height: 1440 }, {
isDestroyed: () => false,
getTitle: () => 'SubMiner Overlay',
getBounds: () => ({ x: 0, y: 0, width: 3440, height: 1440 }),
getContentBounds: () => ({ x: 0, y: 14, width: 3440, height: 1426 }),
setBounds: (bounds: { x: number; y: number; width: number; height: number }) => {
calls.push(bounds);
},
} as never);
} finally {
if (originalPlatformDescriptor) {
Object.defineProperty(process, 'platform', originalPlatformDescriptor);
}
if (originalHyprlandSignature === undefined) {
delete process.env.HYPRLAND_INSTANCE_SIGNATURE;
} else {
process.env.HYPRLAND_INSTANCE_SIGNATURE = originalHyprlandSignature;
}
}
assert.deepEqual(calls, [{ x: 0, y: -14, width: 3440, height: 1454 }]);
});
+6 -1
View File
@@ -56,7 +56,12 @@ export function updateOverlayWindowBounds(
} = {},
): void {
if (!geometry || !window || window.isDestroyed()) return;
const bounds = normalizeOverlayWindowBoundsForPlatform(geometry, process.platform, screen);
const bounds = normalizeOverlayWindowBoundsForPlatform(
geometry,
process.platform,
screen,
window,
);
window.setBounds(bounds);
ensureHyprlandWindowFloatingByTitle({
title: window.getTitle(),
+19
View File
@@ -8,6 +8,7 @@ import type { WindowGeometry } from '../../types';
const DEFAULT_STATS_WINDOW_WIDTH = 900;
const DEFAULT_STATS_WINDOW_HEIGHT = 700;
export const STATS_WINDOW_TITLE = 'SubMiner Stats';
const STATS_POST_SHOW_RECONCILE_DELAYS_MS = [50, 150, 300, 600] as const;
type StatsWindowLevelController = Pick<BrowserWindow, 'setAlwaysOnTop' | 'moveTop'> &
Partial<Pick<BrowserWindow, 'setVisibleOnAllWorkspaces' | 'setFullScreenable'>>;
@@ -26,6 +27,14 @@ type StatsNativeConfirmDialogPresenter<WindowT> = {
type StatsWindowBoundsController = Pick<BrowserWindow, 'getBounds' | 'getContentBounds'>;
type StatsWindowPresentationController = Pick<BrowserWindow, 'show' | 'focus'> &
Partial<Pick<BrowserWindow, 'showInactive'>>;
type StatsWindowReconcileScheduler = {
setTimeout: (
callback: () => void,
delayMs: number,
) => {
unref?: () => void;
};
};
function isBareToggleKeyInput(input: Electron.Input, toggleKey: string): boolean {
return (
@@ -189,6 +198,16 @@ export function presentStatsWindow(
window.focus();
}
export function scheduleStatsWindowPostShowReconciles(
reconcile: () => void,
scheduler: StatsWindowReconcileScheduler = globalThis,
): void {
for (const delayMs of STATS_POST_SHOW_RECONCILE_DELAYS_MS) {
const timeout = scheduler.setTimeout(reconcile, delayMs);
timeout.unref?.();
}
}
export function buildStatsWindowLoadFileOptions(apiBaseUrl?: string): {
query: Record<string, string>;
} {
+37
View File
@@ -9,6 +9,7 @@ import {
promoteVisibleStatsWindowAboveOverlay,
promoteStatsWindowLevel,
resolveStatsWindowOuterBoundsForContent,
scheduleStatsWindowPostShowReconciles,
showStatsNativeConfirmDialog,
shouldHideStatsWindowForInput,
} from './stats-window-runtime';
@@ -402,3 +403,39 @@ test('presentStatsWindow shows and focuses on non-macOS platforms', () => {
assert.deepEqual(calls, ['show', 'focus']);
});
test('scheduleStatsWindowPostShowReconciles retries placement after a reused hidden window is remapped', () => {
const calls: string[] = [];
scheduleStatsWindowPostShowReconciles(
() => {
calls.push('reconcile');
},
{
setTimeout: (callback, delayMs) => {
calls.push(`timer:${delayMs}`);
callback();
return {
unref: () => {
calls.push(`unref:${delayMs}`);
},
};
},
},
);
assert.deepEqual(calls, [
'timer:50',
'reconcile',
'unref:50',
'timer:150',
'reconcile',
'unref:150',
'timer:300',
'reconcile',
'unref:300',
'timer:600',
'reconcile',
'unref:600',
]);
});
+22
View File
@@ -10,6 +10,7 @@ import {
promoteStatsWindowLevel,
promoteVisibleStatsWindowAboveOverlay,
resolveStatsWindowOuterBoundsForContent,
scheduleStatsWindowPostShowReconciles,
showStatsNativeConfirmDialog,
shouldHideStatsWindowForInput,
STATS_WINDOW_TITLE,
@@ -58,6 +59,25 @@ function syncStatsWindowBounds(
return outerBounds;
}
function reconcileStatsWindowBounds(window: BrowserWindow, options: StatsWindowOptions): void {
if (window.isDestroyed() || !window.isVisible()) {
return;
}
const placementBounds = syncStatsWindowBounds(window, options.resolveBounds());
if (placementBounds) {
ensureHyprlandWindowFloatingByTitle({ title: STATS_WINDOW_TITLE, bounds: placementBounds });
}
}
function scheduleStatsWindowBoundsReconcile(
window: BrowserWindow,
options: StatsWindowOptions,
): void {
scheduleStatsWindowPostShowReconciles(() => {
reconcileStatsWindowBounds(window, options);
});
}
function showStatsWindow(window: BrowserWindow, options: StatsWindowOptions): void {
const bounds = options.resolveBounds();
let placementBounds = syncStatsWindowBounds(window, bounds);
@@ -71,6 +91,8 @@ function showStatsWindow(window: BrowserWindow, options: StatsWindowOptions): vo
}
options.onVisibilityChanged?.(true);
promoteStatsOverlayAbovePlayback();
reconcileStatsWindowBounds(window, options);
scheduleStatsWindowBoundsReconcile(window, options);
}
export function promoteStatsOverlayAbovePlayback(): boolean {
+21 -2
View File
@@ -398,6 +398,8 @@ import {
acquireYoutubeSubtitleTrack,
acquireYoutubeSubtitleTracks,
} 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 { probeYoutubeTracks } from './core/services/youtube/track-probe';
import { startStatsServer } from './core/services/stats-server';
@@ -5460,16 +5462,33 @@ function maybeExitLinuxFullscreenOverrideForTrackedGeometry(geometry: WindowGeom
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(
(hasLiveOverlayWindowBoundsMismatch(
[overlayManager.getMainWindow(), overlayManager.getModalWindow()],
geometry,
)),
) ||
hasHyprlandOverlayWindowPlacementMismatch(geometry))),
setOverlayWindowBounds: (geometry) => applyOverlayRegions(geometry),
afterSetOverlayWindowBounds: () => {
if (!overlayManager.getVisibleOverlayVisible()) {
+1
View File
@@ -230,6 +230,7 @@ test('sendToActiveOverlayWindow targets modal window with full geometry and trac
runtime.notifyOverlayModalOpened('runtime-options');
assert.equal(window.getShowCount(), 1);
assert.equal(window.isFocused(), true);
assert.deepEqual(calls, ['bounds:10,20,300,200', 'bounds:10,20,300,200']);
assert.deepEqual(window.alwaysOnTopCalls, ['top:true:screen-saver:3']);
assert.deepEqual(window.sent, [['runtime-options:open']]);
});
+23
View File
@@ -4,6 +4,7 @@ import type { WindowGeometry } from '../types';
import { OVERLAY_WINDOW_CONTENT_READY_FLAG } from '../core/services/overlay-window-flags';
const MODAL_REVEAL_FALLBACK_DELAY_MS = 250;
const MODAL_POST_SHOW_BOUNDS_RECONCILE_DELAY_MS = 50;
function requestOverlayApplicationFocus(): void {
try {
@@ -144,6 +145,24 @@ export function createOverlayModalRuntimeService(
window.moveTop();
};
const reconcileModalWindowBounds = (window: BrowserWindow): void => {
const modalWindow = deps.getModalWindow();
if (!modalWindow || modalWindow !== window || window.isDestroyed()) {
return;
}
deps.setModalWindowBounds(deps.getModalGeometry());
};
const scheduleModalWindowBoundsReconcile = (window: BrowserWindow): void => {
const timeout = setTimeout(() => {
if (window.isDestroyed() || !window.isVisible()) {
return;
}
reconcileModalWindowBounds(window);
}, MODAL_POST_SHOW_BOUNDS_RECONCILE_DELAY_MS);
timeout.unref?.();
};
const sendOrQueueForWindow = (
window: BrowserWindow,
sendNow: (window: BrowserWindow) => void,
@@ -187,6 +206,8 @@ export function createOverlayModalRuntimeService(
if (!window.webContents.isFocused()) {
window.webContents.focus();
}
reconcileModalWindowBounds(window);
scheduleModalWindowBoundsReconcile(window);
};
const ensureModalWindowInteractive = (window: BrowserWindow): void => {
@@ -198,6 +219,8 @@ export function createOverlayModalRuntimeService(
if (window.isVisible()) {
window.focus();
window.webContents.focus();
reconcileModalWindowBounds(window);
scheduleModalWindowBoundsReconcile(window);
return;
}
@@ -99,6 +99,37 @@ test('live overlay bounds mismatch forces refresh after window manager restore d
);
});
test('live overlay bounds mismatch compares content bounds when compositor adds insets', () => {
const geometry = { x: 0, y: 0, width: 3440, height: 1440 };
assert.equal(
hasLiveOverlayWindowBoundsMismatch(
[
{
isDestroyed: () => false,
getBounds: () => ({ ...geometry }),
getContentBounds: () => ({ x: 0, y: 14, width: 3440, height: 1426 }),
},
],
geometry,
),
true,
);
assert.equal(
hasLiveOverlayWindowBoundsMismatch(
[
{
isDestroyed: () => false,
getBounds: () => ({ x: 0, y: -14, width: 3440, height: 1454 }),
getContentBounds: () => ({ ...geometry }),
},
],
geometry,
),
false,
);
});
test('ensure overlay window level handler delegates to core', () => {
const calls: string[] = [];
const ensureLevel = createEnsureOverlayWindowLevelHandler({
+14 -1
View File
@@ -3,12 +3,25 @@ import type { WindowGeometry } from '../../types';
type OverlayBoundsWindow = {
isDestroyed: () => boolean;
getBounds: () => WindowGeometry;
getContentBounds?: () => WindowGeometry;
};
function sameGeometry(a: WindowGeometry | null | undefined, b: WindowGeometry): boolean {
return a?.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height;
}
function getWindowAlignmentBounds(window: OverlayBoundsWindow): WindowGeometry | null {
try {
return window.getContentBounds?.() ?? window.getBounds();
} catch {
try {
return window.getBounds();
} catch {
return null;
}
}
}
export function hasLiveOverlayWindowBoundsMismatch(
windows: Array<OverlayBoundsWindow | null | undefined>,
geometry: WindowGeometry,
@@ -17,7 +30,7 @@ export function hasLiveOverlayWindowBoundsMismatch(
if (!window || window.isDestroyed()) {
return false;
}
return !sameGeometry(window.getBounds(), geometry);
return !sameGeometry(getWindowAlignmentBounds(window), geometry);
});
}