bun-app-window 0.7.0 → 0.8.0
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.
- package/CHANGELOG.md +264 -73
- package/README.md +247 -142
- package/README.zh-CN.md +238 -142
- package/README.zh-TW.md +241 -141
- package/package.json +9 -5
- package/src/shell/app.ts +255 -2
- package/src/shell/assets.d.ts +4 -4
- package/src/shell/embedded.ts +9 -10
- package/src/shell/index.ts +301 -318
- package/src/shell/ipc.ts +91 -33
- package/src/shell/native.ts +61 -64
- package/src/shell/pack.ts +247 -28
- package/src/shell/port.ts +37 -38
- package/src/shell/resolve.ts +121 -140
- package/src/shell/state.ts +8 -19
- package/src/shell/types.ts +79 -81
- package/src/ui/titlebar.ts +1 -2
package/src/shell/index.ts
CHANGED
|
@@ -26,15 +26,15 @@ import type { NativeSymbols } from "./native";
|
|
|
26
26
|
import { resolveNativeLib } from "./resolve";
|
|
27
27
|
import { EVENT_QUEUE_CAPACITY, EVENT_RECORD_BYTES, decodeEvents, type WindowEvent } from "./events";
|
|
28
28
|
import {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
29
|
+
STATE_FOCUSED,
|
|
30
|
+
STATE_MAXIMIZED,
|
|
31
|
+
STATE_MINIMIZED,
|
|
32
|
+
resizeIndex,
|
|
33
|
+
type NativeLibInfo,
|
|
34
|
+
type ResizeEdge,
|
|
35
|
+
type ShellOptions,
|
|
36
|
+
type StartResult,
|
|
37
|
+
type WindowState,
|
|
38
38
|
} from "./types";
|
|
39
39
|
|
|
40
40
|
export * from "./types";
|
|
@@ -44,11 +44,7 @@ export { serveOnSafePort, isRestrictedPort } from "./port";
|
|
|
44
44
|
export type { ServeOptions } from "./port";
|
|
45
45
|
export { cstr, readCStr } from "./native";
|
|
46
46
|
export type { NativeSymbols, NativeLib } from "./native";
|
|
47
|
-
export type {
|
|
48
|
-
ClosedEvent,
|
|
49
|
-
StateEvent,
|
|
50
|
-
WindowEvent,
|
|
51
|
-
} from "./events";
|
|
47
|
+
export type { ClosedEvent, StateEvent, WindowEvent } from "./events";
|
|
52
48
|
|
|
53
49
|
/**
|
|
54
50
|
* 原生调用的回执。
|
|
@@ -63,10 +59,10 @@ export type {
|
|
|
63
59
|
* (`state()` / `getZoom()` / `getBounds()`)。
|
|
64
60
|
*/
|
|
65
61
|
export type NativeAck = {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
62
|
+
/** `0` = 已投递给 UI 线程;负数 = 当场被拒(窗口已销毁、参数越界等) */
|
|
63
|
+
rc: number;
|
|
64
|
+
/** 失败原因;成功时恒为空串 */
|
|
65
|
+
err: string;
|
|
70
66
|
};
|
|
71
67
|
|
|
72
68
|
/**
|
|
@@ -76,314 +72,301 @@ export type NativeAck = {
|
|
|
76
72
|
* 成功时读它只会拿到上一次的残留——显示出来就是一个假故障。
|
|
77
73
|
*/
|
|
78
74
|
export function ackOf(shell: Shell, rc: number): NativeAck {
|
|
79
|
-
|
|
75
|
+
return { rc, err: rc === 0 ? "" : shell.lastError() };
|
|
80
76
|
}
|
|
81
77
|
|
|
82
78
|
export type Shell = {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
/** 逃生舱:直接访问 dlopen 出来的符号 */
|
|
251
|
-
readonly symbols: NativeSymbols;
|
|
252
|
-
|
|
253
|
-
// 排障面(hittest / metrics / dpi / selfBase / selfPath)已移到独立入口
|
|
254
|
-
// import { debugOf } from "bun-app-window/debug"
|
|
255
|
-
// 目的:公共面只承诺产品向能力。把排障面留在主入口,等于把它也变成契约,
|
|
256
|
-
// 将来改一次诊断输出就成了破坏性变更。
|
|
79
|
+
/** 创建窗口并加载 URL。返回码 0 表示成功;失败原因见 lastError() */
|
|
80
|
+
start(): StartResult;
|
|
81
|
+
/** 宿 → 页面:投递 JSON 文本(单向,不等结果) */
|
|
82
|
+
post(json: string): number;
|
|
83
|
+
setTitle(title: string): number;
|
|
84
|
+
minimize(): number;
|
|
85
|
+
toggleMaximize(): number;
|
|
86
|
+
close(): number;
|
|
87
|
+
/** 由页面 pointerdown 触发原生拖拽;保留贴边分屏 */
|
|
88
|
+
drag(): number;
|
|
89
|
+
/**
|
|
90
|
+
* 由页面触发的边框缩放(进入 Windows 的模态缩放循环,鼠标被系统接管)。
|
|
91
|
+
*
|
|
92
|
+
* **入参是 `RESIZE_EDGE` 的 HT* 值(10–17)**,不是内部数组下标。
|
|
93
|
+
*
|
|
94
|
+
* 0.5.x 的实现把入参直传给原生 `shell_begin_resize()`,而原生收的是
|
|
95
|
+
* `codes[8]` 的**数组下标 0–7**——两者的映射是 `HT*` 常量在 Win32 里的定义顺序,
|
|
96
|
+
* 既不是恒等也不是「减 10」(`HTTOPLEFT`=13 在下标 4、`HTBOTTOM`=15 在下标 3)。
|
|
97
|
+
* 于是照着注释传 `RESIZE_EDGE.LEFT`(=10)会被 `edge > 7` 当场拒掉,
|
|
98
|
+
* `rc=-1`,外部表现是「点了没反应」,而门禁全绿。
|
|
99
|
+
*
|
|
100
|
+
* 0.6.0 起由本方法在 TS 层查表转换,原生 DLL 不需要重编。非法值仍然返回 -1。
|
|
101
|
+
*/
|
|
102
|
+
beginResize(edge: ResizeEdge): number;
|
|
103
|
+
/** 最小尺寸,逻辑像素 */
|
|
104
|
+
setMinSize(width: number, height: number): void;
|
|
105
|
+
/**
|
|
106
|
+
* 显示窗口。配合 `FLAG_DEFER_SHOW` 使用:页面就绪后再调用,消除首帧闪烁。
|
|
107
|
+
* 未启用延迟显示时是幂等 no-op。
|
|
108
|
+
*/
|
|
109
|
+
show(): number;
|
|
110
|
+
/**
|
|
111
|
+
* 设置 AppUserModelID(任务栏分组与图标归属)。
|
|
112
|
+
* 只在 `start()` 之前调用才对本窗口生效。
|
|
113
|
+
*/
|
|
114
|
+
setAppId(appId: string): number;
|
|
115
|
+
/**
|
|
116
|
+
* 弹出系统文件夹选择框,返回选中路径;用户取消返回 null。
|
|
117
|
+
*
|
|
118
|
+
* 直接调 shell32,不起子进程、不落临时脚本——比「spawn powershell 跑 WinForms」
|
|
119
|
+
* 快一个数量级,也不受执行策略限制。
|
|
120
|
+
*
|
|
121
|
+
* **同步阻塞**:SHBrowseForFolder 自带模态循环,调用期间调用线程被占住
|
|
122
|
+
* (实测:Bun 主线程上的定时器在此期间不会触发)。同理,被阻塞的调用链
|
|
123
|
+
* 无法取消或超时。需要并发时把它放到 Worker 里调用。
|
|
124
|
+
*/
|
|
125
|
+
pickFolder(title?: string): string | null;
|
|
126
|
+
state(): WindowState;
|
|
127
|
+
/**
|
|
128
|
+
* 窗口是否仍然存在。
|
|
129
|
+
*
|
|
130
|
+
* `state()` 无法区分「未启动」与「已销毁」(两者都返回全 false),
|
|
131
|
+
* 宿主需要这个信号才能判断窗口是否已被用户关掉,进而回收自己的进程——
|
|
132
|
+
* 窗口销毁后原生 UI 线程会返回,但宿主进程与它起的本地服务不会自己退出。
|
|
133
|
+
*
|
|
134
|
+
* 想要「等窗口关闭」而不是「每一拍问一次」时,用 `pollEvents()` 的
|
|
135
|
+
* `EVENT_CLOSED`(或 `bun-app-window/events` 的 `attachEventPump()`)。
|
|
136
|
+
* 两者不等价:`isAlive()` 读的是此刻的 `g_webview` 是否为空,而 `CLOSED`
|
|
137
|
+
* 是原生侧在 teardown 全部做完之后写进队列的显式终态。
|
|
138
|
+
*/
|
|
139
|
+
isAlive(): boolean;
|
|
140
|
+
/**
|
|
141
|
+
* 批量拉取窗口生命周期事件(`EVENT_STATE` / `EVENT_CLOSED`)。
|
|
142
|
+
*
|
|
143
|
+
* **消费式**:读走的记录不会再来。所以一个进程只应有一个消费者——
|
|
144
|
+
* 要么用 `bun-app-window/events` 的 `attachEventPump()`,要么自己按节奏调
|
|
145
|
+
* 这个方法,别两者都来:两个消费者会各自拿到一部分,谁也看不全。
|
|
146
|
+
*
|
|
147
|
+
* 同步、零分配以外的副作用;`max` 是记录条数,内部夹在
|
|
148
|
+
* `1..EVENT_QUEUE_CAPACITY`。窗口未启动时返回空数组。
|
|
149
|
+
*
|
|
150
|
+
* ```ts
|
|
151
|
+
* for (const e of shell.pollEvents()) {
|
|
152
|
+
* if (e.type === EVENT_CLOSED) break;
|
|
153
|
+
* if (e.maximized) { ... }
|
|
154
|
+
* }
|
|
155
|
+
* ```
|
|
156
|
+
*/
|
|
157
|
+
pollEvents(max?: number): WindowEvent[];
|
|
158
|
+
/** 最近一次失败的原因;无错误时为空串。走拷贝式读出,不受并发写影响 */
|
|
159
|
+
lastError(): string;
|
|
160
|
+
|
|
161
|
+
// ---- 应用化 ----
|
|
162
|
+
/**
|
|
163
|
+
* 关掉指定的浏览器行为(`BROWSER_UI_*` 按位或)。运行时生效。
|
|
164
|
+
*
|
|
165
|
+
* 一般通过 `ShellOptions.browserUi` 声明即可;这个方法用于运行中切换
|
|
166
|
+
* (例如调试模式临时放开 `Ctrl+F`)。
|
|
167
|
+
*/
|
|
168
|
+
setBrowserUi(mask: number): number;
|
|
169
|
+
/**
|
|
170
|
+
* 在屏幕坐标弹出**原生**上下文菜单。
|
|
171
|
+
*
|
|
172
|
+
* 关掉 WebView2 默认右键菜单后,右键就成了「点了没反应」,那也不像原生应用。
|
|
173
|
+
* 原生应用右键是有菜单的,只是菜单属于应用:
|
|
174
|
+
*
|
|
175
|
+
* ```ts
|
|
176
|
+
* document.addEventListener("contextmenu", (e) => {
|
|
177
|
+
* e.preventDefault();
|
|
178
|
+
* fetch("/api/menu", { method: "POST", body: JSON.stringify({ x: e.screenX, y: e.screenY }) });
|
|
179
|
+
* });
|
|
180
|
+
* ```
|
|
181
|
+
*
|
|
182
|
+
* `items` 用行描述,`id|label` 每行一项,单独 `-` 是分隔线:
|
|
183
|
+
* `"open|打开题库\nrefresh|刷新\n-\nquit|退出"`。
|
|
184
|
+
* 选中后通过 shell-msg 推回 `{ t: "menu", id: "<id>" }`。
|
|
185
|
+
*/
|
|
186
|
+
showContextMenu(x: number, y: number, items: string): number;
|
|
187
|
+
|
|
188
|
+
// ---- 窗口位置 ----
|
|
189
|
+
/**
|
|
190
|
+
* 当前窗口矩形,格式 "x,y,w,h,maximized,dpi"(物理像素)。
|
|
191
|
+
* 最大化时返回的是「还原后」的矩形。窗口未启动时返回空串。
|
|
192
|
+
*
|
|
193
|
+
* 一般不需要直接解析它——用 bun-app-window/state 的 capturePlacement()。
|
|
194
|
+
*/
|
|
195
|
+
getBounds(): string;
|
|
196
|
+
/**
|
|
197
|
+
* 恢复窗口矩形。savedDpi 是保存时的 DPI,用于跨缩放比例换算;
|
|
198
|
+
* 原生侧还会校验矩形是否仍在某块屏上,不在则居中。
|
|
199
|
+
*
|
|
200
|
+
* 一般不需要直接调用——用 bun-app-window/state 的 restorePlacement()。
|
|
201
|
+
*/
|
|
202
|
+
restoreBounds(x: number, y: number, width: number, height: number, savedDpi: number, maximized: boolean): number;
|
|
203
|
+
|
|
204
|
+
// ---- 输入与视图 ----
|
|
205
|
+
/**
|
|
206
|
+
* 页面缩放倍率(0.25 ~ 3.0)。也支持用户键盘操作:Ctrl± / Ctrl+0 / Ctrl+滚轮。
|
|
207
|
+
* 生效是异步的(投到 UI 线程),getZoom() 读的是缓存值。
|
|
208
|
+
*/
|
|
209
|
+
setZoom(factor: number): number;
|
|
210
|
+
/** 当前缩放倍率(缓存值) */
|
|
211
|
+
getZoom(): number;
|
|
212
|
+
/** 重新加载页面(等同 Ctrl+R / F5) */
|
|
213
|
+
reload(): number;
|
|
214
|
+
/** 打开开发者工具(等同 F12) */
|
|
215
|
+
openDevTools(): number;
|
|
216
|
+
/** 切换全屏(等同 F11);全屏与最大化不同,连边框与任务栏一起让出 */
|
|
217
|
+
toggleFullscreen(): number;
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* 运行时切换窗口形态(ADR 0019)。`frameless = false` 让系统标题栏回来。
|
|
221
|
+
*
|
|
222
|
+
* **只在创建期给了 `FLAG_FRAMELESS` 时可用**:带边框起点是 webview 自建的窗口,
|
|
223
|
+
* 我们没有它的 `WM_NCCALCSIZE` 控制权。那种情况下返回 -1,理由在 `lastError()`。
|
|
224
|
+
*
|
|
225
|
+
* 生效是异步的(投到 UI 线程)。确认方式两条:`isFrameless()` 读缓存,或页面
|
|
226
|
+
* 监听 `shell-msg` 的 `win` 消息——它的 `frameless` 字段会在切换后推一次。
|
|
227
|
+
* 全屏期间调用会被拒:`toggleFullscreen` 自带样式位与矩形的缓存,两者同时改
|
|
228
|
+
* 会让退出全屏还原成一个混合体。
|
|
229
|
+
*/
|
|
230
|
+
setFrameless(frameless: boolean): number;
|
|
231
|
+
/** 当前运行时形态。读缓存,无窗口调用;未启动时回创建期形态 */
|
|
232
|
+
isFrameless(): boolean;
|
|
233
|
+
|
|
234
|
+
/** 停止并释放窗口(可重复调用) */
|
|
235
|
+
stop(): void;
|
|
236
|
+
|
|
237
|
+
readonly started: boolean;
|
|
238
|
+
readonly libInfo: NativeLibInfo;
|
|
239
|
+
/** 逃生舱:直接访问 dlopen 出来的符号 */
|
|
240
|
+
readonly symbols: NativeSymbols;
|
|
241
|
+
|
|
242
|
+
// 排障面(hittest / metrics / dpi / selfBase / selfPath)已移到独立入口
|
|
243
|
+
// import { debugOf } from "bun-app-window/debug"
|
|
244
|
+
// 目的:公共面只承诺产品向能力。把排障面留在主入口,等于把它也变成契约,
|
|
245
|
+
// 将来改一次诊断输出就成了破坏性变更。
|
|
257
246
|
};
|
|
258
247
|
|
|
259
248
|
export async function createShell(opts: ShellOptions): Promise<Shell> {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
symbols,
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
};
|
|
377
|
-
|
|
378
|
-
return shell;
|
|
249
|
+
if (!opts.url) throw new Error("createShell: url is required");
|
|
250
|
+
|
|
251
|
+
const libInfo = await resolveNativeLib({
|
|
252
|
+
...(opts.nativeLibPath === undefined ? {} : { nativeLibPath: opts.nativeLibPath }),
|
|
253
|
+
...(opts.appName === undefined ? {} : { appName: opts.appName }),
|
|
254
|
+
...(opts.embedded === undefined ? {} : { embedded: opts.embedded }),
|
|
255
|
+
});
|
|
256
|
+
const { symbols } = openNativeLib(libInfo.path);
|
|
257
|
+
|
|
258
|
+
let started = false;
|
|
259
|
+
let stopped = false;
|
|
260
|
+
|
|
261
|
+
// 拷贝式读出:shell_error() 返回共享缓冲区的指针,解锁后可能被别的线程改写
|
|
262
|
+
const lastError = () => readBuffer((p, len) => symbols.shell_error_text(p, len), 512);
|
|
263
|
+
|
|
264
|
+
const shell: Shell = {
|
|
265
|
+
start(): StartResult {
|
|
266
|
+
if (started) return { rc: -1, error: "shell already started" };
|
|
267
|
+
const rc = symbols.shell_start_ex(
|
|
268
|
+
cstr(opts.url),
|
|
269
|
+
cstr(opts.title ?? "bun-app-window"),
|
|
270
|
+
opts.width ?? 1000,
|
|
271
|
+
opts.height ?? 680,
|
|
272
|
+
opts.flags ?? 0,
|
|
273
|
+
);
|
|
274
|
+
started = rc === 0;
|
|
275
|
+
if (started && opts.minWidth && opts.minHeight) {
|
|
276
|
+
symbols.shell_set_min_size(opts.minWidth, opts.minHeight);
|
|
277
|
+
}
|
|
278
|
+
if (started && opts.browserUi) {
|
|
279
|
+
// 放在 start 之后:需要 webview 已存在。这些是 UI 行为开关,
|
|
280
|
+
// 运行时改即时生效,不涉及渲染,不必赶在 navigate 之前。
|
|
281
|
+
symbols.shell_set_browser_ui(opts.browserUi);
|
|
282
|
+
}
|
|
283
|
+
return { rc, error: rc === 0 ? "" : lastError() };
|
|
284
|
+
},
|
|
285
|
+
|
|
286
|
+
post: (json) => symbols.shell_post(cstr(json)),
|
|
287
|
+
setTitle: (title) => symbols.shell_title(cstr(title)),
|
|
288
|
+
minimize: () => symbols.shell_minimize(),
|
|
289
|
+
toggleMaximize: () => symbols.shell_toggle_maximize(),
|
|
290
|
+
close: () => symbols.shell_close(),
|
|
291
|
+
drag: () => symbols.shell_drag(),
|
|
292
|
+
beginResize: (edge) => symbols.shell_begin_resize(resizeIndex(edge)),
|
|
293
|
+
setMinSize: (w, h) => symbols.shell_set_min_size(w, h),
|
|
294
|
+
show: () => symbols.shell_show(),
|
|
295
|
+
setAppId: (appId) => symbols.shell_set_app_id(cstr(appId)),
|
|
296
|
+
|
|
297
|
+
pickFolder(title?: string): string | null {
|
|
298
|
+
const buf = new Uint8Array(4096);
|
|
299
|
+
const rc = symbols.shell_pick_folder(cstr(title ?? "选择文件夹"), ptr(buf), buf.length);
|
|
300
|
+
if (rc !== 0) return null; // 1 = 取消,-1 = 失败
|
|
301
|
+
const end = buf.indexOf(0);
|
|
302
|
+
return new TextDecoder().decode(buf.subarray(0, end < 0 ? buf.length : end));
|
|
303
|
+
},
|
|
304
|
+
|
|
305
|
+
state(): WindowState {
|
|
306
|
+
const raw = symbols.shell_get_state();
|
|
307
|
+
return {
|
|
308
|
+
raw,
|
|
309
|
+
maximized: (raw & STATE_MAXIMIZED) !== 0,
|
|
310
|
+
minimized: (raw & STATE_MINIMIZED) !== 0,
|
|
311
|
+
focused: (raw & STATE_FOCUSED) !== 0,
|
|
312
|
+
dark: symbols.shell_is_dark() !== 0,
|
|
313
|
+
};
|
|
314
|
+
},
|
|
315
|
+
|
|
316
|
+
isAlive: () => symbols.shell_is_alive() !== 0,
|
|
317
|
+
|
|
318
|
+
pollEvents(max = 64): WindowEvent[] {
|
|
319
|
+
// 原生侧环容量是 128,一次要更多没有意义;下限 1 免得 0 变成「永远拉不到」
|
|
320
|
+
const cap = Math.max(1, Math.min(Math.trunc(max), EVENT_QUEUE_CAPACITY));
|
|
321
|
+
const buf = new Uint8Array(cap * EVENT_RECORD_BYTES);
|
|
322
|
+
const n = symbols.shell_poll_events(ptr(buf), cap);
|
|
323
|
+
return n > 0 ? decodeEvents(buf, n) : [];
|
|
324
|
+
},
|
|
325
|
+
|
|
326
|
+
lastError,
|
|
327
|
+
|
|
328
|
+
setBrowserUi: (mask) => symbols.shell_set_browser_ui(mask),
|
|
329
|
+
showContextMenu: (x, y, items) => symbols.shell_show_app_menu(x, y, cstr(items)),
|
|
330
|
+
|
|
331
|
+
getBounds: () => readBuffer((p, len) => symbols.shell_get_bounds(p, len), 256),
|
|
332
|
+
restoreBounds: (x, y, w, h, savedDpi, maximized) =>
|
|
333
|
+
symbols.shell_restore_bounds(x, y, w, h, savedDpi, maximized ? 1 : 0),
|
|
334
|
+
|
|
335
|
+
setZoom: (factor) => symbols.shell_set_zoom(factor),
|
|
336
|
+
getZoom: () => symbols.shell_get_zoom(),
|
|
337
|
+
reload: () => symbols.shell_reload(),
|
|
338
|
+
openDevTools: () => symbols.shell_devtools(),
|
|
339
|
+
toggleFullscreen: () => symbols.shell_toggle_fullscreen(),
|
|
340
|
+
setFrameless: (frameless) => symbols.shell_set_frameless(frameless ? 1 : 0),
|
|
341
|
+
isFrameless: () => symbols.shell_is_frameless() !== 0,
|
|
342
|
+
|
|
343
|
+
stop() {
|
|
344
|
+
if (stopped) return;
|
|
345
|
+
stopped = true;
|
|
346
|
+
started = false;
|
|
347
|
+
try {
|
|
348
|
+
symbols.shell_stop();
|
|
349
|
+
} catch {
|
|
350
|
+
// 窗口已销毁时无需处理
|
|
351
|
+
}
|
|
352
|
+
},
|
|
353
|
+
|
|
354
|
+
get started() {
|
|
355
|
+
return started;
|
|
356
|
+
},
|
|
357
|
+
libInfo,
|
|
358
|
+
symbols,
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
return shell;
|
|
379
362
|
}
|
|
380
363
|
|
|
381
364
|
/** 语法糖:创建并启动;失败直接抛错 */
|
|
382
365
|
export async function openShell(opts: ShellOptions): Promise<Shell> {
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
366
|
+
const shell = await createShell(opts);
|
|
367
|
+
const { rc, error } = shell.start();
|
|
368
|
+
if (rc !== 0) {
|
|
369
|
+
throw new Error(`shell_start_ex failed (rc=${rc}): ${error || "unknown"}`);
|
|
370
|
+
}
|
|
371
|
+
return shell;
|
|
389
372
|
}
|