bun-app-window 0.6.2 → 0.7.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 CHANGED
@@ -6,6 +6,53 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.7.0] - 2026-10-25
10
+
11
+ ### Added
12
+
13
+ - **`setFrameless()` switches the window form at runtime; `isFrameless()` reports the current one.** `FLAG_FRAMELESS` had been a creation-time flag since 0.3, so comparing a frameless window against a framed one meant restarting the process. That is a real cost for acceptance: the form check has three branches (`frameless` + restored, `frameless` + maximized, `framed`), one run only ever proves one of them — and a restart throws away the event ring, the zoom factor and the restored geometry, which are exactly the state that makes "is the geometry consistent across a switch" testable. Consumers can now also expose a title-bar toggle instead of shipping two builds.
14
+
15
+ **The creation path and the behaviour predicates are now two variables.** `g_frameless` decides *which window creation path runs* — our own `webview_shell` window (embedded `webview_create(1, hwnd)`, full `shell_wnd_proc`) versus the window the webview library creates (`toplevel_proc` subclassing only). That is fixed the moment `CreateWindowExW` / `webview_create` returns and cannot change. `g_frameless_live` decides *behaviour*: whether `WM_NCCALCSIZE` intercepts, whether resize borders and caption buttons take part in hit-testing, whether `fit_widget` reserves the title-bar strip, and what `push_state` reports as `frameless`. Five predicates moved to the live variable; the creation path stayed where it was.
16
+
17
+ **Switching does not touch the style bits.** `create_own_window` already passes `WS_OVERLAPPEDWINDOW` — the caption, border and sizing frame are all there. Frameless is achieved by `WM_NCCALCSIZE` returning 0, which erases the non-client area; it is not achieved by removing style bits. So going back to framed takes two steps: let `WM_NCCALCSIZE` fall through, then `SetWindowPos(SWP_FRAMECHANGED)` to force the system to recompute. Assembling `WS_CAPTION | WS_THICKFRAME | WS_SYSMENU | WS_MINIMIZEBOX | WS_MAXIMIZEBOX` by hand would give five chances to produce a window that cannot be dragged or has no system menu; `WM_NCCALCSIZE` just hands the system back the answer it would have computed anyway.
18
+
19
+ **Three refusals, each with a concrete reason.** (1) No window yet. (2) Created framed — that window belongs to the webview library and we only subclass it, so `WM_NCCALCSIZE` is not ours to answer; switching that direction needs `shell_close` + a fresh `shell_start_ex`, a different route not taken here. (3) Currently fullscreen — `toggle_fullscreen` caches the pre-fullscreen style bits and rect, and changing form mid-flight makes the restore produce a window with one form's style bits and the other form's geometry. **Note the scope: only a frameless-created window can switch**, and framed is the default, so this is narrower than the name suggests. It is stated in the interface docs and in ADR 0019 rather than left to be discovered.
20
+
21
+ The state push after a switch uses `PushMode::BypassAll`: `PushedState`'s change detection has no `frameless` bit, and the client area can come out the same size in both forms at some window sizes — the normal path would be absorbed as "nothing changed" and the page would never learn to mount or unmount its own title bar.
22
+
23
+ Verified on a real window at 200% DPI (`scripts/probe-frameless-toggle.ts`, 10 assertions, all passing): frameless `win=1800x1200 client=1800x1200`; framed `client=1774x1129` — 26×50 physical pixels handed back, consistent with the `border=13x13` on the same line once you add the title bar; switching back restores the client area pixel for pixel. The load-bearing assertion is **"client area matches the initial reading after switching back"**, not "`win == client`" — the latter stays green if both sides shrink together.
24
+
25
+ ### Note
26
+
27
+ - **A release sentinel now blocks a half-published release.** Releasing writes the same facts into six carriers — `package.json`'s `version`, `build-pack.ts`'s PE version, the CHANGELOG section, the CHANGELOG link definitions, the package size in all three READMEs, and the git state. Missing any one produces no compile error and no test failure; it produces a release where the tarball on npm describes itself wrongly, discovered only after the version number is permanently spent.
28
+
29
+ `bun run scripts/release-sentinel.ts` turns "do the six agree" into a decidable question. One run, sixteen checks, exit 1 with a list of what to fix. It measures the tarball for real (`bun pm pack` → `tar -xzf` → sum the bytes) rather than trusting the number written in the docs, because that number changes on **every commit that adds, removes, or edits a shipped file** — including edits to the READMEs themselves.
30
+
31
+ **It is deliberately not a fourth gate stage.** Every stage of `gate.ts` asserts "failure = the code is broken". Two of the sentinel's checks fail legitimately during normal development — a dirty working tree, and a CHANGELOG with no section for a version that has not been released yet. Putting that in a gate that runs on every commit only teaches people to reach for `--skip`, and a criterion that gets habitually skipped does not exist.
32
+
33
+ - **The static half of that contract now runs on every `bun test`.** `tests/readme-contract.test.ts` (31 assertions) covers what can be judged without packing or touching git: all ten `FLAG_*` and five `BROWSER_UI_*` bits mentioned in all three READMEs, all ten `exports` subpaths mentioned, the three files structurally isomorphic, no references to files outside the publish surface, and no self-version literals. The split between the two layers has exactly one criterion: **will this check fail legitimately during normal development?** If yes, sentinel only.
34
+
35
+ Shared logic lives in `scripts/lib/readme-contract.ts` so the test and the sentinel cannot disagree — two implementations of the same predicate is the one failure mode where a person ends up bypassing whichever is stricter.
36
+
37
+ - **Six README defects fixed, then the checks that would have caught them were written.** The two worst:
38
+
39
+ | Defect | Before | Now |
40
+ | --- | --- | --- |
41
+ | `FLAG_TITLEBAR_RESERVE` (256) / `FLAG_NATIVE_CAPTIONS` (512) | **absent from the flag table entirely** — the latter is the only way Snap Layouts can trigger | documented, including why the first is superseded by the second |
42
+ | All three READMEs | pointed readers at `AGENTS.md` for "shape A / shape B" — gitignored, not in `files`, so **unreachable for anyone who installed the package** from the day those sentences were written | rewritten self-contained |
43
+ | `ackOf()`, `titleBarCss()`, `detectIconSet()`, `captions: "native"` | never mentioned | documented |
44
+ | Default glyph set | "buttons default to geometric glyphs; setting `icons: \"fluent\"` switches to Segoe Fluent Icons" — **backwards since 0.6.1** | `detectIconSet()` picks `fluent` by default, `unicode` only as fallback |
45
+ | `types` subpath constants | `STATE_*` only | three named constants plus the `CAPTION_*` trio, and why the caption order is load-bearing |
46
+ | Tarball size | 1.37 MB unpacked | 1.39 MB — measured, not estimated |
47
+
48
+ The glyph row was the worst of the six: a reader following it would set `icons: "fluent"` to *get* the Win 11 glyphs, when that had been the default since 0.6.1 — the documentation described the opposite of what the code did. Two `about 0.4 s` / `about two seconds` figures were removed rather than updated; they drift with the machine and the reader cannot verify them. Two first-person phrasings were also removed from the Chinese READMEs (`各替我省一次调用` → `各能省掉一次调用`).
49
+
50
+ Then the checks above were written, and verified in both directions: four mutations were injected into the real documents — deleting a flag mention, adding a heading to one file, corrupting one file's size, adding an `AGENTS.md` reference — and each turned exactly the expected assertion red. All four were reverted and the suite returned green.
51
+
52
+ - **`dist/libshell.dll` changed** — `shell.cc` gained the two exports and the `g_frameless_live` predicates (`1,026,560 B`). The declared symbol count grew by two, tracked in `tests/native-surface.test.ts`. Three pre-existing static assertions in `tests/nav-guard.test.ts` were widened from `g_frameless` to `g_frameless(_live)?`; each carries a comment saying why the live variant satisfies the original intent. The caption-ordering assertion also moved its anchor off `WEBVIEW_NATIVE_HANDLE_KIND_UI_WIDGET`, which appears in three other functions and was a substring trap waiting to misfire.
53
+
54
+ Note the self-reference: editing the READMEs changes the tarball they describe. `1.39 MB` is the figure after these edits, measured by packing and unpacking; the digits were changed last, and since `1.38` and `1.39` are the same byte length the measurement stays put.
55
+
9
56
  ## [0.6.2] - 2026-10-23
10
57
 
11
58
  ### Fixed
@@ -645,7 +692,8 @@ First public release.
645
692
  - Not yet verified in managed environments that enforce AppLocker, WDAC, or third-party EDR behaviour blocking.
646
693
  - No commercially trusted code signing: only a self-signed certificate has been exercised end to end.
647
694
 
648
- [Unreleased]: https://github.com/TiancongLx/bun-app-window/compare/v0.6.2...HEAD
695
+ [Unreleased]: https://github.com/TiancongLx/bun-app-window/compare/v0.7.0...HEAD
696
+ [0.7.0]: https://github.com/TiancongLx/bun-app-window/compare/v0.6.2...v0.7.0
649
697
  [0.6.2]: https://github.com/TiancongLx/bun-app-window/compare/v0.6.1...v0.6.2
650
698
  [0.6.1]: https://github.com/TiancongLx/bun-app-window/compare/v0.6.0...v0.6.1
651
699
  [0.6.0]: https://github.com/TiancongLx/bun-app-window/compare/v0.5.1...v0.6.0
package/README.md CHANGED
@@ -48,7 +48,7 @@ bun add bun-app-window
48
48
 
49
49
  ### What the tarball contains
50
50
 
51
- The published package holds the prebuilt `dist/libshell.dll` plus the TypeScript sources — 21 files, roughly 0.4 MB packed. The native C++ layer (`native/`) and the vendored upstream headers (`vendor/`) are **not** shipped: they are only needed to rebuild `libshell.dll`, and rebuilding requires cloning the repository. `bun run build:native` and `build:exe` are therefore repository-only scripts that do nothing useful inside `node_modules`.
51
+ The published package holds the prebuilt `dist/libshell.dll` plus the TypeScript sources — 22 files, 0.48 MB packed (1.39 MB unpacked). The native C++ layer (`native/`) and the vendored upstream headers (`vendor/`) are **not** shipped: they are only needed to rebuild `libshell.dll`, and rebuilding requires cloning the repository. `bun run build:native` and `build:exe` are therefore repository-only scripts that do nothing useful inside `node_modules`.
52
52
 
53
53
  ## Quick Start
54
54
 
@@ -79,6 +79,10 @@ Note: `createShell(opts)` only resolves and loads the native DLL without opening
79
79
 
80
80
  ## Core API
81
81
 
82
+ `ackOf(shell, rc)` turns a bare status code into `{ rc, err }` and only reads `lastError()` on
83
+ failure — the native error slot is not cleared by a successful call, so reading it afterwards
84
+ returns a stale message that looks like a fresh failure.
85
+
82
86
  ### Instance Methods & Properties
83
87
 
84
88
  | Member | Description |
@@ -97,6 +101,7 @@ Note: `createShell(opts)` only resolves and loads the native DLL without opening
97
101
  | `post(json)` | Sends a fire-and-forget JSON message from the host to the web page |
98
102
  | `setZoom(f)` / `getZoom()` | Gets or sets the page zoom factor (clamped automatically to `0.25`–`3.0`) |
99
103
  | `reload()` / `openDevTools()` / `toggleFullscreen()` | Equivalent to `Ctrl+R`, `F12`, and `F11` respectively |
104
+ | `setFrameless(frameless)` / `isFrameless()` | Switches the window form at runtime and reports the current one. Only a window **created** with `FLAG_FRAMELESS` can switch; a framed-created window refuses with `rc=-1` and a reason in `lastError()`. See "Window Flags" below |
100
105
  | `setBrowserUi(mask)` | Disables specific default browser behaviors using a bitwise OR of `BROWSER_UI_*` flags |
101
106
  | `showContextMenu(x, y, items)` | Displays a native Win32 context menu at the specified screen coordinates (see below) |
102
107
  | `pickFolder(title?)` | Opens a native folder selection dialog (synchronously blocks the calling thread; **when exposed over IPC, pair it with `timeoutMs: 0`**, see below) |
@@ -109,7 +114,7 @@ Combine via bitwise OR (`|`) and pass to `opts.flags`:
109
114
 
110
115
  | Flag | Description |
111
116
  | --- | --- |
112
- | `FLAG_FRAMELESS` | Creates a frameless window without the default system title bar, delegating title bar rendering to the web UI |
117
+ | `FLAG_FRAMELESS` | Creates a frameless window without the default system title bar, delegating title bar rendering to the web UI. A window created this way can switch back to framed at runtime with `setFrameless(false)` |
113
118
  | `FLAG_RESIZABLE` | Allows user resizing |
114
119
  | `FLAG_CENTER` | Centers the window within the active monitor work area on startup (works in both standard and frameless modes) |
115
120
  | `FLAG_TOPMOST` | Keeps the window always on top |
@@ -117,6 +122,8 @@ Combine via bitwise OR (`|`) and pass to `opts.flags`:
117
122
  | `FLAG_ACCEPT_DROP` | Enables native file and folder drag-and-drop from Windows Explorer |
118
123
  | `FLAG_START_MAXIMIZED` | Opens the window directly in a maximized state without an intermediate normal-sized frame flash |
119
124
  | `FLAG_ALLOW_EXTERNAL_NAV` | Opts out of external-navigation interception — for apps that genuinely are browsers |
125
+ | `FLAG_TITLEBAR_RESERVE` | In frameless mode, shifts the web content down by one title-bar height so the top strip belongs to the native window. Superseded by `FLAG_NATIVE_CAPTIONS`: the caption hit zone is pinned to client `y ∈ [0, 32)`, so this variant puts the page's own title bar 32 px away from where clicks are actually routed |
126
+ | `FLAG_NATIVE_CAPTIONS` | In frameless mode, draws the three caption buttons on a layered child window owned by the shell and answers `HTMAXBUTTON` there. This is the only way Snap Layouts can trigger: the WebView2 render child lives in another process and always returns `HTCLIENT`. Pair it with `mountTitleBar({ captions: "native" })` so the page reserves `46 × 3` instead of drawing its own buttons |
120
127
 
121
128
  ### External navigation is blocked by default
122
129
 
@@ -362,7 +369,9 @@ api.on("windowState", (s) => bar.setMaximized(s.maximized));
362
369
  Implementation notes:
363
370
 
364
371
  - Native dragging: Calling `shell.drag()` dispatches a native `WM_NCLBUTTONDOWN` (`HTCAPTION`) message, preserving Windows 11 Snap Layouts and double-click maximize behavior.
365
- - Theming & icons: Colors and dimensions are controlled via CSS custom properties (`--ws-tb-*`). Window control buttons default to geometric glyphs for universal compatibility; setting `icons: "fluent"` switches to `Segoe Fluent Icons` (note: missing fonts on older systems will render as missing-glyph boxes).
372
+ - Theming: Colors and dimensions are controlled via CSS custom properties (`--ws-tb-*`). `titleBarCss()` is exported if you need to inject the stylesheet yourself instead of letting `mountTitleBar` do it once.
373
+ - Glyphs: the default is `detectIconSet()`, which asks `document.fonts` for `Segoe Fluent Icons` (Win 11) or `Segoe MDL2 Assets` (Win 10) and picks `fluent` when either is present — the same code points exist in both fonts, so one table covers both systems. It only falls back to the `unicode` geometric set (`─ □ ❐ ✕`) when neither font is found. Pass `icons: "unicode" | "fluent"` to override the probe.
374
+ - Let the shell draw the buttons: pass `captions: "native"` and the component renders **no** buttons at all, reserving `46 × 3` on the right for the shell's own layered child window. Pair it with `FLAG_NATIVE_CAPTIONS` — enabling only one of the two gives you either two overlapping button sets or an empty strip that cannot be clicked. Note that clicks on those buttons arrive as non-client messages, so `bridge.minimize` / `toggleMaximize` / `close` are not called on the mouse path; the shell performs the action itself. Keyboard Tab + Enter still goes through the bridge.
366
375
 
367
376
  ### `bun-app-window/state` — Window Placement Persistence
368
377
 
@@ -462,8 +471,13 @@ must; do not put a parser in production code, because the layout is not part of
462
471
 
463
472
  ### `bun-app-window/types` — Constants, Safe for the Browser
464
473
 
465
- A zero-dependency module holding the whole constant surface: `FLAG_*`, `BROWSER_UI_*`, `RESIZE_EDGE`,
466
- `RESIZE_INDEX`, `ZOOM_LADDER`, `ZOOM_MIN`, `ZOOM_MAX`, `STATE_*`, and the related types.
474
+ A zero-dependency module holding the whole constant surface: `FLAG_*` (all ten, including
475
+ `FLAG_TITLEBAR_RESERVE` and `FLAG_NATIVE_CAPTIONS`), `BROWSER_UI_*`, `RESIZE_EDGE`, `RESIZE_INDEX`,
476
+ `resizeIndex`, `ZOOM_LADDER`, `ZOOM_MIN`, `ZOOM_MAX`, `STATE_MAXIMIZED` / `STATE_MINIMIZED` /
477
+ `STATE_FOCUSED`, and the caption geometry trio `CAPTION_BAR_HEIGHT` (32) / `CAPTION_BUTTON_WIDTH` (46)
478
+ / `CAPTION_BUTTONS` — the last of which is ordered `close`, `maximize`, `minimize` from right to left,
479
+ and that order is load-bearing: if a page draws its buttons in a different order than
480
+ `caption_button_at()` reports them, clicking "close" maximizes the window.
467
481
 
468
482
  **Use this subpath on the page side.** The main entry imports `bun:ffi` through `./native`, which a
469
483
  browser bundle cannot resolve — so `import { BROWSER_UI_ALL } from "bun-app-window"` inside page code
@@ -683,9 +697,9 @@ bun run scripts/gate.ts # bun check + bun test + consumer acceptanc
683
697
  bun run scripts/gate.ts --clean # same gates, but inside a fresh clone of HEAD
684
698
  ```
685
699
 
686
- `-Clean` reproduces the conditions a hosted runner would provide: it clones `HEAD` into `%TEMP%`, installs from the lockfile, and runs the gates there, so local artefacts (`.build/`, `node_modules/`, `vendor/webview2/`) cannot mask a missing file. It takes about two seconds. Uncommitted changes are reported and excluded — run without `-Clean` to verify the working tree as-is.
700
+ `-Clean` reproduces the conditions a hosted runner would provide: it clones `HEAD` into `%TEMP%`, installs from the lockfile, and runs the gates there, so local artefacts (`.build/`, `node_modules/`, `vendor/webview2/`) cannot mask a missing file. Uncommitted changes are reported and excluded — run without `-Clean` to verify the working tree as-is.
687
701
 
688
- The gate has three stages: `bun check`, `bun test`, and consumer acceptance (`scripts/verify-pack.ts`). The third packs the tarball, installs it into a scratch project under `%TEMP%`, type-checks that project, and runs `scripts/consumer-probe.ts` against the installed package — assertions covering every subpath, the DLL actually being `dlopen`-ed out of `node_modules`, and `libInfo.source === "package"` (the probe reports its own count). It runs offline in about 0.4 s and denies by default: a file packed without being listed in the allow-list fails the gate. Pass `-SkipPack` only when the gate itself is what you are changing.
702
+ The gate has three stages: `bun check`, `bun test`, and consumer acceptance (`scripts/verify-pack.ts`). The third packs the tarball, installs it into a scratch project under `%TEMP%`, type-checks that project, and runs `scripts/consumer-probe.ts` against the installed package — assertions covering every subpath, the DLL actually being `dlopen`-ed out of `node_modules`, and `libInfo.source === "package"` (the probe reports its own count). It runs offline and denies by default: a file packed without being listed in the allow-list fails the gate. Pass `-SkipPack` only when the gate itself is what you are changing.
689
703
 
690
704
  The underlying commands remain available directly:
691
705
 
@@ -695,9 +709,9 @@ bun test # Run automated test suite (tests/)
695
709
  bun run demo # Launch interactive self-test demo (requires desktop session)
696
710
  ```
697
711
 
698
- Note that `bun run demo` and the compiled-executable acceptance (shape B in `AGENTS.md`) both need an interactive desktop, so they were never CI-capable — they are local-only by construction.
712
+ Note that `bun run demo` and the single-file `.exe` acceptance both need an interactive desktop, so they were never CI-capable — they are local-only by construction. For the `.exe`, the whole check is: compile it, copy the result into an empty directory, launch it, and confirm the window comes up with `libInfo.source === "embedded"`.
699
713
 
700
- Whenever `src/shell/` or `package.json` is modified, the consumer acceptance stage already covers the installed-package path (shape A in `AGENTS.md`); it runs as part of the gate, so there is nothing to remember. Only shape B — building the single-file `.exe` — still needs a manual run inside a desktop session.
714
+ Whenever `src/shell/` or `package.json` is modified, the consumer acceptance stage already covers the installed-package path; it runs as part of the gate, so there is nothing to remember. Only the `.exe` build still needs a manual run inside a desktop session.
701
715
 
702
716
  For window enumeration (`EnumWindows`) or automated screenshot scripts, filter by the corresponding Win32 window class name:
703
717
 
package/README.zh-CN.md CHANGED
@@ -48,7 +48,7 @@ bun add bun-app-window
48
48
 
49
49
  ### 包内包含什么
50
50
 
51
- 发布的 tarball 只有预编译的 `dist/libshell.dll` 与 TypeScript 源码,共 21 个文件(约 0.4 MB)。原生 C++ 层(`native/`)与上游 vendored 头文件(`vendor/`)**不随包分发**:它们只在重编 `libshell.dll` 时需要,而重编必须 clone 仓库。因此 `bun run build:native` 与 `build:exe` 是本仓库专用脚本,装进 `node_modules` 后执行没有意义。
51
+ 发布的 tarball 只有预编译的 `dist/libshell.dll` 与 TypeScript 源码,共 22 个文件(0.48 MB,解包后 1.39 MB)。原生 C++ 层(`native/`)与上游 vendored 头文件(`vendor/`)**不随包分发**:它们只在重编 `libshell.dll` 时需要,而重编必须 clone 仓库。因此 `bun run build:native` 与 `build:exe` 是本仓库专用脚本,装进 `node_modules` 后执行没有意义。
52
52
 
53
53
  ## 快速开始
54
54
 
@@ -79,6 +79,8 @@ shell.setTitle("Doing work…");
79
79
 
80
80
  ## 核心 API
81
81
 
82
+ `ackOf(shell, rc)` 把裸状态码转成 `{ rc, err }`,并且**只在失败时才读 `lastError()`**——原生错误槽不会被成功调用清空,成功之后再读会拿到上一条残留,看起来像新的失败。
83
+
82
84
  ### 实例方法与属性
83
85
 
84
86
  | 成员 | 说明 |
@@ -97,6 +99,7 @@ shell.setTitle("Doing work…");
97
99
  | `post(json)` | 宿主向页面单向投递 JSON 消息,异步发送且不等待返回结果 |
98
100
  | `setZoom(f)` / `getZoom()` | 设置或获取页面缩放比例(有效范围 `0.25`–`3.0`,越界值自动钳制) |
99
101
  | `reload()` / `openDevTools()` / `toggleFullscreen()` | 页面重载、打开开发者工具、切换全屏(分别对应 `Ctrl+R` / `F12` / `F11`) |
102
+ | `setFrameless(frameless)` / `isFrameless()` | 运行时切换窗口形态并读取当前形态。只有**创建时**给了 `FLAG_FRAMELESS` 的窗口能切;带边框起点的窗口返回 `rc=-1`,理由在 `lastError()`。详见下文「窗口标志位」 |
100
103
  | `setBrowserUi(mask)` | 禁用指定的浏览器默认行为,传入 `BROWSER_UI_*` 标志位的按位或组合 |
101
104
  | `showContextMenu(x, y, items)` | 在指定屏幕坐标弹出 Win32 原生上下文菜单(详见下文用法) |
102
105
  | `pickFolder(title?)` | 调用原生文件夹选择对话框(同步阻塞当前调用线程;**经 IPC 暴露时务必配 `timeoutMs: 0`**,见下文) |
@@ -109,7 +112,7 @@ shell.setTitle("Doing work…");
109
112
 
110
113
  | 标志位 | 作用说明 |
111
114
  | --- | --- |
112
- | `FLAG_FRAMELESS` | 无边框窗口模式,移除系统默认标题栏,交由前端页面自行绘制 |
115
+ | `FLAG_FRAMELESS` | 无边框窗口模式,移除系统默认标题栏,交由前端页面自行绘制。以这种方式创建的窗口可在运行时用 `setFrameless(false)` 切回带边框 |
113
116
  | `FLAG_RESIZABLE` | 允许用户调整窗口大小 |
114
117
  | `FLAG_CENTER` | 启动时在当前显示器工作区居中显示(有边框与无边框模式均生效) |
115
118
  | `FLAG_TOPMOST` | 窗口始终置顶 |
@@ -117,6 +120,8 @@ shell.setTitle("Doing work…");
117
120
  | `FLAG_ACCEPT_DROP` | 允许窗口接收外部文件或文件夹拖放事件 |
118
121
  | `FLAG_START_MAXIMIZED` | 窗口首次显示即处于最大化状态,避免从普通尺寸跳变到最大化的中间闪烁帧 |
119
122
  | `FLAG_ALLOW_EXTERNAL_NAV` | 放弃外部导航拦截——用于本身就是浏览器的应用 |
123
+ | `FLAG_TITLEBAR_RESERVE` | 无边框模式下把网页内容整体下移一个标题栏高度,让顶部那条归原生窗口。**已被 `FLAG_NATIVE_CAPTIONS` 取代**:caption 命中区固定在客户区 `y ∈ [0, 32)`,这条路会把页面自绘的标题栏放到与点击真正落点相差 32 px 的位置 |
124
+ | `FLAG_NATIVE_CAPTIONS` | 无边框模式下,由壳层自己的 layered 子窗口绘制三个 caption 按钮并在那里应答 `HTMAXBUTTON`。这是 Snap Layouts 唯一能触发的途径:WebView2 的渲染子窗口在另一个进程里,恒回 `HTCLIENT`。必须与 `mountTitleBar({ captions: "native" })` 配套,页面才会留出 `46 × 3` 而不是自己画一套 |
120
125
 
121
126
  ### 外部导航默认被拦截
122
127
 
@@ -188,7 +193,7 @@ const { code } = await bundlePage({ entry: "src/ui/page.ts" });
188
193
  const app = await createApp({ html, page: code, service });
189
194
  ```
190
195
 
191
- `App` 上另有两个成员,各替我省一次调用和一条子路径导入:
196
+ `App` 上另有两个成员,各能省掉一次调用和一条子路径导入:
192
197
 
193
198
  - `app.debug` —— 与 `debugOf(app.shell)` 返回同一个对象。同样不在稳定性承诺范围内。
194
199
  - `app.attachPlacement(file)` —— 把位置持久化接到骨架的**唯一**事件泵上。自己调
@@ -353,7 +358,9 @@ api.on("windowState", (s) => bar.setMaximized(s.maximized));
353
358
  实现细节:
354
359
 
355
360
  - 原生拖拽体验:拖拽操作通过调用 `shell.drag()` 交由底层发送 `WM_NCLBUTTONDOWN`(`HTCAPTION`)消息处理,完整保留 Windows 11 贴边分屏(Snap Layouts)与双击最大化等系统级行为。
356
- - 样式定制与图标方案:颜色与尺寸均通过 CSS 变量(`--ws-tb-*`)控制,覆盖变量即可实现换肤。按钮图标默认采用兼容性更好的几何矢量符号;若配置 `icons: "fluent"` 则切换为 `Segoe Fluent Icons` 字体渲染(注意:若目标系统缺失该字体,按钮将回退显示为缺字方框)。
361
+ - 样式定制:颜色与尺寸均通过 CSS 变量(`--ws-tb-*`)控制,覆盖变量即可实现换肤。若需要自己注入样式表而不是让 `mountTitleBar` 注入一次,可单独调用导出的 `titleBarCss()`。
362
+ - 字形选择:默认走 `detectIconSet()`,它向 `document.fonts` 询问 `Segoe Fluent Icons`(Win 11)或 `Segoe MDL2 Assets`(Win 10),有一个在就用 `fluent`——两套字体的码位相同,一张表覆盖两代系统。只有两个字体都探测不到时才退回 `unicode` 几何符号(`─ □ ❐ ✕`)。要覆盖探测结果就显式传 `icons: "unicode" | "fluent"`。
363
+ - 交给壳层画按钮:传 `captions: "native"`,组件**一个按钮都不渲染**,只在右侧留出 `46 × 3` 给壳层的 layered 子窗口。必须与 `FLAG_NATIVE_CAPTIONS` 配套——只开一半的结果是两套按钮叠在一起,或者一条点不到的空区。注意这三个按钮上的鼠标点击走**非客户区**消息,所以 `bridge.minimize` / `toggleMaximize` / `close` 在鼠标这条路上不会被调用,动作由壳层自己执行;键盘 Tab + Enter 仍然经过 bridge。
357
364
 
358
365
  ### `bun-app-window/state` — 窗口位置与尺寸持久化
359
366
 
@@ -445,8 +452,7 @@ d.hittest(x, y);
445
452
 
446
453
  ### `bun-app-window/types` — 常量面,浏览器安全
447
454
 
448
- 一个零依赖模块,承载全部常量:`FLAG_*`、`BROWSER_UI_*`、`RESIZE_EDGE`、`RESIZE_INDEX`、
449
- `ZOOM_LADDER`、`ZOOM_MIN`、`ZOOM_MAX`、`STATE_*` 及相关类型。
455
+ 一个零依赖模块,承载全部常量:`FLAG_*`(十个全在,含 `FLAG_TITLEBAR_RESERVE` 与 `FLAG_NATIVE_CAPTIONS`)、`BROWSER_UI_*`、`RESIZE_EDGE`、`RESIZE_INDEX`、`resizeIndex`、`ZOOM_LADDER`、`ZOOM_MIN`、`ZOOM_MAX`、`STATE_MAXIMIZED` / `STATE_MINIMIZED` / `STATE_FOCUSED`,以及 caption 几何三件套 `CAPTION_BAR_HEIGHT`(32)/ `CAPTION_BUTTON_WIDTH`(46)/ `CAPTION_BUTTONS`——最后这个的顺序是「从右往左 close、maximize、minimize」,而这个顺序是有承重的:页面画的按钮顺序若与 `caption_button_at()` 的报告顺序不一致,点「关闭」会变成最大化。
450
456
 
451
457
  **页面侧请用这个子路径。** 主入口经 `./native` 引入 `bun:ffi`,浏览器打包器解析不了它——
452
458
  所以在页面代码里写 `import { BROWSER_UI_ALL } from "bun-app-window"` 会在打包期失败。
@@ -661,9 +667,9 @@ bun run scripts/gate.ts # 对当前工作区跑三段门禁
661
667
  bun run scripts/gate.ts --clean # 同上,但在 HEAD 的干净副本里跑
662
668
  ```
663
669
 
664
- `-Clean` 复现了托管 runner 的条件:把 `HEAD` clone 到 `%TEMP%`,按 lockfile 安装依赖,再在其中跑门禁。这样本地产物(`.build/`、`node_modules/`、`vendor/webview2/`)就无法掩盖缺失的文件。耗时约两秒。未提交改动会被提示并排除在干净副本之外——如需验证工作区现状,请去掉 `-Clean`。
670
+ `-Clean` 复现了托管 runner 的条件:把 `HEAD` clone 到 `%TEMP%`,按 lockfile 安装依赖,再在其中跑门禁。这样本地产物(`.build/`、`node_modules/`、`vendor/webview2/`)就无法掩盖缺失的文件。未提交改动会被提示并排除在干净副本之外——如需验证工作区现状,请去掉 `-Clean`。
665
671
 
666
- 门禁分三段:`bun check`、`bun test`,以及消费方验收(`scripts/verify-pack.ts`)。第三段会把 tarball 打进 `%TEMP%`,装进一个临时工程,对那个工程做类型检查,再运行 `scripts/consumer-probe.ts`——断言覆盖全部子路径、DLL 是否真从 `node_modules` 里被 `dlopen`、以及 `libInfo.source` 是否为 `package`(条数由探针自报)。离线可跑,约 0.4 秒,且默认拒绝:任何没在允许清单里登记的文件被打进包,门禁就红。只有在改动门禁自身时才用 `-SkipPack`。
672
+ 门禁分三段:`bun check`、`bun test`,以及消费方验收(`scripts/verify-pack.ts`)。第三段会把 tarball 打进 `%TEMP%`,装进一个临时工程,对那个工程做类型检查,再运行 `scripts/consumer-probe.ts`——断言覆盖全部子路径、DLL 是否真从 `node_modules` 里被 `dlopen`、以及 `libInfo.source` 是否为 `package`(条数由探针自报)。离线可跑,且默认拒绝:任何没在允许清单里登记的文件被打进包,门禁就红。只有在改动门禁自身时才用 `-SkipPack`。
667
673
 
668
674
  底层命令仍可直接调用:
669
675
 
@@ -673,9 +679,9 @@ bun test # 运行自动化测试套件(位于 tests/)
673
679
  bun run demo # 启动交互式自检示例程序(需在桌面会话下运行)
674
680
  ```
675
681
 
676
- 需要注意:`bun run demo` 与「编译为单文件可执行程序」的验收(`AGENTS.md` 中的形态 B)都需要交互式桌面,本就不具备 CI 可行性,属于构造上的本地专属项。
682
+ 注意:`bun run demo` 与单文件 `.exe` 的验收都需要交互式桌面,因此两者都不具备 CI 可行性,属于构造上的本地专属项。`.exe` 那条的完整判据是:编译 → 把产物拷进一个空目录 → 启动 → 确认窗口起得来,且 `libInfo.source` 报 `embedded`。
677
683
 
678
- 注意:每当修改 `src/shell/` 或 `package.json` 后,「作为外部工程依赖安装」这条链路(`AGENTS.md` 中的形态 A)已由消费方验收阶段自动覆盖——它随门禁一起跑,不需要额外记得。只剩形态 B(编译单文件 `.exe`)仍需在桌面会话下手动执行一次。
684
+ 每当修改 `src/shell/` 或 `package.json` 后,「作为外部工程依赖安装」这条链路已由消费方验收阶段自动覆盖——它随门禁一起跑,不需要额外记得。只剩 `.exe` 构建仍需在桌面会话下手动执行一次。
679
685
 
680
686
  自动化测试或截图脚本若需通过 Win32 `EnumWindows` 过滤窗口句柄,请根据窗口模式匹配对应的窗口类名(Window Class):
681
687
 
package/README.zh-TW.md CHANGED
@@ -48,7 +48,7 @@ bun add bun-app-window
48
48
 
49
49
  ### 套件內容包含什麼
50
50
 
51
- 發佈的 tarball 只有預先編譯的 `dist/libshell.dll` 與 TypeScript 原始碼,共 21 個檔案(約 0.4 MB)。原生 C++ 層(`native/`)與上游 vendored 標頭檔(`vendor/`)**不隨套件散布**:它們只在重新編譯 `libshell.dll` 時需要,而重新編譯必須 clone 儲存庫。因此 `bun run build:native` 與 `build:exe` 是本儲存庫專用指令碼,安裝進 `node_modules` 後執行沒有意義。
51
+ 發佈的 tarball 只有預先編譯的 `dist/libshell.dll` 與 TypeScript 原始碼,共 22 個檔案(0.48 MB,解包後 1.39 MB)。原生 C++ 層(`native/`)與上游 vendored 標頭檔(`vendor/`)**不隨套件散布**:它們只在重新編譯 `libshell.dll` 時需要,而重新編譯必須 clone 儲存庫。因此 `bun run build:native` 與 `build:exe` 是本儲存庫專用指令碼,安裝進 `node_modules` 後執行沒有意義。
52
52
 
53
53
  ## 快速開始
54
54
 
@@ -79,6 +79,8 @@ shell.setTitle("Doing work…");
79
79
 
80
80
  ## 核心 API
81
81
 
82
+ `ackOf(shell, rc)` 把裸狀態碼轉成 `{ rc, err }`,並且**只在失敗時才讀 `lastError()`**——原生錯誤槽不會被成功呼叫清空,成功之後再讀會拿到上一條殘留,看起來像新的失敗。
83
+
82
84
  ### 實例方法與屬性
83
85
 
84
86
  | 成員 | 說明 |
@@ -97,6 +99,7 @@ shell.setTitle("Doing work…");
97
99
  | `post(json)` | 宿主向頁面單向投遞 JSON 訊息,非同步發送且不等待回傳結果 |
98
100
  | `setZoom(f)` / `getZoom()` | 設定或取得頁面縮放比例(有效範圍 `0.25`–`3.0`,越界值自動鉗制) |
99
101
  | `reload()` / `openDevTools()` / `toggleFullscreen()` | 頁面重新載入、開啟開發人員工具、切換全螢幕(分別對應 `Ctrl+R` / `F12` / `F11`) |
102
+ | `setFrameless(frameless)` / `isFrameless()` | 執行時期切換視窗形態並讀取目前形態。只有**建立時**給了 `FLAG_FRAMELESS` 的視窗能切;帶邊框起點的視窗回傳 `rc=-1`,理由在 `lastError()`。詳見下文「視窗旗標」 |
100
103
  | `setBrowserUi(mask)` | 停用指定的瀏覽器預設行為,傳入 `BROWSER_UI_*` 旗標的按位元 OR 組合 |
101
104
  | `showContextMenu(x, y, items)` | 於指定螢幕座標彈出 Win32 原生右鍵快顯功能表(詳見下文用法) |
102
105
  | `pickFolder(title?)` | 呼叫原生資料夾選擇對話方塊(同步阻塞目前呼叫執行緒;**經 IPC 暴露時務必搭配 `timeoutMs: 0`**,見下文) |
@@ -109,7 +112,7 @@ shell.setTitle("Doing work…");
109
112
 
110
113
  | 旗標 | 作用說明 |
111
114
  | --- | --- |
112
- | `FLAG_FRAMELESS` | 無邊框視窗模式,移除系統預設標題列,交由前端頁面自行繪製 |
115
+ | `FLAG_FRAMELESS` | 無邊框視窗模式,移除系統預設標題列,交由前端頁面自行繪製。以這種方式建立的視窗可在執行時期用 `setFrameless(false)` 切回帶邊框 |
113
116
  | `FLAG_RESIZABLE` | 允許使用者調整視窗大小 |
114
117
  | `FLAG_CENTER` | 啟動時於目前顯示器工作區置中顯示(有邊框與無邊框模式均生效) |
115
118
  | `FLAG_TOPMOST` | 視窗保持最上層顯示 |
@@ -117,6 +120,8 @@ shell.setTitle("Doing work…");
117
120
  | `FLAG_ACCEPT_DROP` | 允許視窗接收外部檔案或資料夾拖放事件 |
118
121
  | `FLAG_START_MAXIMIZED` | 視窗首次顯示即處於最大化狀態,避免從一般尺寸跳變至最大化的中間閃爍畫格 |
119
122
  | `FLAG_ALLOW_EXTERNAL_NAV` | 放棄外部導覽攔截——用於本身就是瀏覽器的應用程式 |
123
+ | `FLAG_TITLEBAR_RESERVE` | 無邊框模式下把網頁內容整體下移一個標題列高度,讓頂端那一條歸原生視窗。**已被 `FLAG_NATIVE_CAPTIONS` 取代**:caption 命中區固定在客戶區 `y ∈ [0, 32)`,這條路會把頁面自繪的標題列放到與點擊真正落點相差 32 px 的位置 |
124
+ | `FLAG_NATIVE_CAPTIONS` | 無邊框模式下,由殼層自己的 layered 子視窗繪製三個 caption 按鈕並在那裡應答 `HTMAXBUTTON`。這是 Snap Layouts 唯一能觸發的途徑:WebView2 的渲染子視窗在另一個處理序裡,恆回 `HTCLIENT`。必須與 `mountTitleBar({ captions: "native" })` 搭配,頁面才會留出 `46 × 3` 而不是自己畫一套 |
120
125
 
121
126
  ### 外部導覽預設被攔截
122
127
 
@@ -187,7 +192,7 @@ const { code } = await bundlePage({ entry: "src/ui/page.ts" });
187
192
  const app = await createApp({ html, page: code, service });
188
193
  ```
189
194
 
190
- `App` 上另有兩個成員,各替我省一次呼叫和一條子路徑匯入:
195
+ `App` 上另有兩個成員,各能省掉一次呼叫和一條子路徑匯入:
191
196
 
192
197
  - `app.debug` —— 與 `debugOf(app.shell)` 回傳同一個物件。同樣不在穩定性承諾範圍內。
193
198
  - `app.attachPlacement(file)` —— 把位置持久化接到骨架的**唯一**事件泵上。自己呼叫
@@ -352,7 +357,9 @@ api.on("windowState", (s) => bar.setMaximized(s.maximized));
352
357
  實作細節:
353
358
 
354
359
  - 原生拖曳體驗:拖曳操作透過呼叫 `shell.drag()` 交由底層發送 `WM_NCLBUTTONDOWN`(`HTCAPTION`)訊息處理,完整保留 Windows 11 貼齊版面配置(Snap Layouts)與連按兩下最大化等系統級行為。
355
- - 樣式自訂與圖示方案:色彩與尺寸均透過 CSS 變數(`--ws-tb-*`)控制,覆寫變數即可實作佈景主題切換。按鈕圖示預設採用相容性較佳的幾何向量符號;若設定 `icons: "fluent"` 則切換為 `Segoe Fluent Icons` 字型渲染(注意:若目標系統缺少該字型,按鈕將退回顯示為缺字方框)。
360
+ - 樣式自訂:色彩與尺寸均透過 CSS 變數(`--ws-tb-*`)控制,覆寫變數即可實作佈景主題切換。若需自行注入樣式表而不是讓 `mountTitleBar` 注入一次,可單獨呼叫匯出的 `titleBarCss()`。
361
+ - 字形選擇:預設走 `detectIconSet()`,它向 `document.fonts` 詢問 `Segoe Fluent Icons`(Win 11)或 `Segoe MDL2 Assets`(Win 10),有一個在就用 `fluent`——兩套字型的碼位相同,一張表涵蓋兩代系統。只有兩個字型都探測不到時才退回 `unicode` 幾何符號(`─ □ ❐ ✕`)。要覆寫探測結果就明確傳 `icons: "unicode" | "fluent"`。
362
+ - 交給殼層繪製按鈕:傳 `captions: "native"`,元件**一個按鈕都不渲染**,只在右側留出 `46 × 3` 給殼層的 layered 子視窗。必須與 `FLAG_NATIVE_CAPTIONS` 搭配——只開一半的結果是兩套按鈕疊在一起,或者一條點不到的空白區。注意這三個按鈕上的滑鼠點擊走**非客戶區**訊息,所以 `bridge.minimize` / `toggleMaximize` / `close` 在滑鼠這條路上不會被呼叫,動作由殼層自己執行;鍵盤 Tab + Enter 仍然經過 bridge。
356
363
 
357
364
  ### `bun-app-window/state` — 視窗位置與尺寸持久化
358
365
 
@@ -445,8 +452,7 @@ d.hittest(x, y);
445
452
 
446
453
  ### `bun-app-window/types` — 常數面,瀏覽器安全
447
454
 
448
- 一個零依賴模組,承載全部常數:`FLAG_*`、`BROWSER_UI_*`、`RESIZE_EDGE`、`RESIZE_INDEX`、
449
- `ZOOM_LADDER`、`ZOOM_MIN`、`ZOOM_MAX`、`STATE_*` 及相關型別。
455
+ 一個零依賴模組,承載全部常數:`FLAG_*`(十個全在,含 `FLAG_TITLEBAR_RESERVE` 與 `FLAG_NATIVE_CAPTIONS`)、`BROWSER_UI_*`、`RESIZE_EDGE`、`RESIZE_INDEX`、`resizeIndex`、`ZOOM_LADDER`、`ZOOM_MIN`、`ZOOM_MAX`、`STATE_MAXIMIZED` / `STATE_MINIMIZED` / `STATE_FOCUSED`,以及 caption 幾何三件套 `CAPTION_BAR_HEIGHT`(32)/ `CAPTION_BUTTON_WIDTH`(46)/ `CAPTION_BUTTONS`——最後這個的順序是「從右往左 close、maximize、minimize」,而這個順序是有承重的:頁面畫的按鈕順序若與 `caption_button_at()` 的回報順序不一致,點「關閉」會變成最大化。
450
456
 
451
457
  **頁面側請用這個子路徑。** 主入口經 `./native` 引入 `bun:ffi`,瀏覽器打包器解析不了它——
452
458
  所以在頁面程式碼裡寫 `import { BROWSER_UI_ALL } from "bun-app-window"` 會在打包期失敗。
@@ -661,9 +667,9 @@ bun run scripts/gate.ts # 對目前工作區執行三段把關
661
667
  bun run scripts/gate.ts --clean # 同上,但在 HEAD 的乾淨副本內執行
662
668
  ```
663
669
 
664
- `-Clean` 重現了託管 runner 的條件:將 `HEAD` clone 到 `%TEMP%`,依 lockfile 安裝相依套件,再於其中執行把關。如此一來,本機產物(`.build/`、`node_modules/`、`vendor/webview2/`)便無法掩蓋缺少的檔案。耗時約兩秒。未提交的變更會被提示並排除於乾淨副本之外——如需驗證工作區現況,請移除 `-Clean`。
670
+ `-Clean` 重現了託管 runner 的條件:將 `HEAD` clone 到 `%TEMP%`,依 lockfile 安裝相依套件,再於其中執行把關。如此一來,本機產物(`.build/`、`node_modules/`、`vendor/webview2/`)便無法掩蓋缺少的檔案。未提交的變更會被提示並排除於乾淨副本之外——如需驗證工作區現況,請移除 `-Clean`。
665
671
 
666
- 品質把關分三段:`bun check`、`bun test`,以及消費方驗收(`scripts/verify-pack.ts`)。第三段會把 tarball 打包到 `%TEMP%`,安裝進一個臨時專案,對該專案執行型別檢查,再執行 `scripts/consumer-probe.ts`——斷言涵蓋全部子路徑、DLL 是否真的從 `node_modules` 被 `dlopen`、以及 `libInfo.source` 是否為 `package`(條數由探針自報)。可離線執行,約 0.4 秒,且預設拒絕:任何未在允許清單登記的檔案被打進套件,把關就會失敗。只有在改動把關腳本本身時才用 `-SkipPack`。
672
+ 品質把關分三段:`bun check`、`bun test`,以及消費方驗收(`scripts/verify-pack.ts`)。第三段會把 tarball 打包到 `%TEMP%`,安裝進一個臨時專案,對該專案執行型別檢查,再執行 `scripts/consumer-probe.ts`——斷言涵蓋全部子路徑、DLL 是否真的從 `node_modules` 被 `dlopen`、以及 `libInfo.source` 是否為 `package`(條數由探針自報)。可離線執行,且預設拒絕:任何未在允許清單登記的檔案被打進套件,把關就會失敗。只有在改動把關腳本本身時才用 `-SkipPack`。
667
673
 
668
674
  底層指令仍可直接呼叫:
669
675
 
@@ -673,9 +679,9 @@ bun test # 執行自動化測試套件(位於 tests/)
673
679
  bun run demo # 啟動互動式自我檢測範例程式(需於桌面工作階段下執行)
674
680
  ```
675
681
 
676
- 需注意:`bun run demo` 與「編譯為單一可執行檔」的驗收(`AGENTS.md` 中的形態 B)皆需要互動式桌面,本就不具備 CI 可行性,屬於構造上的本機專屬項目。
682
+ 需注意:`bun run demo` 與單一 `.exe` 的驗收都需要互動式桌面,因此兩者都不具備 CI 可行性,屬於構造上的本機專屬項目。`.exe` 那條的完整判據是:編譯 → 把產物複製進一個空目錄 → 啟動 → 確認視窗起得來,且 `libInfo.source` 回報 `embedded`。
677
683
 
678
- 注意:每當修改 `src/shell/` 或 `package.json` 後,「作為外部專案相依套件安裝」這條鏈路(`AGENTS.md` 中的形態 A)已由消費方驗收階段自動涵蓋——它隨品質把關一起執行,不需要額外記得。只剩形態 B(編譯單一 `.exe`)仍需於桌面工作階段手動執行一次。
684
+ 每當修改 `src/shell/` 或 `package.json` 後,「作為外部專案相依套件安裝」這條鏈路已由消費方驗收階段自動涵蓋——它隨品質把關一起執行,不需要額外記得。只剩 `.exe` 建置仍需於桌面工作階段手動執行一次。
679
685
 
680
686
  自動化測試或截圖腳本若需透過 Win32 `EnumWindows` 過濾視窗控制代碼(HWND),請依據視窗模式比對對應的視窗類別名稱(Window Class):
681
687
 
package/dist/libshell.dll CHANGED
Binary file
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "bun-app-window",
4
- "version": "0.6.2",
4
+ "version": "0.7.0",
5
5
  "description": "Native WebView2 window shell for Bun — frameless or system-decorated windows, DPI-aware geometry, typed IPC, system-accurate title bar",
6
6
  "type": "module",
7
7
  "license": "MIT",
@@ -226,6 +226,22 @@ export type Shell = {
226
226
  openDevTools(): number;
227
227
  /** 切换全屏(等同 F11);全屏与最大化不同,连边框与任务栏一起让出 */
228
228
  toggleFullscreen(): number;
229
+
230
+ /**
231
+ * 运行时切换窗口形态(ADR 0019)。`frameless = false` 让系统标题栏回来。
232
+ *
233
+ * **只在创建期给了 `FLAG_FRAMELESS` 时可用**:带边框起点是 webview 自建的窗口,
234
+ * 我们没有它的 `WM_NCCALCSIZE` 控制权。那种情况下返回 -1,理由在 `lastError()`。
235
+ *
236
+ * 生效是异步的(投到 UI 线程)。确认方式两条:`isFrameless()` 读缓存,或页面
237
+ * 监听 `shell-msg` 的 `win` 消息——它的 `frameless` 字段会在切换后推一次。
238
+ * 全屏期间调用会被拒:`toggleFullscreen` 自带样式位与矩形的缓存,两者同时改
239
+ * 会让退出全屏还原成一个混合体。
240
+ */
241
+ setFrameless(frameless: boolean): number;
242
+ /** 当前运行时形态。读缓存,无窗口调用;未启动时回创建期形态 */
243
+ isFrameless(): boolean;
244
+
229
245
  /** 停止并释放窗口(可重复调用) */
230
246
  stop(): void;
231
247
 
@@ -336,6 +352,8 @@ export async function createShell(opts: ShellOptions): Promise<Shell> {
336
352
  reload: () => symbols.shell_reload(),
337
353
  openDevTools: () => symbols.shell_devtools(),
338
354
  toggleFullscreen: () => symbols.shell_toggle_fullscreen(),
355
+ setFrameless: (frameless) => symbols.shell_set_frameless(frameless ? 1 : 0),
356
+ isFrameless: () => symbols.shell_is_frameless() !== 0,
339
357
 
340
358
  stop() {
341
359
  if (stopped) return;
@@ -10,8 +10,9 @@ import type { Pointer } from "bun:ffi";
10
10
  /**
11
11
  * 打开 libshell.dll。
12
12
  *
13
- * 导出面说明:DLL 实际导出 34 个符号,其中 `shell_start` 是 1.x 遗留入口,
14
- * TS 侧不声明、也不对外承诺(故下方声明 33 个);`shell_debug_*` / `shell_self_*` 属排障面。
13
+ * 导出面说明:`shell_start` 是 1.x 遗留入口,TS 侧不声明、也不对外承诺;
14
+ * `shell_debug_*` / `shell_self_*` 属排障面。声明总数由
15
+ * `tests/native-surface.test.ts` 的 `DECLARED` 钉住——本文不写数字,避免随增删漂移。
15
16
  */
16
17
  export function openNativeLib(path: string) {
17
18
  return dlopen(path, {
@@ -57,6 +58,10 @@ export function openNativeLib(path: string) {
57
58
  shell_reload: { args: [], returns: FFIType.i32 },
58
59
  shell_devtools: { args: [], returns: FFIType.i32 },
59
60
  shell_toggle_fullscreen: { args: [], returns: FFIType.i32 },
61
+ // 运行时形态切换(ADR 0019)。只在创建期给了 SHELL_FLAG_FRAMELESS 时可用,
62
+ // 否则 set 返回 -1 并把理由写进 shell_error_text;is 恒回创建期形态。
63
+ shell_set_frameless: { args: [FFIType.i32], returns: FFIType.i32 },
64
+ shell_is_frameless: { args: [], returns: FFIType.i32 },
60
65
 
61
66
  // 排障面:不进稳定契约
62
67
  shell_debug_hittest: { args: [FFIType.i32, FFIType.i32], returns: FFIType.i32 },