diff --git a/electron/main.ts b/electron/main.ts index 89d05051..1cfa1c07 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -8,8 +8,8 @@ import { dialog, ipcMain, Menu, - nativeImage, Notification, + nativeImage, session, systemPreferences, Tray, @@ -22,27 +22,28 @@ import { killWindowsCaptureProcess, registerIpcHandlers, } from "./ipc/handlers"; +import type { UpdateToastPayload } from "./updater"; import { checkForAppUpdates, + deferUpdateReminder, dismissUpdateToast, downloadAvailableUpdate, - deferUpdateReminder, getCurrentUpdateToastPayload, getUpdaterLogPath, getUpdateStatusSummary, installDownloadedUpdateNow, previewUpdateToast, - skipAvailableUpdateVersion, setupAutoUpdates, + skipAvailableUpdateVersion, } from "./updater"; -import type { UpdateToastPayload } from "./updater"; import { createEditorWindow, createHudOverlayWindow, createSourceSelectorWindow, - getUpdateToastWindow, getHudOverlayWindow, + getUpdateToastWindow, hideUpdateToastWindow, + isHudOverlayMousePassthroughSupported, showUpdateToastWindow, } from "./windows"; @@ -76,14 +77,8 @@ async function logSmokeExportGpuDiagnostics() { } try { - console.log( - "[smoke-export] GPU feature status", - JSON.stringify(app.getGPUFeatureStatus()), - ); - console.log( - "[smoke-export] GPU info", - JSON.stringify(await app.getGPUInfo("basic")), - ); + console.log("[smoke-export] GPU feature status", JSON.stringify(app.getGPUFeatureStatus())); + console.log("[smoke-export] GPU info", JSON.stringify(await app.getGPUInfo("basic"))); } catch (error) { console.warn("[smoke-export] Failed to read GPU diagnostics:", error); } @@ -207,11 +202,17 @@ function focusOrCreateMainWindow() { return; } - // On Win32, calling show/moveTop/focus on the transparent HUD overlay - // permanently corrupts setIgnoreMouseEvents forwarding, making it - // click-through. Only focus the editor window; the HUD is alwaysOnTop - // so it doesn't need explicit focus. - if (process.platform === "win32" && !isEditorWindow(mainWindow)) { + // On Win32 with mouse passthrough enabled (Win11+), calling + // show/moveTop/focus on the transparent HUD overlay permanently corrupts + // setIgnoreMouseEvents forwarding, making it click-through. Only focus + // the editor window; the HUD is alwaysOnTop so it doesn't need explicit + // focus. On Win10 (passthrough disabled), the HUD is always interactive + // and can be safely shown/restored. + if ( + process.platform === "win32" && + !isEditorWindow(mainWindow) && + isHudOverlayMousePassthroughSupported() + ) { return; } @@ -229,7 +230,7 @@ function focusOrCreateMainWindow() { * operation that may alter focus or z-order so that hover detection keeps working. */ function reassertHudOverlayMouseState() { - if (process.platform !== "win32") { + if (process.platform !== "win32" || !isHudOverlayMousePassthroughSupported()) { return; } @@ -448,7 +449,9 @@ function sendUpdateToastToWindows(channel: "update-toast-state", payload: unknow return false; } - const notificationKey = [updatePayload.phase, updatePayload.version, updatePayload.detail].join(":"); + const notificationKey = [updatePayload.phase, updatePayload.version, updatePayload.detail].join( + ":", + ); if (activeUpdateNotificationKey === notificationKey) { return true; } @@ -723,10 +726,14 @@ app.whenReady().then(async () => { const cameraStatus = systemPreferences.getMediaAccessStatus("camera"); const micStatus = systemPreferences.getMediaAccessStatus("microphone"); if (cameraStatus !== "granted") { - console.warn(`[permissions] Camera access is "${cameraStatus}" — webcam may not work. Check Windows Settings > Privacy > Camera.`); + console.warn( + `[permissions] Camera access is "${cameraStatus}" — webcam may not work. Check Windows Settings > Privacy > Camera.`, + ); } if (micStatus !== "granted") { - console.warn(`[permissions] Microphone access is "${micStatus}" — mic recording may not work. Check Windows Settings > Privacy > Microphone.`); + console.warn( + `[permissions] Microphone access is "${micStatus}" — mic recording may not work. Check Windows Settings > Privacy > Microphone.`, + ); } } diff --git a/electron/windows.ts b/electron/windows.ts index 2589e215..240d89f1 100644 --- a/electron/windows.ts +++ b/electron/windows.ts @@ -1,5 +1,6 @@ import fs from "node:fs"; import { createRequire } from "node:module"; +import os from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { app, BrowserWindow, ipcMain } from "electron"; @@ -96,8 +97,26 @@ function isHudOverlayCaptureProtectionSupported(): boolean { return process.platform !== "linux"; } -function isHudOverlayMousePassthroughSupported(): boolean { - return process.platform !== "linux"; +function getWindowsBuildNumber(): number | null { + if (process.platform !== "win32") { + return null; + } + + const build = Number.parseInt(os.release().split(".")[2] ?? "", 10); + return Number.isFinite(build) ? build : null; +} + +export function isHudOverlayMousePassthroughSupported(): boolean { + if (process.platform === "linux") { + return false; + } + + const build = getWindowsBuildNumber(); + if (build !== null && build < 22000) { + return false; + } + + return true; } function loadHudOverlayCaptureProtectionSetting(): boolean { @@ -138,7 +157,9 @@ function persistHudOverlayCaptureProtectionSetting(enabled: boolean): void { function getScreen() { if (!app.isReady()) { - throw new Error("getScreen() called before app is ready. Ensure all screen access happens after app.whenReady()."); + throw new Error( + "getScreen() called before app is ready. Ensure all screen access happens after app.whenReady().", + ); } return nodeRequire("electron").screen as typeof import("electron").screen; } @@ -405,12 +426,13 @@ export function createHudOverlayWindow(): BrowserWindow { win.setIgnoreMouseEvents(true, { forward: true }); } - // On Windows 10, focus changes (e.g. showing a native notification) can break + // On Windows 11+, focus changes (e.g. showing a native notification) can break // setIgnoreMouseEvents forwarding on a transparent always-on-top window, making // it permanently click-through without hover detection. Re-initialise the // pass-through-with-forwarding state whenever the window gains focus by toggling // the flag off then back on so the native WS_EX_TRANSPARENT flag is fully reset. - if (process.platform === "win32") { + // On Windows 10 (build < 22000) passthrough is disabled entirely, so skip this. + if (process.platform === "win32" && isHudOverlayMousePassthroughSupported()) { win.on("focus", () => { if (!win.isDestroyed()) { win.setIgnoreMouseEvents(false); @@ -429,7 +451,7 @@ export function createHudOverlayWindow(): BrowserWindow { if (!win.isDestroyed()) { win.show(); win.moveTop(); - if (process.platform === "win32") { + if (process.platform === "win32" && isHudOverlayMousePassthroughSupported()) { win.setIgnoreMouseEvents(false); setTimeout(() => { if (!win.isDestroyed()) {