From 57ddd1995388ea04a3f3a945afb829dceeb90bf3 Mon Sep 17 00:00:00 2001 From: sudacode Date: Tue, 11 Aug 2026 22:24:01 -0700 Subject: [PATCH] fix(overlay): handle X11 display scaling across monitors (#193) --- changes/x11-overlay-display-scaling.md | 4 ++ docs-site/troubleshooting.md | 3 +- src/core/utils/electron-backend.test.ts | 69 ++++++++++++++++++- src/core/utils/electron-backend.ts | 38 +++++++++- src/main-entry-runtime.test.ts | 16 +++++ src/main-entry-runtime.ts | 21 ++++++ src/main-entry.ts | 35 +++++----- src/main.ts | 10 --- .../runtime/linux-x11-cursor-point.test.ts | 23 +++++++ src/main/runtime/linux-x11-cursor-point.ts | 4 +- .../visible-overlay-interaction-runtime.ts | 8 ++- src/window-trackers/index.ts | 8 ++- src/window-trackers/x11-tracker.test.ts | 41 +++++++++++ src/window-trackers/x11-tracker.ts | 29 +++++++- 14 files changed, 272 insertions(+), 37 deletions(-) create mode 100644 changes/x11-overlay-display-scaling.md diff --git a/changes/x11-overlay-display-scaling.md b/changes/x11-overlay-display-scaling.md new file mode 100644 index 00000000..d07aa4f1 --- /dev/null +++ b/changes/x11-overlay-display-scaling.md @@ -0,0 +1,4 @@ +type: fixed +area: overlay + +- Fixed X11/XWayland overlays being oversized and offset from mpv under fractional or mixed-monitor display scaling. diff --git a/docs-site/troubleshooting.md b/docs-site/troubleshooting.md index d4c4c47b..f383474a 100644 --- a/docs-site/troubleshooting.md +++ b/docs-site/troubleshooting.md @@ -405,8 +405,9 @@ On any Wayland session that is not Hyprland or Sway (KDE Plasma, GNOME, and othe SubMiner handles this automatically: -- It launches its own window under XWayland (it sets `--ozone-platform-hint=x11`). +- It launches its own window under XWayland (it sets `--ozone-platform=x11`). - Every mpv it launches (via the `subminer` launcher, Jellyfin, or YouTube) is pinned to XWayland too - Wayland environment hints are stripped and an X11 GPU context (`--gpu-context=x11vk,x11egl,x11`) is applied. Only the window context is overridden; your `vo`/`gpu-api` and user shaders are left alone. +- Fractional and mixed-monitor display scaling is handled per screen when SubMiner maps XWayland mpv coordinates to the overlay. - While mpv is windowed, the overlay is a managed X11 window owned by the tracked mpv window (`WM_TRANSIENT_FOR`), so it stays above mpv while other foreground X11/Xwayland apps can still cover both windows. - While tracked mpv is fullscreen, SubMiner swaps the visible overlay to a focusable-false X11 override-redirect window. That path can stay above the active fullscreen mpv window without requiring a KDE/KWin-specific rule, and SubMiner hides/releases it when mpv is no longer the active X11/Xwayland window. - The visible overlay is shown inactive on Linux, so normal hover should not steal keyboard focus from mpv. diff --git a/src/core/utils/electron-backend.test.ts b/src/core/utils/electron-backend.test.ts index 561492b7..7058ef0a 100644 --- a/src/core/utils/electron-backend.test.ts +++ b/src/core/utils/electron-backend.test.ts @@ -1,6 +1,6 @@ import assert from 'node:assert/strict'; import test from 'node:test'; -import { shouldForceX11ElectronBackend } from './electron-backend'; +import { resolveX11ElectronRelaunchArgs, shouldForceX11ElectronBackend } from './electron-backend'; function withPlatform(platform: NodeJS.Platform, run: () => void): void { const original = Object.getOwnPropertyDescriptor(process, 'platform'); @@ -32,3 +32,70 @@ test('shouldForceX11ElectronBackend is false off Linux', () => { assert.equal(shouldForceX11ElectronBackend({}), false); }); }); + +test('resolveX11ElectronRelaunchArgs adds the raw X11 Ozone argument on unsupported Linux', () => { + assert.deepEqual( + resolveX11ElectronRelaunchArgs( + ['--start'], + { + DISPLAY: ':1', + WAYLAND_DISPLAY: 'wayland-0', + XDG_CURRENT_DESKTOP: 'KDE', + }, + 'linux', + ), + ['--start', '--ozone-platform=x11'], + ); +}); + +test('resolveX11ElectronRelaunchArgs avoids loops and preserves native Wayland backends', () => { + const kdeWayland = { + DISPLAY: ':1', + WAYLAND_DISPLAY: 'wayland-0', + XDG_CURRENT_DESKTOP: 'KDE', + }; + assert.equal( + resolveX11ElectronRelaunchArgs(['--start', '--ozone-platform=x11'], kdeWayland, 'linux'), + null, + ); + assert.equal( + resolveX11ElectronRelaunchArgs( + ['--start'], + { ...kdeWayland, HYPRLAND_INSTANCE_SIGNATURE: 'hypr' }, + 'linux', + ), + null, + ); + assert.equal(resolveX11ElectronRelaunchArgs(['--start'], kdeWayland, 'darwin'), null); + assert.equal( + resolveX11ElectronRelaunchArgs( + [], + { + ...kdeWayland, + SUBMINER_APP_ARGC: '1', + SUBMINER_APP_ARG_0: '--start', + }, + 'linux', + )?.at(-1), + '--ozone-platform=x11', + ); + assert.equal( + resolveX11ElectronRelaunchArgs([], { ...kdeWayland, SUBMINER_X11_BOOTSTRAPPED: '1' }, 'linux'), + null, + ); +}); + +test('resolveX11ElectronRelaunchArgs replaces an explicit unsupported Wayland argument', () => { + assert.deepEqual( + resolveX11ElectronRelaunchArgs( + ['--start', '--ozone-platform', 'wayland'], + { + DISPLAY: ':1', + WAYLAND_DISPLAY: 'wayland-0', + XDG_CURRENT_DESKTOP: 'KDE', + }, + 'linux', + ), + ['--start', '--ozone-platform=x11'], + ); +}); diff --git a/src/core/utils/electron-backend.ts b/src/core/utils/electron-backend.ts index e366a15c..12618c18 100644 --- a/src/core/utils/electron-backend.ts +++ b/src/core/utils/electron-backend.ts @@ -4,6 +4,9 @@ import { isSupportedWaylandCompositor } from '../../shared/mpv-x11-backend'; const logger = createLogger('core:electron-backend'); +export const X11_ELECTRON_BOOTSTRAP_ENV = 'SUBMINER_X11_BOOTSTRAPPED'; +const X11_ELECTRON_OZONE_ARG = '--ozone-platform=x11'; + function getElectronOzonePlatformHint(env: NodeJS.ProcessEnv = process.env): string | null { const hint = env.ELECTRON_OZONE_PLATFORM_HINT?.trim().toLowerCase(); if (hint) return hint; @@ -24,11 +27,42 @@ function getElectronOzonePlatformHint(env: NodeJS.ProcessEnv = process.env): str * Electron Wayland backend is unsupported); the Hyprland/Sway case is left untouched so * {@link enforceUnsupportedWaylandMode} can report it. */ -export function shouldForceX11ElectronBackend(env: NodeJS.ProcessEnv = process.env): boolean { - if (process.platform !== 'linux') return false; +export function shouldForceX11ElectronBackend( + env: NodeJS.ProcessEnv = process.env, + platform: NodeJS.Platform = process.platform, +): boolean { + if (platform !== 'linux') return false; return !isSupportedWaylandCompositor(env); } +export function resolveX11ElectronRelaunchArgs( + args: string[], + env: NodeJS.ProcessEnv = process.env, + platform: NodeJS.Platform = process.platform, +): string[] | null { + if (!shouldForceX11ElectronBackend(env, platform)) return null; + if (env[X11_ELECTRON_BOOTSTRAP_ENV] === '1') return null; + + const retainedArgs: string[] = []; + let alreadyForced = false; + for (let index = 0; index < args.length; index += 1) { + const arg = args[index]; + if (arg === '--ozone-platform') { + const value = args[index + 1]; + alreadyForced = value?.trim().toLowerCase() === 'x11'; + if (value && !value.startsWith('--')) index += 1; + continue; + } + if (arg?.startsWith('--ozone-platform=')) { + alreadyForced = arg.slice('--ozone-platform='.length).trim().toLowerCase() === 'x11'; + continue; + } + if (arg) retainedArgs.push(arg); + } + + return alreadyForced ? null : [...retainedArgs, X11_ELECTRON_OZONE_ARG]; +} + export function forceX11Backend(args: CliArgs): void { if (!shouldStartApp(args)) return; if (!shouldForceX11ElectronBackend()) return; diff --git a/src/main-entry-runtime.test.ts b/src/main-entry-runtime.test.ts index 2fbcf1e3..a41a100e 100644 --- a/src/main-entry-runtime.test.ts +++ b/src/main-entry-runtime.test.ts @@ -25,8 +25,24 @@ import { applyBackgroundBootstrapCommandLineSwitches, applyEarlyLinuxCommandLineSwitches, resolveLinuxPasswordStoreValue, + spawnDetachedApp, } from './main-entry-runtime'; +test('detached app launch policy stays in the startup runtime utilities', () => { + const entrySource = fs.readFileSync(path.join(process.cwd(), 'src/main-entry.ts'), 'utf8'); + const runtimeSource = fs.readFileSync( + path.join(process.cwd(), 'src/main-entry-runtime.ts'), + 'utf8', + ); + + assert.equal(typeof spawnDetachedApp, 'function'); + assert.doesNotMatch(entrySource, /function spawnDetachedApp/); + assert.match( + runtimeSource, + /child\.once\('error', \(error\) => \{\s*console\.error\([^;]*error\);\s*\}\);\s*child\.unref\(\)/, + ); +}); + test('background bootstrap exits through Electron so Chromium children shut down', () => { const exitCodes: number[] = []; exitBackgroundBootstrap({ exit: (code) => exitCodes.push(code) }); diff --git a/src/main-entry-runtime.ts b/src/main-entry-runtime.ts index 6854ff5f..34bcfe10 100644 --- a/src/main-entry-runtime.ts +++ b/src/main-entry-runtime.ts @@ -1,7 +1,9 @@ import fs from 'node:fs'; import os from 'node:os'; +import { spawn } from 'node:child_process'; import { CliArgs, hasExplicitCommand, parseArgs, shouldStartApp } from './cli/args'; import { resolveConfigDir } from './config/path-resolution'; +import { resolveAppImageMountKeepaliveInvocation } from './main/appimage-mount-keepalive'; const BACKGROUND_ARG = '--background'; const START_ARG = '--start'; @@ -265,6 +267,25 @@ export function exitBackgroundBootstrap(app: BackgroundBootstrapAppLike): void { app.exit(0); } +export function spawnDetachedApp(childArgs: string[], env: NodeJS.ProcessEnv): void { + const keepalive = resolveAppImageMountKeepaliveInvocation(env); + const child = keepalive + ? spawn(keepalive.command, [...keepalive.args, ...childArgs], { + detached: true, + stdio: 'ignore', + env, + }) + : spawn(process.execPath, childArgs, { + detached: true, + stdio: 'ignore', + env, + }); + child.once('error', (error) => { + console.error('Failed to spawn detached SubMiner app:', error); + }); + child.unref(); +} + export function shouldHandleHelpOnlyAtEntry(argv: string[], env: NodeJS.ProcessEnv): boolean { if (env.ELECTRON_RUN_AS_NODE === '1') return false; const args = parseCliArgs(argv); diff --git a/src/main-entry.ts b/src/main-entry.ts index 5f7e5864..4b53efbc 100644 --- a/src/main-entry.ts +++ b/src/main-entry.ts @@ -1,5 +1,4 @@ import os from 'node:os'; -import { spawn } from 'node:child_process'; import { app, dialog, shell } from 'electron'; import { printHelp } from './cli/help'; import { @@ -20,9 +19,9 @@ import { shouldHandleHelpOnlyAtEntry, shouldHandleLaunchMpvAtEntry, shouldHandleStatsDaemonCommandAtEntry, + spawnDetachedApp, } from './main-entry-runtime'; import { requestSingleInstanceLockEarly } from './main/early-single-instance'; -import { resolveAppImageMountKeepaliveInvocation } from './main/appimage-mount-keepalive'; import { readConfiguredWindowsMpvLaunch } from './main-entry-launch-config'; import { isAppControlServerAvailable, sendAppControlCommand } from './shared/app-control-client'; import { @@ -44,6 +43,10 @@ import { resolveDefaultLogFilePath, type LogRotation, } from './shared/log-files'; +import { + resolveX11ElectronRelaunchArgs, + X11_ELECTRON_BOOTSTRAP_ENV, +} from './core/utils/electron-backend'; const DEFAULT_TEXTHOOKER_PORT = 5174; @@ -296,26 +299,26 @@ async function runEntryProcess(): Promise { return; } + const childArgs = hasTransportedStartupArgs(process.env) ? [] : process.argv.slice(1); + const x11ChildArgs = resolveX11ElectronRelaunchArgs(childArgs, process.env); + if (shouldDetachBackgroundLaunch(process.argv, process.env)) { - const childArgs = hasTransportedStartupArgs(process.env) ? [] : process.argv.slice(1); - const keepalive = resolveAppImageMountKeepaliveInvocation(process.env); - const child = keepalive - ? spawn(keepalive.command, [...keepalive.args, ...childArgs], { - detached: true, - stdio: 'ignore', - env: sanitizeBackgroundEnv(process.env), - }) - : spawn(process.execPath, childArgs, { - detached: true, - stdio: 'ignore', - env: sanitizeBackgroundEnv(process.env), - }); - child.unref(); + const childEnv = sanitizeBackgroundEnv(process.env); + if (x11ChildArgs) childEnv[X11_ELECTRON_BOOTSTRAP_ENV] = '1'; + spawnDetachedApp(x11ChildArgs ?? childArgs, childEnv); // Let Electron stop bootstrap Chromium children before its AppImage mount is released. exitBackgroundBootstrap(app); return; } + if (x11ChildArgs) { + const childEnv = sanitizeStartupEnv(process.env); + childEnv[X11_ELECTRON_BOOTSTRAP_ENV] = '1'; + spawnDetachedApp(x11ChildArgs, childEnv); + exitBackgroundBootstrap(app); + return; + } + startMainProcess(); } diff --git a/src/main.ts b/src/main.ts index eaa7e152..0d405a54 100644 --- a/src/main.ts +++ b/src/main.ts @@ -256,7 +256,6 @@ import { import { enforceUnsupportedWaylandMode, forceX11Backend, - shouldForceX11ElectronBackend, generateDefaultConfigFile, resolveConfiguredShortcuts, resolveKeybindings, @@ -597,15 +596,6 @@ if (process.platform === 'linux') { ); app.commandLine.appendSwitch('password-store', passwordStore); createLogger('main').debug(`Applied --password-store ${passwordStore}`); - // Pin the overlay to XWayland on unsupported Wayland sessions (everything except - // Hyprland/Sway). `setAlwaysOnTop`/`moveTop` are no-ops under a native Wayland surface, - // so the overlay can only stay above mpv under X11/XWayland. The command-line switch is - // applied at module load (before app init) so it reliably wins over the late env-var - // fallback in forceX11Backend(). - if (shouldForceX11ElectronBackend(process.env)) { - app.commandLine.appendSwitch('ozone-platform-hint', 'x11'); - createLogger('main').debug('Forced ozone-platform-hint=x11 for XWayland overlay stacking'); - } } app.setName('SubMiner'); diff --git a/src/main/runtime/linux-x11-cursor-point.test.ts b/src/main/runtime/linux-x11-cursor-point.test.ts index 18952cd3..9e7a0b13 100644 --- a/src/main/runtime/linux-x11-cursor-point.test.ts +++ b/src/main/runtime/linux-x11-cursor-point.test.ts @@ -47,6 +47,29 @@ WINDOW=44040194 assert.deepEqual(reader.getCursorScreenPoint({ x: 877, y: 718 }), { x: 1700, y: 1050 }); }); +test('createLinuxX11CursorPointReader converts physical X11 coordinates to Electron DIP', async () => { + const convertedPoints: Array<{ x: number; y: number }> = []; + const reader = createLinuxX11CursorPointReader({ + env: { DISPLAY: ':1' }, + platform: 'linux', + runCommand: async () => `X=1424 +Y=697 +SCREEN=0 +WINDOW=44040194 +`, + screenToDipPoint: (point) => { + convertedPoints.push(point); + return { x: 1139, y: 557 }; + }, + }); + + reader.refresh(); + await new Promise((resolve) => setImmediate(resolve)); + + assert.deepEqual(reader.getCursorScreenPoint({ x: 0, y: 0 }), { x: 1139, y: 557 }); + assert.deepEqual(convertedPoints, [{ x: 1424, y: 697 }]); +}); + test('createLinuxX11CursorPointReader does not spawn off X11 Linux', () => { const calls: string[] = []; const reader = createLinuxX11CursorPointReader({ diff --git a/src/main/runtime/linux-x11-cursor-point.ts b/src/main/runtime/linux-x11-cursor-point.ts index c9d14557..1d7c0494 100644 --- a/src/main/runtime/linux-x11-cursor-point.ts +++ b/src/main/runtime/linux-x11-cursor-point.ts @@ -36,11 +36,13 @@ export function createLinuxX11CursorPointReader(options?: { now?: () => number; platform?: NodeJS.Platform; runCommand?: CommandRunner; + screenToDipPoint?: (point: PointerPoint) => PointerPoint; }) { const env = options?.env ?? process.env; const now = options?.now ?? (() => Date.now()); const platform = options?.platform ?? process.platform; const runCommand = options?.runCommand ?? execFileUtf8; + const screenToDipPoint = options?.screenToDipPoint ?? ((point: PointerPoint) => point); let latest: { point: PointerPoint; updatedAtMs: number } | null = null; let inFlight = false; let retryAfterMs = 0; @@ -63,7 +65,7 @@ export function createLinuxX11CursorPointReader(options?: { retryAfterMs = now() + COMMAND_FAILURE_RETRY_DELAY_MS; return; } - latest = { point, updatedAtMs: now() }; + latest = { point: screenToDipPoint(point), updatedAtMs: now() }; retryAfterMs = 0; }) .catch(() => { diff --git a/src/main/runtime/visible-overlay-interaction-runtime.ts b/src/main/runtime/visible-overlay-interaction-runtime.ts index 615becd8..c5ca1d9d 100644 --- a/src/main/runtime/visible-overlay-interaction-runtime.ts +++ b/src/main/runtime/visible-overlay-interaction-runtime.ts @@ -289,7 +289,9 @@ export function createVisibleOverlayInteractionRuntime(deps: VisibleOverlayInter if (initialArgs && isHeadlessInitialCommand(initialArgs)) { return null; } - return createWindowTrackerCore(override, targetMpvSocketPath); + return createWindowTrackerCore(override, targetMpvSocketPath, (point) => + screen.screenToDipPoint(point), + ); } function bindVisibleOverlayOwner(): void { @@ -627,7 +629,9 @@ export function createVisibleOverlayInteractionRuntime(deps: VisibleOverlayInter ensureWindowsVisibleOverlayForegroundPollLoop(); - const linuxX11CursorPointReader = createLinuxX11CursorPointReader(); + const linuxX11CursorPointReader = createLinuxX11CursorPointReader({ + screenToDipPoint: (point) => screen.screenToDipPoint(point), + }); function getLinuxOverlayPointerMeasurement() { const measurement = overlayContentMeasurementStore.getLatestByLayer('visible'); diff --git a/src/window-trackers/index.ts b/src/window-trackers/index.ts index 9406419b..9cbcb66d 100644 --- a/src/window-trackers/index.ts +++ b/src/window-trackers/index.ts @@ -20,6 +20,7 @@ import { BaseWindowTracker } from './base-tracker'; import { HyprlandWindowTracker } from './hyprland-tracker'; import { SwayWindowTracker } from './sway-tracker'; import { X11WindowTracker } from './x11-tracker'; +import type { ScreenToDipPoint } from './x11-tracker'; import { MacOSWindowTracker } from './macos-tracker'; import { WindowsWindowTracker } from './windows-tracker'; import { createLogger } from '../logger'; @@ -51,6 +52,7 @@ function normalizeCompositor(value: string): Compositor | null { export function createWindowTracker( override?: string | null, targetMpvSocketPath?: string | null, + screenToDipPoint?: ScreenToDipPoint, ): BaseWindowTracker | null { let compositor = detectCompositor(); @@ -70,7 +72,11 @@ export function createWindowTracker( case 'sway': return new SwayWindowTracker(targetMpvSocketPath?.trim() || undefined); case 'x11': - return new X11WindowTracker(targetMpvSocketPath?.trim() || undefined); + return new X11WindowTracker( + targetMpvSocketPath?.trim() || undefined, + undefined, + screenToDipPoint, + ); case 'macos': return new MacOSWindowTracker(targetMpvSocketPath?.trim() || undefined); case 'windows': diff --git a/src/window-trackers/x11-tracker.test.ts b/src/window-trackers/x11-tracker.test.ts index f4f7eeb8..9103b519 100644 --- a/src/window-trackers/x11-tracker.test.ts +++ b/src/window-trackers/x11-tracker.test.ts @@ -82,6 +82,47 @@ Height: 360`; }); }); +test('X11WindowTracker converts both physical rectangle corners to Electron DIP', async () => { + const convertedPoints: Array<{ x: number; y: number }> = []; + const tracker = new X11WindowTracker( + undefined, + async (command, args) => { + if (command === 'xdotool' && args[0] === 'search') { + return '123'; + } + if (command === 'xdotool' && args[0] === 'getactivewindow') { + return '123'; + } + if (command === 'xwininfo') { + return `Absolute upper-left X: 2000 +Absolute upper-left Y: 125 +Width: 1000 +Height: 750`; + } + return ''; + }, + (point) => { + convertedPoints.push(point); + if (point.x === 2000) return { x: 1600, y: 100 }; + return { x: 2400, y: 700 }; + }, + ); + + (tracker as unknown as { pollGeometry: () => void }).pollGeometry(); + await new Promise((resolve) => setTimeout(resolve, 0)); + + assert.deepEqual(convertedPoints, [ + { x: 2000, y: 125 }, + { x: 3000, y: 875 }, + ]); + assert.deepEqual(tracker.getGeometry(), { + x: 1600, + y: 100, + width: 800, + height: 600, + }); +}); + test('X11WindowTracker updates target focus from active X11 window', async () => { let activeWindowId = '999'; const tracker = new X11WindowTracker(undefined, async (command, args) => { diff --git a/src/window-trackers/x11-tracker.ts b/src/window-trackers/x11-tracker.ts index 6a83c7a2..5e6d3b91 100644 --- a/src/window-trackers/x11-tracker.ts +++ b/src/window-trackers/x11-tracker.ts @@ -20,6 +20,9 @@ import { execFile } from 'child_process'; import { BaseWindowTracker } from './base-tracker'; type CommandRunner = (command: string, args: string[]) => Promise; +export type ScreenToDipPoint = (point: { x: number; y: number }) => { x: number; y: number }; + +const preservePoint: ScreenToDipPoint = (point) => point; function execFileUtf8(command: string, args: string[]): Promise { return new Promise((resolve, reject) => { @@ -87,16 +90,22 @@ export class X11WindowTracker extends BaseWindowTracker { private pollInterval: ReturnType | null = null; private readonly targetMpvSocketPath: string | null; private readonly runCommand: CommandRunner; + private readonly screenToDipPoint: ScreenToDipPoint; private targetWindowId: string | null = null; private targetWindowPid: number | null = null; private pollInFlight = false; private currentPollIntervalMs = 750; private readonly stablePollIntervalMs = 250; - constructor(targetMpvSocketPath?: string, runCommand: CommandRunner = execFileUtf8) { + constructor( + targetMpvSocketPath?: string, + runCommand: CommandRunner = execFileUtf8, + screenToDipPoint: ScreenToDipPoint = preservePoint, + ) { super(); this.targetMpvSocketPath = targetMpvSocketPath?.trim() || null; this.runCommand = runCommand; + this.screenToDipPoint = screenToDipPoint; } start(): void { @@ -196,11 +205,25 @@ export class X11WindowTracker extends BaseWindowTracker { this.targetWindowPid = targetPid; const winInfo = await this.runCommand('xwininfo', ['-id', windowId]); - const geometry = parseX11WindowGeometry(winInfo); - if (!geometry) { + const physicalGeometry = parseX11WindowGeometry(winInfo); + if (!physicalGeometry) { this.updateGeometry(null); return; } + const topLeft = this.screenToDipPoint({ + x: physicalGeometry.x, + y: physicalGeometry.y, + }); + const bottomRight = this.screenToDipPoint({ + x: physicalGeometry.x + physicalGeometry.width, + y: physicalGeometry.y + physicalGeometry.height, + }); + const geometry = { + x: topLeft.x, + y: topLeft.y, + width: bottomRight.x - topLeft.x, + height: bottomRight.y - topLeft.y, + }; const focused = await this.isWindowActive(windowId, targetPid); this.updateGeometry(geometry, focused);