@next2d/builder 0.4.6 → 0.4.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,6 +5,15 @@ export interface IElectronOverlayConfig {
5
5
  anchor: "bottom";
6
6
  clickThrough: boolean;
7
7
  }
8
+ export type ElectronMiniAnchor = "bottom-right" | "bottom-left" | "top-right" | "top-left";
9
+ /** A small always-on-top window the game can switch to and back from at run time (idle/companion modes). */
10
+ export interface IElectronMiniConfig {
11
+ width: number;
12
+ height: number;
13
+ anchor: ElectronMiniAnchor;
14
+ margin: number;
15
+ alwaysOnTop: boolean;
16
+ }
8
17
  export interface IElectronConfig {
9
18
  nativeBridge?: INativeBridgeConfig;
10
19
  appId: string;
@@ -19,6 +28,7 @@ export interface IElectronConfig {
19
28
  height: number;
20
29
  fullscreen: boolean;
21
30
  overlay?: IElectronOverlayConfig;
31
+ mini?: IElectronMiniConfig;
22
32
  };
23
33
  steam?: {
24
34
  appId: string | null;
@@ -3,6 +3,7 @@ import path from "node:path";
3
3
  import { getTemplateDir } from "./utils.js";
4
4
  import { resolveDescription } from "./description.js";
5
5
  import { validateNativeBridge } from "./electron-native.js";
6
+ const MINI_ANCHORS = ["bottom-right", "bottom-left", "top-right", "top-left"];
6
7
  export const validateSteamId = (value) => {
7
8
  const id = String(value);
8
9
  if (!/^[1-9][0-9]*$/.test(id) || !Number.isSafeInteger(Number(id)) || Number(id) > 4294967295) {
@@ -74,6 +75,32 @@ export const readElectronConfig = (root) => {
74
75
  config.window.fullscreen = false;
75
76
  }
76
77
  }
78
+ const mini = config.window.mini;
79
+ if (mini !== undefined) {
80
+ if (!mini || typeof mini !== "object" || Array.isArray(mini)) {
81
+ throw new Error("electron.config.json window.mini must be an object.");
82
+ }
83
+ config.window.mini = { "width": 720, "height": 180, "anchor": "bottom-right", "margin": 16, "alwaysOnTop": true, ...mini };
84
+ const { width, height, anchor, margin, alwaysOnTop } = config.window.mini;
85
+ for (const dimension of [width, height]) {
86
+ if (!Number.isInteger(dimension) || dimension < 1 || dimension > 16384) {
87
+ throw new Error("electron.config.json window.mini.width/height must be integers between 1 and 16384.");
88
+ }
89
+ }
90
+ if (!MINI_ANCHORS.includes(anchor)) {
91
+ throw new Error(`electron.config.json window.mini.anchor must be one of ${MINI_ANCHORS.map((a) => `"${a}"`).join(", ")}.`);
92
+ }
93
+ if (!Number.isInteger(margin) || margin < 0 || margin > 1000) {
94
+ throw new Error("electron.config.json window.mini.margin must be an integer between 0 and 1000.");
95
+ }
96
+ if (typeof alwaysOnTop !== "boolean") {
97
+ throw new Error("electron.config.json window.mini.alwaysOnTop must be a boolean.");
98
+ }
99
+ // The overlay already is the small always-on-top window, and it never takes keyboard focus.
100
+ if (config.window.overlay?.enabled) {
101
+ throw new Error("electron.config.json window.mini cannot be combined with window.overlay.enabled.");
102
+ }
103
+ }
77
104
  if (config.macos.notarize && !config.macos.sign) {
78
105
  throw new Error("macos.notarize requires macos.sign.");
79
106
  }
@@ -132,8 +159,7 @@ export const createElectronPackagerOptions = (root, config, os, resources) => {
132
159
  ...os === "macos" && (config.macos.localNetworkUsageDescription || config.window.overlay?.enabled) ? {
133
160
  "extendInfo": {
134
161
  ...config.macos.localNetworkUsageDescription ? { "NSLocalNetworkUsageDescription": config.macos.localNetworkUsageDescription } : {},
135
- // An overlay starts as an agent app (no Dock icon) so that a second launch, which quits at once,
136
- // never shows one. The first instance shows its Dock icon itself (templates/electron/index.js).
162
+ // An overlay is an agent app: no Dock icon, also for a second launch that quits at once.
137
163
  ...config.window.overlay?.enabled ? { "LSUIElement": true } : {}
138
164
  },
139
165
  ...config.macos.localNetworkUsageDescription ? {
package/docs/steam.md CHANGED
@@ -105,12 +105,44 @@ await window.next2dWindow.quit();
105
105
  const off = window.next2dWindow.onResume(() => { /* 状態の再送など */ });
106
106
  ```
107
107
 
108
- 重ね表示が有効なときは、アプリは**1つだけ**起動します(2つ目の起動はすぐに終了します。macOSではDockにアイコンも出しません)。開発中に複数起動したいときは `--next2d-allow-multiple` を付けて起動します。
108
+ 重ね表示が有効なときは、アプリは**1つだけ**起動します(2つ目の起動はすぐに終了します)。macOSではDockにアイコンを出しません(エージェントアプリ。終了はゲーム内のボタンで行います)。開発中に複数起動したいときは `--next2d-allow-multiple` を付けて起動します。
109
109
 
110
- スリープ(ノートPCのふたを閉じる等)から復帰すると、GPUがリセットされて描画が止まることがあるため、画面(renderer)を読み込み直します(ゲームは自動保存を前提にしてください)。電源・ディスプレイ・GPUのイベントは `userData/diagnostics.log`(Windowsは `%APPDATA%\<appId>\diagnostics.log`)に記録されます。
110
+ Windowsでスリープ(ノートPCのふたを閉じる等)から復帰すると、重ね表示の窓がマウスを受け取れなくなることがあるため、窓を作り直します(ゲームは自動保存を前提にしてください)。電源・ディスプレイ・GPUのイベントは `userData/diagnostics.log`(Windowsは `%APPDATA%\<appId>\diagnostics.log`)に記録されます。
111
111
 
112
112
  Linuxの透明表示はコンポジタが必要です。
113
113
 
114
+ #### ミニウィンドウ(通常の窓 ⇄ 小窓)
115
+
116
+ ゲーム中に、通常の窓(全画面を含む)と、デスクトップの隅に置く最前面の小窓を切り替えられます。放置ゲームの「作業しながら遊ぶ」表示などに使います。`window.mini` 未指定なら従来どおりです。重ね表示(`window.overlay.enabled: true`)とは同時に使えません。
117
+
118
+ ```json
119
+ "window": {
120
+ "width": 1920, "height": 1080, "fullscreen": true,
121
+ "mini": { "width": 720, "height": 180, "anchor": "bottom-right", "margin": 16, "alwaysOnTop": true }
122
+ }
123
+ ```
124
+
125
+ | 項目 | 内容 |
126
+ |---|---|
127
+ | `width` / `height` | 小窓の中身(タイトルバーを除く)の大きさ。既定 720×180 |
128
+ | `anchor` | 初めて小窓にしたときの位置。`"bottom-right"`(既定)/ `"bottom-left"` / `"top-right"` / `"top-left"`。窓のある画面の作業領域(タスクバー・Dockを除く)の隅 |
129
+ | `margin` | 隅からの間隔(px)。既定 `16` |
130
+ | `alwaysOnTop` | `true`(既定)で小窓を最前面に置く(macOSは全ワークスペース・全画面アプリ上にも表示) |
131
+
132
+ 小窓は通常の窓のまま(タイトルバーあり。タイトルバーで動かせる)で、キー入力も受け取ります。リサイズ・最大化・全画面はできません。動かした位置は、アプリを終了するまで次の小窓で使います。通常の窓に戻すと、小窓にする前の位置・大きさ・最大化・全画面に戻ります。小窓の間は F11/Alt+Enter/Esc で全画面を切り替えません(Esc はゲームに届きます)。
133
+
134
+ rendererへ `window.next2dWindow` を公開します(アプリのメインフレームのみ)。
135
+
136
+ ```ts
137
+ const { mode, width, height } = await window.next2dWindow.getMode(); // mode は "full" か "mini"
138
+ await window.next2dWindow.setMode("mini"); // 切り替えが終わると { mode, width, height } で解決
139
+ const off = window.next2dWindow.onModeChange(({ mode, width, height }) => { /* 表示を合わせ直す */ });
140
+ const { x, y, width: w, height: h, scaleFactor } = await window.next2dWindow.getBounds();
141
+ await window.next2dWindow.quit();
142
+ ```
143
+
144
+ `width` / `height` は窓の中身の大きさ(CSS ピクセル)です。Next2D のステージは縦横比を保ったまま窓に合わせて縮むため、1920×1080 のステージを 720×180 の小窓で表示すると、ステージの範囲は高さ 180・幅 320 になります(`options.fullScreen: true` なら、左右のステージの外も描かれます)。小窓用の表示は、この大きさに合わせてゲーム側で配置してください。
145
+
114
146
  ### STEP4:認証を用意する
115
147
 
116
148
  #### macOSの署名・公証
@@ -327,12 +359,44 @@ await window.next2dWindow.quit();
327
359
  const off = window.next2dWindow.onResume(() => { /* resend state, etc. */ });
328
360
  ```
329
361
 
330
- With the overlay enabled the app runs **once**: a second launch quits at once (and, on macOS, never shows a Dock icon). Start with `--next2d-allow-multiple` to run several copies while developing.
362
+ With the overlay enabled the app runs **once**: a second launch quits at once. On macOS it is an agent app with no Dock icon (quit from inside the game). Start with `--next2d-allow-multiple` to run several copies while developing.
331
363
 
332
- After sleep (e.g. closing a laptop lid) the GPU may be reset and drawing stops, so the page (renderer) is loaded again; the game should save automatically. Power, display and GPU events are logged to `userData/diagnostics.log` (Windows: `%APPDATA%\<appId>\diagnostics.log`).
364
+ On Windows, after sleep (e.g. closing a laptop lid) the overlay window can stop receiving the mouse, so a fresh window replaces it; the game should save automatically. Power, display and GPU events are logged to `userData/diagnostics.log` (Windows: `%APPDATA%\<appId>\diagnostics.log`).
333
365
 
334
366
  Transparency on Linux requires a compositor.
335
367
 
368
+ #### Mini window (normal window ⇄ small window)
369
+
370
+ At run time the game can switch between its normal window (fullscreen included) and a small always-on-top window in a corner of the desktop, for "play while you work" idle modes and the like. Without `window.mini` the window behaves as before. It cannot be combined with the overlay (`window.overlay.enabled: true`).
371
+
372
+ ```json
373
+ "window": {
374
+ "width": 1920, "height": 1080, "fullscreen": true,
375
+ "mini": { "width": 720, "height": 180, "anchor": "bottom-right", "margin": 16, "alwaysOnTop": true }
376
+ }
377
+ ```
378
+
379
+ | Field | Behaviour |
380
+ |---|---|
381
+ | `width` / `height` | Content size of the small window (title bar excluded). Default 720×180. |
382
+ | `anchor` | Where the small window first appears: `"bottom-right"` (default), `"bottom-left"`, `"top-right"` or `"top-left"` corner of the work area (taskbar/Dock excluded) of the display the window is on. |
383
+ | `margin` | Distance from that corner in px. Default `16`. |
384
+ | `alwaysOnTop` | `true` (default) keeps the small window on top (on macOS also on every workspace and above fullscreen apps). |
385
+
386
+ The small window stays a normal window: it has a title bar (drag it to move the window) and takes keyboard input. It cannot be resized, maximized or made fullscreen. Where the player moves it is used for the next small window until the app quits. Switching back restores the position, size, maximized and fullscreen state from before. In the small window F11 / Alt+Enter / Escape do not toggle fullscreen (Escape reaches the game).
387
+
388
+ The renderer gets `window.next2dWindow` (app main frame only):
389
+
390
+ ```ts
391
+ const { mode, width, height } = await window.next2dWindow.getMode(); // mode is "full" or "mini"
392
+ await window.next2dWindow.setMode("mini"); // resolves with { mode, width, height } once applied
393
+ const off = window.next2dWindow.onModeChange(({ mode, width, height }) => { /* lay the page out again */ });
394
+ const { x, y, width: w, height: h, scaleFactor } = await window.next2dWindow.getBounds();
395
+ await window.next2dWindow.quit();
396
+ ```
397
+
398
+ `width` / `height` are the content size in CSS pixels. The Next2D stage keeps its aspect ratio when it is fitted into the window, so a 1920×1080 stage shown in a 720×180 window covers 320×180 (with `options.fullScreen: true` the area left and right of the stage is drawn too). Lay out the small-window view for that size in the game.
399
+
336
400
  ### STEP4: Prepare credentials
337
401
 
338
402
  #### macOS signing and notarization
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@next2d/builder",
3
- "version": "0.4.6",
3
+ "version": "0.4.8",
4
4
  "description": "Multi-platform builder for Next2d Framework, supporting export to various platforms such as macOS, Windows, iOS, Android and Web(HTML)",
5
5
  "author": "Toshiyuki Ienaga <ienaga@next2d.app>",
6
6
  "license": "MIT",
@@ -47,7 +47,7 @@
47
47
  "@typescript-eslint/parser": "^8.72.1",
48
48
  "eslint": "^10.14.0",
49
49
  "eslint-plugin-unused-imports": "^4.4.1",
50
- "globals": "^17.13.0",
50
+ "globals": "^17.14.0",
51
51
  "typescript": "^6.0.3",
52
52
  "vite": "^8.3.5"
53
53
  }
@@ -34,7 +34,92 @@ protocol.registerSchemesAsPrivileged([{
34
34
 
35
35
  const startURL = "next2d://game/index.html";
36
36
  const overlay = config.window.overlay?.enabled ? config.window.overlay : null;
37
+ // The game can switch between its normal window ("full") and a small always-on-top window ("mini") at run time.
38
+ const mini = !overlay && config.window.mini ? config.window.mini : null;
37
39
  let mainWindow;
40
+ let windowMode = "full";
41
+ // How the window looked before it became small, to put it back exactly (position, size, maximized, fullscreen).
42
+ let fullState = null;
43
+ // Where the player last left the small window (it has a title bar and can be moved).
44
+ let miniPosition = null;
45
+ // Mode changes are applied one after another (leaving fullscreen is animated on macOS).
46
+ let modeQueue = Promise.resolve();
47
+ const alive = () => Boolean(mainWindow && !mainWindow.isDestroyed());
48
+ const modeInfo = () => {
49
+ const [width, height] = mainWindow.getContentSize();
50
+ return { mode: windowMode, width, height };
51
+ };
52
+ // Entering and leaving fullscreen is animated on macOS: wait for it, so the size the page is told is the real one.
53
+ const setFullScreenAndWait = (win, value) => new Promise((resolve) => {
54
+ if (win.isFullScreen() === value) {
55
+ resolve();
56
+ return;
57
+ }
58
+ let timer;
59
+ const finish = () => {
60
+ clearTimeout(timer);
61
+ resolve();
62
+ };
63
+ // Some window managers never report it; do not wait forever.
64
+ timer = setTimeout(finish, 1500);
65
+ win.once(value ? "enter-full-screen" : "leave-full-screen", finish);
66
+ win.setFullScreen(value);
67
+ });
68
+ const cornerOf = (win) => {
69
+ const { width, height } = win.getBounds();
70
+ const { workArea } = screen.getDisplayMatching(win.getBounds());
71
+ const right = mini.anchor.endsWith("right");
72
+ const bottom = mini.anchor.startsWith("bottom");
73
+ return {
74
+ x: right ? workArea.x + workArea.width - width - mini.margin : workArea.x + mini.margin,
75
+ y: bottom ? workArea.y + workArea.height - height - mini.margin : workArea.y + mini.margin
76
+ };
77
+ };
78
+ const toMini = async () => {
79
+ const win = mainWindow;
80
+ fullState = { bounds: win.getNormalBounds(), maximized: win.isMaximized(), fullScreen: win.isFullScreen() };
81
+ await setFullScreenAndWait(win, false);
82
+ if (win.isMaximized()) win.unmaximize();
83
+ // Resize first: some platforms ignore size changes of a window that is no longer resizable.
84
+ win.setContentSize(mini.width, mini.height);
85
+ win.setFullScreenable(false);
86
+ win.setMaximizable(false);
87
+ win.setResizable(false);
88
+ const { x, y } = miniPosition ?? cornerOf(win);
89
+ win.setPosition(x, y);
90
+ if (mini.alwaysOnTop) {
91
+ win.setAlwaysOnTop(true, "floating");
92
+ if (process.platform === "darwin") win.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
93
+ }
94
+ windowMode = "mini";
95
+ };
96
+ const toFull = async () => {
97
+ const win = mainWindow;
98
+ const [x, y] = win.getPosition();
99
+ miniPosition = { x, y };
100
+ if (mini.alwaysOnTop) {
101
+ win.setAlwaysOnTop(false);
102
+ if (process.platform === "darwin") win.setVisibleOnAllWorkspaces(false);
103
+ }
104
+ win.setResizable(true);
105
+ win.setMaximizable(true);
106
+ win.setFullScreenable(true);
107
+ if (fullState) {
108
+ win.setBounds(fullState.bounds);
109
+ if (fullState.maximized) win.maximize();
110
+ if (fullState.fullScreen) await setFullScreenAndWait(win, true);
111
+ }
112
+ win.focus();
113
+ windowMode = "full";
114
+ };
115
+ const setWindowMode = (mode) => {
116
+ modeQueue = modeQueue.then(async () => {
117
+ if (!alive() || mode === windowMode) return;
118
+ await (mode === "mini" ? toMini() : toFull());
119
+ mainWindow.webContents.send("next2d:window:mode", modeInfo());
120
+ }).catch((error) => console.error("Window mode change failed:", error));
121
+ return modeQueue.then(() => (alive() ? modeInfo() : { mode: windowMode, width: 0, height: 0 }));
122
+ };
38
123
  // The last click-through state asked for, so it can be applied again after sleep/lock.
39
124
  let overlayIgnore = Boolean(config.window.overlay?.enabled && config.window.overlay.clickThrough);
40
125
  // Bottom anchor: flush with the primary display's work area (above the taskbar/Dock), centred.
@@ -76,9 +161,6 @@ const diag = (message) => {
76
161
  fs.appendFileSync(file, `${new Date().toISOString()} ${message}\n`);
77
162
  } catch { /* diagnostics must never break the game */ }
78
163
  };
79
- // After sleep (closing a laptop lid) the GPU is often reset. The renderer then keeps showing its last frame but can
80
- // no longer draw (the web page is not told), so the pet looks alive and menus never appear. Load the page again to
81
- // get a fresh GPU device. The game saves automatically, so progress is kept.
82
164
  // Counts of mouse events the page really receives (moves are forwarded while click-through; down/up only arrive
83
165
  // while the window accepts clicks). Logged when they change, to tell "clicks never reach the page" from other causes.
84
166
  const DIAG_SCRIPT = `(() => {
@@ -103,19 +185,37 @@ const snapshotRenderer = async () => {
103
185
  diag(`renderer snapshot failed: ${error?.message ?? error}`);
104
186
  }
105
187
  };
106
- const reloadRenderer = () => {
107
- if (!mainWindow || mainWindow.isDestroyed()) return;
108
- diag("reload renderer (resume)");
188
+ // After sleep (closing a laptop lid) Windows can leave the overlay window itself broken: mouse moves are no longer
189
+ // forwarded to it, or clicks fall through even where the pet is drawn. Reloading the page does not help (the
190
+ // window is the same), so build a new window and close the old one. The game saves automatically, so progress is
191
+ // kept. The new window is created first so the app never has zero windows (which would quit it).
192
+ let recreating = false;
193
+ const recreateWindow = async () => {
194
+ if (recreating || !mainWindow || mainWindow.isDestroyed()) return;
195
+ recreating = true;
196
+ diag("recreate window (resume)");
197
+ const old = mainWindow;
109
198
  overlayIgnore = Boolean(overlay?.clickThrough);
110
- mainWindow.setIgnoreMouseEvents(overlayIgnore, overlayIgnore ? { forward: true } : undefined);
111
- mainWindow.webContents.reloadIgnoringCache();
199
+ try {
200
+ await createWindow();
201
+ } catch (error) {
202
+ diag(`recreate window failed: ${error?.message ?? error}`);
203
+ } finally {
204
+ recreating = false;
205
+ if (old !== mainWindow && !old.isDestroyed()) {
206
+ // Closing on purpose: do not treat the old page going away as a crash.
207
+ old.webContents.removeAllListeners("render-process-gone");
208
+ old.destroy();
209
+ }
210
+ }
112
211
  };
113
212
  const createWindow = async () => {
114
213
  const { width, height } = screen.getPrimaryDisplay().workAreaSize;
115
214
  // The preload reads these flags; each API is exposed only when its feature is configured.
116
215
  const preloadFlags = [
117
216
  ...(config.nativeBridge ? ["--next2d-native-bridge"] : []),
118
- ...(overlay ? ["--next2d-overlay"] : [])
217
+ ...(overlay ? ["--next2d-overlay"] : []),
218
+ ...(mini ? ["--next2d-window-mode"] : [])
119
219
  ];
120
220
  mainWindow = new BrowserWindow({
121
221
  width: Math.min(config.window.width, width),
@@ -183,7 +283,8 @@ const createWindow = async () => {
183
283
  }
184
284
  });
185
285
  mainWindow.webContents.on("before-input-event", (event, input) => {
186
- if (input.type !== "keyDown" || overlay) {
286
+ // The small window never goes fullscreen; Escape belongs to the game there.
287
+ if (input.type !== "keyDown" || overlay || windowMode === "mini") {
187
288
  return;
188
289
  }
189
290
  if (input.key === "F11" || (input.alt && input.key === "Enter")) {
@@ -205,9 +306,8 @@ const createWindow = async () => {
205
306
 
206
307
  app.whenReady().then(async () => {
207
308
  if (!hasLock) return;
208
- // The overlay app is packaged as an agent app (LSUIElement) so a second launch never shows a Dock icon;
209
- // the instance that keeps running shows it.
210
- if (process.platform === "darwin" && overlay) app.dock?.show();
309
+ // The overlay app is packaged as an agent app (LSUIElement): it never shows a Dock icon (a desktop pet stays out
310
+ // of the Dock and the app switcher; the game offers its own "quit").
211
311
  Menu.setApplicationMenu(process.platform === "darwin" ? Menu.buildFromTemplate([
212
312
  { role: "appMenu" }, { role: "editMenu" }, { role: "windowMenu" }
213
313
  ]) : null);
@@ -246,24 +346,42 @@ app.whenReady().then(async () => {
246
346
  return nativeBridge.request(method, params);
247
347
  });
248
348
  }
349
+ const trusted = (event) => {
350
+ if (!isTrustedSender(event, mainWindow)) {
351
+ diag(`window API access denied (frame url: ${event?.senderFrame?.url})`);
352
+ throw new Error("Window API access denied");
353
+ }
354
+ };
355
+ if (overlay || mini) {
356
+ ipcMain.handle("next2d:window:get-bounds", (event) => {
357
+ trusted(event);
358
+ const bounds = mainWindow.getBounds();
359
+ return { ...bounds, scaleFactor: screen.getDisplayMatching(bounds).scaleFactor };
360
+ });
361
+ ipcMain.handle("next2d:window:quit", (event) => {
362
+ trusted(event);
363
+ // Reply before the window is torn down.
364
+ setImmediate(() => app.quit());
365
+ });
366
+ }
367
+ if (mini) {
368
+ ipcMain.handle("next2d:window:get-mode", (event) => {
369
+ trusted(event);
370
+ return modeInfo();
371
+ });
372
+ ipcMain.handle("next2d:window:set-mode", (event, mode) => {
373
+ trusted(event);
374
+ if (mode !== "full" && mode !== "mini") throw new TypeError("mode must be \"full\" or \"mini\"");
375
+ return setWindowMode(mode);
376
+ });
377
+ }
249
378
  if (overlay) {
250
- const trusted = (event) => {
251
- if (!isTrustedSender(event, mainWindow)) {
252
- diag(`window API access denied (frame url: ${event?.senderFrame?.url})`);
253
- throw new Error("Window API access denied");
254
- }
255
- };
256
379
  ipcMain.handle("next2d:window:set-ignore-mouse-events", (event, ignore) => {
257
380
  trusted(event);
258
381
  if (typeof ignore !== "boolean") throw new TypeError("ignore must be a boolean");
259
382
  overlayIgnore = ignore;
260
383
  mainWindow.setIgnoreMouseEvents(ignore, ignore ? { forward: true } : undefined);
261
384
  });
262
- ipcMain.handle("next2d:window:get-bounds", (event) => {
263
- trusted(event);
264
- const bounds = mainWindow.getBounds();
265
- return { ...bounds, scaleFactor: screen.getDisplayMatching(bounds).scaleFactor };
266
- });
267
385
  // After sleep or screen lock the OS can drop the mouse-forwarding hook and reset the window's
268
386
  // z-order/bounds. Apply the overlay state again and tell the page so it can recover too.
269
387
  const recoverOverlay = () => {
@@ -278,19 +396,15 @@ app.whenReady().then(async () => {
278
396
  for (const name of ["resume", "unlock-screen"]) {
279
397
  powerMonitor.on(name, () => {
280
398
  diag(name);
281
- setTimeout(() => {
282
- recoverOverlay();
283
- // Only a resume from sleep can reset the GPU (unlocking the screen cannot).
284
- if (name === "resume") reloadRenderer();
285
- }, 1000);
399
+ if (name === "resume" && process.platform === "win32") {
400
+ // Give the GPU/display time to come back, then start over with a fresh window.
401
+ setTimeout(() => { void recreateWindow(); }, 3000);
402
+ } else {
403
+ setTimeout(recoverOverlay, 1000);
404
+ }
286
405
  });
287
406
  }
288
407
  app.on("child-process-gone", (_event, details) => diag(`child-process-gone ${details.type} ${details.reason}`));
289
- ipcMain.handle("next2d:window:quit", (event) => {
290
- trusted(event);
291
- // Reply before the window is torn down.
292
- setImmediate(() => app.quit());
293
- });
294
408
  }
295
409
  await createWindow();
296
410
  }).catch((error) => {
@@ -31,3 +31,24 @@ if (flags.has("--next2d-overlay")) {
31
31
  }
32
32
  });
33
33
  }
34
+
35
+ // window.mini: switch between the normal window ("full") and the small always-on-top window ("mini").
36
+ // Never set together with the overlay (the config is rejected), so the two never expose next2dWindow twice.
37
+ if (flags.has("--next2d-window-mode")) {
38
+ contextBridge.exposeInMainWorld("next2dWindow", {
39
+ getMode: () => ipcRenderer.invoke("next2d:window:get-mode"),
40
+ setMode: async (mode) => {
41
+ if (mode !== "full" && mode !== "mini") throw new TypeError("Expected \"full\" or \"mini\"");
42
+ return ipcRenderer.invoke("next2d:window:set-mode", mode);
43
+ },
44
+ getBounds: () => ipcRenderer.invoke("next2d:window:get-bounds"),
45
+ quit: async () => { await ipcRenderer.invoke("next2d:window:quit"); },
46
+ // Fires after every change with { mode, width, height } (content size in CSS pixels).
47
+ onModeChange: (listener) => {
48
+ if (typeof listener !== "function") throw new TypeError("Expected mode listener");
49
+ const handler = (_event, info) => listener(info);
50
+ ipcRenderer.on("next2d:window:mode", handler);
51
+ return () => ipcRenderer.removeListener("next2d:window:mode", handler);
52
+ }
53
+ });
54
+ }