From 73baf41f6ff7f327a03d5ed07305a7940a5ef01a Mon Sep 17 00:00:00 2001 From: ienaga Date: Sun, 1 Nov 2026 17:29:48 +0900 Subject: [PATCH 1/3] =?UTF-8?q?overlay:=20Windows=20=E3=81=A7=E6=9C=80?= =?UTF-8?q?=E5=89=8D=E9=9D=A2=E3=82=92=E5=AE=9A=E6=9C=9F=E7=9A=84=E3=81=AB?= =?UTF-8?q?=E5=8F=96=E3=82=8A=E7=9B=B4=E3=81=97=E3=80=81=E3=83=87=E3=82=A3?= =?UTF-8?q?=E3=82=B9=E3=83=97=E3=83=AC=E3=82=A4=E5=A4=89=E6=9B=B4=E3=83=BB?= =?UTF-8?q?=E5=BE=A9=E5=B8=B0=E6=99=82=E3=81=AB=E3=82=82=E6=9C=80=E5=89=8D?= =?UTF-8?q?=E9=9D=A2=E3=81=AB=E6=88=BB=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- templates/electron/index.js | 19 ++++++++++++++++--- tests/electron-overlay.test.ts | 26 +++++++++++++++++++++++++- 3 files changed, 42 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 4d28072..f032082 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@next2d/builder", - "version": "0.4.2", + "version": "0.4.3", "description": "Multi-platform builder for Next2d Framework, supporting export to various platforms such as macOS, Windows, iOS, Android and Web(HTML)", "author": "Toshiyuki Ienaga ", "license": "MIT", diff --git a/templates/electron/index.js b/templates/electron/index.js index 73608f3..db980e4 100644 --- a/templates/electron/index.js +++ b/templates/electron/index.js @@ -51,6 +51,13 @@ const overlayBounds = () => { const placeOverlay = () => { if (mainWindow && !mainWindow.isDestroyed()) mainWindow.setBounds(overlayBounds()); }; +// The overlay never takes focus, so Windows does not raise it when another topmost window (the taskbar, another +// always-on-top app) is activated, nor after the display is turned off and on. Put it back on top. +const keepOnTop = () => { + if (!mainWindow || mainWindow.isDestroyed()) return; + mainWindow.setAlwaysOnTop(true, "floating"); + mainWindow.moveTop(); +}; const createWindow = async () => { const { width, height } = screen.getPrimaryDisplay().workAreaSize; // The preload reads these flags; each API is exposed only when its feature is configured. @@ -89,10 +96,16 @@ const createWindow = async () => { mainWindow.setIgnoreMouseEvents(true, { forward: true }); } const events = ["display-metrics-changed", "display-added", "display-removed"]; - for (const name of events) screen.on(name, placeOverlay); + const onDisplayChange = () => { placeOverlay(); keepOnTop(); }; + for (const name of events) screen.on(name, onDisplayChange); mainWindow.once("closed", () => { - for (const name of events) screen.removeListener(name, placeOverlay); + for (const name of events) screen.removeListener(name, onDisplayChange); }); + if (process.platform === "win32") { + const timer = setInterval(keepOnTop, 3000); + timer.unref?.(); + mainWindow.once("closed", () => clearInterval(timer)); + } } mainWindow.webContents.setWindowOpenHandler(() => ({ action: "deny" })); mainWindow.webContents.on("will-navigate", (event, url) => { @@ -180,7 +193,7 @@ app.whenReady().then(async () => { // z-order/bounds. Apply the overlay state again and tell the page so it can recover too. const recoverOverlay = () => { if (!mainWindow || mainWindow.isDestroyed()) return; - mainWindow.setAlwaysOnTop(true, "floating"); + keepOnTop(); placeOverlay(); mainWindow.setIgnoreMouseEvents(false); mainWindow.setIgnoreMouseEvents(overlayIgnore, overlayIgnore ? { forward: true } : undefined); diff --git a/tests/electron-overlay.test.ts b/tests/electron-overlay.test.ts index 2f91281..ae25757 100644 --- a/tests/electron-overlay.test.ts +++ b/tests/electron-overlay.test.ts @@ -53,8 +53,9 @@ const fakeElectron = (options = {}) => { isFullScreen() { return false; } isDestroyed() { return false; } isVisible() { return true; } + moveTop() { calls.push(["moveTop"]); } showInactive() { calls.push(["showInactive"]); } - once() {} + once(name, cb) { this.onceListeners = { ...this.onceListeners, [name]: cb }; } async loadURL() {} } const electron = { @@ -244,3 +245,26 @@ test("a game without the overlay is not limited to one instance", async (t) => { assert.equal(calls.some(([name]) => name === "requestSingleInstanceLock"), false); assert.equal(appListeners.has("second-instance"), false); }); + +test("on Windows the overlay is put back on top every few seconds and when the displays change", async (t) => { + t.mock.timers.enable({ apis: ["setInterval", "setTimeout"] }); + const platform = Object.getOwnPropertyDescriptor(process, "platform"); + Object.defineProperty(process, "platform", { value: "win32" }); + t.after(() => Object.defineProperty(process, "platform", platform)); + const { calls, listeners } = await startHost(t, { window: { overlay: { enabled: true, clickThrough: true } } }); + calls.length = 0; + t.mock.timers.tick(3000); + assert.deepEqual(calls.filter(([name]) => /^(setAlwaysOnTop|moveTop)$/.test(name)), [["setAlwaysOnTop", true, "floating"], ["moveTop"]]); + calls.length = 0; + listeners.get("display-added")(); + assert.deepEqual(calls.filter(([name]) => /^(setAlwaysOnTop|moveTop)$/.test(name)), [["setAlwaysOnTop", true, "floating"], ["moveTop"]]); +}); + +test("on other platforms there is no periodic raise", async (t) => { + t.mock.timers.enable({ apis: ["setInterval", "setTimeout"] }); + if (process.platform === "win32") return; + const { calls } = await startHost(t, { window: { overlay: { enabled: true, clickThrough: true } } }); + calls.length = 0; + t.mock.timers.tick(10000); + assert.equal(calls.some(([name]) => name === "moveTop"), false); +}); From 098230ee19f1f1e8d1c1e98daf06ba62696dd6c9 Mon Sep 17 00:00:00 2001 From: ienaga Date: Sun, 1 Nov 2026 17:34:19 +0900 Subject: [PATCH 2/3] =?UTF-8?q?overlay:=20Windows=20=E3=81=A7=E3=80=81?= =?UTF-8?q?=E3=82=AF=E3=83=AA=E3=83=83=E3=82=AF=E9=80=8F=E9=81=8E=E4=B8=AD?= =?UTF-8?q?=E3=81=AE=E3=83=9E=E3=82=A6=E3=82=B9=E8=BB=A2=E9=80=81=E3=81=AE?= =?UTF-8?q?=E3=83=95=E3=83=83=E3=82=AF=E3=82=92=E6=95=B0=E7=A7=92=E3=81=94?= =?UTF-8?q?=E3=81=A8=E3=81=AB=E5=85=A5=E3=82=8C=E7=9B=B4=E3=81=99=EF=BC=88?= =?UTF-8?q?=E3=82=B9=E3=83=AA=E3=83=BC=E3=83=97=E5=BE=8C=E3=81=AB=E5=A4=96?= =?UTF-8?q?=E3=82=8C=E3=81=A6=E3=82=82=E5=9B=9E=E5=BE=A9=E3=81=99=E3=82=8B?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- templates/electron/index.js | 10 +++++++++- tests/electron-overlay.test.ts | 9 ++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/templates/electron/index.js b/templates/electron/index.js index db980e4..1b2f4cc 100644 --- a/templates/electron/index.js +++ b/templates/electron/index.js @@ -58,6 +58,14 @@ const keepOnTop = () => { mainWindow.setAlwaysOnTop(true, "floating"); mainWindow.moveTop(); }; +// Windows forwards mouse moves to a click-through window with a system mouse hook, and Electron installs it only +// when forwarding switches on. After sleep (closing the lid) the hook can be gone while Electron still thinks it is +// installed, so the page never sees the pointer. Switching forwarding off and on again installs a fresh hook. +const refreshForwarding = () => { + if (!overlayIgnore || !mainWindow || mainWindow.isDestroyed()) return; + mainWindow.setIgnoreMouseEvents(false); + mainWindow.setIgnoreMouseEvents(true, { forward: true }); +}; const createWindow = async () => { const { width, height } = screen.getPrimaryDisplay().workAreaSize; // The preload reads these flags; each API is exposed only when its feature is configured. @@ -102,7 +110,7 @@ const createWindow = async () => { for (const name of events) screen.removeListener(name, onDisplayChange); }); if (process.platform === "win32") { - const timer = setInterval(keepOnTop, 3000); + const timer = setInterval(() => { keepOnTop(); refreshForwarding(); }, 3000); timer.unref?.(); mainWindow.once("closed", () => clearInterval(timer)); } diff --git a/tests/electron-overlay.test.ts b/tests/electron-overlay.test.ts index ae25757..48afaca 100644 --- a/tests/electron-overlay.test.ts +++ b/tests/electron-overlay.test.ts @@ -251,10 +251,17 @@ test("on Windows the overlay is put back on top every few seconds and when the d const platform = Object.getOwnPropertyDescriptor(process, "platform"); Object.defineProperty(process, "platform", { value: "win32" }); t.after(() => Object.defineProperty(process, "platform", platform)); - const { calls, listeners } = await startHost(t, { window: { overlay: { enabled: true, clickThrough: true } } }); + const { calls, listeners, handlers, windows } = await startHost(t, { window: { overlay: { enabled: true, clickThrough: true } } }); calls.length = 0; t.mock.timers.tick(3000); assert.deepEqual(calls.filter(([name]) => /^(setAlwaysOnTop|moveTop)$/.test(name)), [["setAlwaysOnTop", true, "floating"], ["moveTop"]]); + // Click-through is on: the mouse-forwarding hook is installed again (off, then on). + assert.deepEqual(calls.filter(([name]) => name === "setIgnoreMouseEvents"), [["setIgnoreMouseEvents", false], ["setIgnoreMouseEvents", true, { forward: true }]]); + // While the page captures the mouse (pointer over the pet) nothing is toggled under it. + handlers.get("next2d:window:set-ignore-mouse-events")({ sender: windows[0].webContents, senderFrame: Object.assign(windows[0].webContents.mainFrame, { url: "next2d://game/index.html" }) }, false); + calls.length = 0; + t.mock.timers.tick(3000); + assert.equal(calls.some(([name]) => name === "setIgnoreMouseEvents"), false); calls.length = 0; listeners.get("display-added")(); assert.deepEqual(calls.filter(([name]) => /^(setAlwaysOnTop|moveTop)$/.test(name)), [["setAlwaysOnTop", true, "floating"], ["moveTop"]]); From 375877d6d570391830252bc9482b6a9cbd6e3a0a Mon Sep 17 00:00:00 2001 From: ienaga Date: Sun, 1 Nov 2026 17:46:18 +0900 Subject: [PATCH 3/3] update packages --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index eb19a81..4f89204 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@next2d/builder", - "version": "0.4.2", + "version": "0.4.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@next2d/builder", - "version": "0.4.2", + "version": "0.4.3", "license": "MIT", "bin": { "builder": "dist/index.js"