diff --git a/changes/fix-windows-overlay-scaling.md b/changes/fix-windows-overlay-scaling.md new file mode 100644 index 0000000..f6b4e34 --- /dev/null +++ b/changes/fix-windows-overlay-scaling.md @@ -0,0 +1,4 @@ +type: fixed +area: overlay + +- Fixed Windows overlay window tracking on scaled displays by converting native tracked window bounds to Electron DIP coordinates before applying overlay bounds. diff --git a/src/core/services/overlay-window-bounds.test.ts b/src/core/services/overlay-window-bounds.test.ts new file mode 100644 index 0000000..255f832 --- /dev/null +++ b/src/core/services/overlay-window-bounds.test.ts @@ -0,0 +1,36 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { normalizeOverlayWindowBoundsForPlatform } from './overlay-window-bounds'; + +test('normalizeOverlayWindowBoundsForPlatform returns original geometry outside Windows', () => { + const geometry = { x: 150, y: 90, width: 1200, height: 675 }; + assert.deepEqual(normalizeOverlayWindowBoundsForPlatform(geometry, 'linux', null), geometry); +}); + +test('normalizeOverlayWindowBoundsForPlatform converts Windows physical pixels to DIP', () => { + assert.deepEqual( + normalizeOverlayWindowBoundsForPlatform( + { + x: 150, + y: 75, + width: 1920, + height: 1080, + }, + 'win32', + { + screenToDipRect: (_window, rect) => ({ + x: Math.round(rect.x / 1.5), + y: Math.round(rect.y / 1.5), + width: Math.round(rect.width / 1.5), + height: Math.round(rect.height / 1.5), + }), + }, + ), + { + x: 100, + y: 50, + width: 1280, + height: 720, + }, + ); +}); diff --git a/src/core/services/overlay-window-bounds.ts b/src/core/services/overlay-window-bounds.ts new file mode 100644 index 0000000..de5e2bc --- /dev/null +++ b/src/core/services/overlay-window-bounds.ts @@ -0,0 +1,25 @@ +import type { WindowGeometry } from '../../types'; + +type ScreenDipConverter = { + screenToDipRect: ( + window: Electron.BrowserWindow | null, + rect: Electron.Rectangle, + ) => Electron.Rectangle; +}; + +export function normalizeOverlayWindowBoundsForPlatform( + geometry: WindowGeometry, + platform: NodeJS.Platform, + screen: ScreenDipConverter | null, +): WindowGeometry { + if (platform !== 'win32' || !screen) { + return geometry; + } + + return screen.screenToDipRect(null, { + x: geometry.x, + y: geometry.y, + width: geometry.width, + height: geometry.height, + }); +} diff --git a/src/core/services/overlay-window.ts b/src/core/services/overlay-window.ts index 65fada1..96393ef 100644 --- a/src/core/services/overlay-window.ts +++ b/src/core/services/overlay-window.ts @@ -1,4 +1,4 @@ -import { BrowserWindow, type Session } from 'electron'; +import { BrowserWindow, screen, type Session } from 'electron'; import * as path from 'path'; import { WindowGeometry } from '../../types'; import { createLogger } from '../../logger'; @@ -8,6 +8,7 @@ import { type OverlayWindowKind, } from './overlay-window-input'; import { buildOverlayWindowOptions } from './overlay-window-options'; +import { normalizeOverlayWindowBoundsForPlatform } from './overlay-window-bounds'; const logger = createLogger('main:overlay-window'); const overlayWindowLayerByInstance = new WeakMap(); @@ -33,12 +34,7 @@ export function updateOverlayWindowBounds( window: BrowserWindow | null, ): void { if (!geometry || !window || window.isDestroyed()) return; - window.setBounds({ - x: geometry.x, - y: geometry.y, - width: geometry.width, - height: geometry.height, - }); + window.setBounds(normalizeOverlayWindowBoundsForPlatform(geometry, process.platform, screen)); } export function ensureOverlayWindowLevel(window: BrowserWindow): void {