cj-tauri 2.0.0 → 2.1.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.
Files changed (158) hide show
  1. package/README.md +113 -99
  2. package/bin/cj-tauri.js +287 -287
  3. package/cjpm.toml +63 -52
  4. package/cli/cjpm.toml +22 -22
  5. package/cli/src/doctor.cj +308 -306
  6. package/cli/src/main.cj +129 -129
  7. package/cli/src/manifest.cj +632 -558
  8. package/cli/src/manifest_test.cj +87 -0
  9. package/cli/src/project.cj +770 -770
  10. package/cli/src/resolve.cj +348 -348
  11. package/cli/src/scaffold.cj +369 -369
  12. package/cli/src/sync.cj +526 -526
  13. package/cli/src/util.cj +168 -168
  14. package/cli/templates/app/README.md +51 -48
  15. package/cli/templates/app/assets/capabilities/default.json +7 -7
  16. package/cli/templates/app/assets/ui/index.html +120 -118
  17. package/cli/templates/app/cj-tauri.toml +32 -22
  18. package/cli/templates/app/cjpm.toml +34 -34
  19. package/cli/templates/app/src/main.cj +101 -90
  20. package/cli/templates/app-react/README.md +56 -56
  21. package/cli/templates/app-react/assets/capabilities/default.json +7 -7
  22. package/cli/templates/app-react/assets/ui/index.html +25 -25
  23. package/cli/templates/app-react/assets/ui/package.json +19 -19
  24. package/cli/templates/app-react/assets/ui/src/App.css +67 -67
  25. package/cli/templates/app-react/assets/ui/src/App.jsx +124 -124
  26. package/cli/templates/app-react/assets/ui/src/main.jsx +6 -6
  27. package/cli/templates/app-react/assets/ui/vite.config.js +24 -24
  28. package/cli/templates/app-react/cj-tauri.toml +33 -23
  29. package/cli/templates/app-react/cjpm.toml +34 -34
  30. package/cli/templates/app-react/src/main.cj +91 -91
  31. package/cli/templates/app-vue/README.md +56 -56
  32. package/cli/templates/app-vue/assets/capabilities/default.json +7 -7
  33. package/cli/templates/app-vue/assets/ui/index.html +25 -25
  34. package/cli/templates/app-vue/assets/ui/package.json +18 -18
  35. package/cli/templates/app-vue/assets/ui/src/App.vue +176 -176
  36. package/cli/templates/app-vue/assets/ui/src/main.js +4 -4
  37. package/cli/templates/app-vue/assets/ui/vite.config.js +24 -24
  38. package/cli/templates/app-vue/cj-tauri.toml +33 -23
  39. package/cli/templates/app-vue/cjpm.toml +34 -34
  40. package/cli/templates/app-vue/src/main.cj +91 -91
  41. package/cli/templates/ohos-skeleton/AppScope/app.json5 +13 -13
  42. package/cli/templates/ohos-skeleton/AppScope/resources/base/element/string.json +8 -8
  43. package/cli/templates/ohos-skeleton/AppScope/resources/base/media/layered_image.json +6 -6
  44. package/cli/templates/ohos-skeleton/README.md +84 -79
  45. package/cli/templates/ohos-skeleton/build-hap.sh +15 -0
  46. package/cli/templates/ohos-skeleton/build-profile.json5 +46 -46
  47. package/cli/templates/ohos-skeleton/entry/build-profile.json5 +26 -26
  48. package/cli/templates/ohos-skeleton/entry/cjpm.toml +51 -43
  49. package/cli/templates/ohos-skeleton/entry/hvigorfile.ts +6 -6
  50. package/cli/templates/ohos-skeleton/entry/oh-package.json5 +9 -9
  51. package/cli/templates/ohos-skeleton/entry/src/main/cangjie/ability_stage.cj +17 -17
  52. package/cli/templates/ohos-skeleton/entry/src/main/cangjie/app_host.cj +94 -94
  53. package/cli/templates/ohos-skeleton/entry/src/main/cangjie/dialog_host.cj +217 -0
  54. package/cli/templates/ohos-skeleton/entry/src/main/cangjie/index.cj +527 -167
  55. package/cli/templates/ohos-skeleton/entry/src/main/cangjie/main_ability.cj +139 -139
  56. package/cli/templates/ohos-skeleton/entry/src/main/module.json5 +57 -49
  57. package/cli/templates/ohos-skeleton/entry/src/main/resources/base/element/color.json +8 -8
  58. package/cli/templates/ohos-skeleton/entry/src/main/resources/base/element/string.json +16 -16
  59. package/cli/templates/ohos-skeleton/entry/src/main/resources/base/media/layered_image.json +6 -6
  60. package/cli/templates/ohos-skeleton/entry/src/main/resources/base/profile/main_pages.json +2 -2
  61. package/cli/templates/ohos-skeleton/entry/src/main/resources/rawfile/capabilities.json +6 -6
  62. package/cli/templates/ohos-skeleton/entry/src/main/resources/rawfile/index.html +92 -92
  63. package/cli/templates/ohos-skeleton/hvigor/hvigor-config.json5 +5 -5
  64. package/cli/templates/ohos-skeleton/hvigorfile.ts +6 -6
  65. package/cli/templates/ohos-skeleton/oh-package.json5 +10 -10
  66. package/native/build_win.bat +62 -62
  67. package/native/core/bridge_core.c +1681 -1626
  68. package/native/core/bridge_core.h +675 -618
  69. package/native/core/bridge_js.h +108 -108
  70. package/native/platforms/harmonyos/README.md +115 -6
  71. package/native/platforms/linux/bridge_linux.c +2787 -2485
  72. package/native/platforms/windows/bridge_win.c +3740 -3248
  73. package/native/tests/gtk_threading_probe.c +143 -143
  74. package/native/tests/test_bridge_core.c +1339 -1228
  75. package/package.json +53 -53
  76. package/prebuilt/linux-x64/cj-tauri +0 -0
  77. package/src/api_system.cj +67 -67
  78. package/src/app.cj +325 -969
  79. package/src/app_events.cj +166 -0
  80. package/src/app_host_ops.cj +287 -0
  81. package/src/app_run.cj +283 -0
  82. package/src/app_spec.cj +76 -76
  83. package/src/arkweb_menu.cj +216 -0
  84. package/src/arkweb_script.cj +142 -142
  85. package/src/capability.cj +409 -409
  86. package/src/clipboard_bridge.cj +178 -0
  87. package/src/dialog_spec.cj +79 -0
  88. package/src/dragdrop.cj +26 -26
  89. package/src/embedded_assets.cj +98 -98
  90. package/src/host.cj +752 -607
  91. package/src/host_arkweb.cj +924 -778
  92. package/src/host_webkit.cj +663 -570
  93. package/src/host_webview2.cj +663 -570
  94. package/src/hotkey.cj +66 -66
  95. package/src/ipc_hub.cj +337 -337
  96. package/src/ipc_message.cj +286 -286
  97. package/src/launch.cj +62 -62
  98. package/src/logging.cj +127 -127
  99. package/src/menu.cj +197 -197
  100. package/src/platform/platform_app_context.cj +77 -77
  101. package/src/platform/platform_assets.cj +33 -33
  102. package/src/platform/platform_discover.cj +176 -176
  103. package/src/platform/platform_env.cj +29 -29
  104. package/src/platform/platform_feature.cj +31 -31
  105. package/src/platform/platform_page_source.cj +87 -87
  106. package/src/platform/platform_target_info.cj +32 -32
  107. package/src/platform_host_provider.cj +87 -87
  108. package/src/plugin.cj +333 -333
  109. package/src/plugins/clipboard/plugin_clipboard.cj +152 -152
  110. package/src/plugins/dialog/plugin_dialog.cj +209 -209
  111. package/src/plugins/fs/plugin_fs.cj +105 -1
  112. package/src/plugins/http/http_backend.cj +131 -0
  113. package/src/plugins/http/http_desktop.cj +195 -0
  114. package/src/plugins/http/http_ohos.cj +204 -0
  115. package/src/plugins/http/plugin_http.cj +310 -459
  116. package/src/plugins/log/plugin_log.cj +105 -105
  117. package/src/plugins/menu/plugin_menu.cj +158 -158
  118. package/src/plugins/notification/plugin_notification.cj +154 -154
  119. package/src/plugins/serial/plugin_serial.cj +458 -454
  120. package/src/plugins/serial/serial_bridge.cj +194 -194
  121. package/src/plugins/serial/serial_manager_ohos.cj +92 -0
  122. package/src/plugins/serial/serial_ohos.cj +529 -0
  123. package/src/plugins/shell/plugin_shell.cj +258 -258
  124. package/src/plugins/sqlite/plugin_sqlite.cj +705 -710
  125. package/src/plugins/sqlite/sqlite_ffi.cj +206 -211
  126. package/src/plugins/ws/plugin_ws.cj +488 -470
  127. package/src/process_hidden.cj +174 -174
  128. package/src/scope.cj +391 -391
  129. package/src/shell_event.cj +69 -69
  130. package/src/tests/app_spec_test.cj +126 -127
  131. package/src/tests/capability_test.cj +243 -243
  132. package/src/tests/dialog_spec_test.cj +73 -0
  133. package/src/tests/fake_host.cj +201 -201
  134. package/src/tests/ipc_hub_test.cj +487 -487
  135. package/src/tests/logging_test.cj +62 -62
  136. package/src/tests/platform_contract_test.cj +251 -251
  137. package/src/tests/plugin_clipboard_test.cj +222 -222
  138. package/src/tests/plugin_dialog_test.cj +237 -237
  139. package/src/tests/plugin_fs_test.cj +207 -0
  140. package/src/tests/plugin_http_test.cj +124 -124
  141. package/src/tests/plugin_notification_test.cj +205 -205
  142. package/src/tests/plugin_serial_test.cj +48 -0
  143. package/src/tests/plugin_test.cj +605 -585
  144. package/src/tests/plugin_ws_test.cj +40 -40
  145. package/src/tests/process_hidden_test.cj +129 -129
  146. package/src/tests/scope_test.cj +350 -350
  147. package/src/tests/system_host_test.cj +99 -81
  148. package/src/tests/urlutil_test.cj +53 -53
  149. package/src/tests/version_test.cj +75 -75
  150. package/src/tests/window_registry_test.cj +123 -123
  151. package/src/tray.cj +90 -90
  152. package/src/urlutil.cj +159 -159
  153. package/src/version.cj +44 -44
  154. package/src/window.cj +51 -51
  155. package/src/window_registry.cj +116 -116
  156. package/prebuilt/win32-x64/cj-tauri.exe +0 -0
  157. package/src/plugins/serial/serial_ohos_stub.cj +0 -61
  158. package/src/plugins/sqlite/sqlite_ohos_stub.cj +0 -91
package/src/host.cj CHANGED
@@ -1,607 +1,752 @@
1
- package cjTauri
2
-
3
- import std.collection.*
4
- import stdx.encoding.json.*
5
- import cjTauri.platform.*
6
-
7
- /**
8
- * WebView 宿主层(对标 Tauri 2 的 tao + wry 抽象)
9
- *
10
- * 平台差异由条件编译隔离在各自的宿主文件内,上层完全共用:
11
- * - Linux 桌面:host_webkit.cj(WebKitGTK,经 native/bridge_linux.c 在原生 pthread 运行)
12
- * - Windows :host_webview2.cj(WebView2 / Edge Chromium,经 native/bridge_win.c)
13
- * - 鸿蒙 :ArkWeb(架构预留,条件编译位)
14
- * IPC 桥(ipc_hub.cj)与能力安全模型(capability.cj)跨平台复用。
15
- */
16
-
17
- /**
18
- * 宿主能力位(与 native/bridge_core.h 的 `CJ_CAP_*` **逐位对齐**,两侧不许各写各的)
19
- *
20
- * 由平台在 C 侧 `cj_plat_init` 时填好,经 `WebViewHost.hostCapabilities()` 取回。
21
- * 「这台机器上的框架支持什么」是**静态事实**(编出来的桥有没有那块),不是运行期探测:
22
- * 应用据此决定要不要建菜单 / 托盘,拿不到就别骗自己。
23
- *
24
- * 与能力安全模型(capability.cj)的 `Capability` 不是一回事:那个管「页面能不能调某个命令」,
25
- * 这个管「宿主有没有这个本事」——所以名字里带 host 前缀,别混。
26
- */
27
- public const CJ_CAP_MENU: Int32 = 1
28
-
29
- public const CJ_CAP_TRAY: Int32 = 2
30
-
31
- public const CJ_CAP_DRAG_DROP: Int32 = 4
32
-
33
- /** 全局快捷键(与 native/bridge_core.h 的 CJ_CAP_HOTKEY 逐位对齐;Linux = 编进了 X11 支持) */
34
- public const CJ_CAP_HOTKEY: Int32 = 16
35
-
36
- /** 标题栏副标题(与 native/bridge_core.h 的 CJ_CAP_SUBTITLE 逐位对齐;Linux = 菜单栏同排的 label,
37
- Windows 的系统菜单没有自绘区、不置位)。应用据它决定要不要自己画页面顶栏。 */
38
- public const CJ_CAP_SUBTITLE: Int32 = 32
39
-
40
- /**
41
- * 宿主能力位的**可读形态**(RFC-002 §5.3):`CJ_CAP_*` 位域 → 具名布尔 + `toJson`。
42
- *
43
- * 为什么要有它:`hostCapabilities()` 给的是位域,应用与 `system:host` 的调用方要的是
44
- * 「menu 有没有」这种具名答案;位域 ↔ 名字的对应只留在这一个地方,
45
- * 免得每个消费点各写各的 `(caps & CJ_CAP_MENU) != 0`(写错一位就是一个静默的假答案)。
46
- *
47
- * `platform` 不在位域里,由调用方(`currentPlatform()`)单独给——位域来自 C 桥,平台标识来自
48
- * 仓颉侧的条件编译,两者本就是两个来源。
49
- */
50
- public class HostCapabilities {
51
- public let platform: String
52
- public let menu: Bool
53
- public let tray: Bool
54
- public let dragDrop: Bool
55
- public let hotkey: Bool
56
- public let subtitle: Bool
57
-
58
- public init(platform: String, menu: Bool, tray: Bool, dragDrop: Bool, hotkey: Bool, subtitle: Bool) {
59
- this.platform = platform
60
- this.menu = menu
61
- this.tray = tray
62
- this.dragDrop = dragDrop
63
- this.hotkey = hotkey
64
- this.subtitle = subtitle
65
- }
66
-
67
- /** 位域 → 具名布尔(`platform` 由调用方给,见类注释) */
68
- public static func fromBits(platform: String, caps: Int32): HostCapabilities {
69
- return HostCapabilities(
70
- platform,
71
- (caps & CJ_CAP_MENU) != 0,
72
- (caps & CJ_CAP_TRAY) != 0,
73
- (caps & CJ_CAP_DRAG_DROP) != 0,
74
- (caps & CJ_CAP_HOTKEY) != 0,
75
- (caps & CJ_CAP_SUBTITLE) != 0
76
- )
77
- }
78
-
79
- /** `system:host` 的返回形状:`{"platform":"linux","menu":true,"tray":true,"dragDrop":true,"hotkey":true,"subtitle":true}` */
80
- public func toJson(): JsonValue {
81
- var obj = JsonObject()
82
- obj.put("platform", JsonString(this.platform))
83
- obj.put("menu", JsonBool(this.menu))
84
- obj.put("tray", JsonBool(this.tray))
85
- obj.put("dragDrop", JsonBool(this.dragDrop))
86
- obj.put("hotkey", JsonBool(this.hotkey))
87
- obj.put("subtitle", JsonBool(this.subtitle))
88
- return obj
89
- }
90
- }
91
-
92
- public interface WebViewHost {
93
- /**
94
- * 本窗口的 label(单窗口阶段恒为 "main")
95
- *
96
- * 与 `WindowRecord.label` 是同一个值:注册表是路由的权威来源,宿主自己也留一份——
97
- * 给前端注入 label(`window.__CJ_TAURI_LABEL__`)以及将来的窗口内自省都用它。
98
- */
99
- func label(): String
100
-
101
- /**
102
- * 加载页面(唯一入口,RFC §4.3):Inline / Url / DevServer / Asset 四态
103
- *
104
- * 原 start(html) / startUrl(url) 双入口在此收敛。Asset 怎么落地是 L0 的事(RFC §6.2):
105
- * 桌面经注入的 AssetResolver 读盘(未注入时按 cwd 相对路径读,与批次 1 行为等价),
106
- * 鸿蒙由胶水印化的 rawfile 解析成 HTML 再落沙箱以 file:// 加载。
107
- * URL 页面有真实来源,相对路径资源、动态 import、前端路由才成立(file:// 下禁止 fetch/XHR)。
108
- */
109
- func load(source: PageSource): Unit
110
-
111
- /**
112
- * 注入资产解析器(装配层在 load 之前调用;None = 各端默认读盘策略)
113
- */
114
- func setAssetResolver(resolver: Option<AssetResolver>): Unit
115
-
116
- /**
117
- * 注册一段「页面脚本执行前」注入的初始化脚本(多次调用按注册顺序注入)
118
- *
119
- * 必须在 load() 之前调用:宿主创建 WebView 时一次性交给 C 桥,桥以
120
- * document-start 注入(WebKit user script / WebView2 AddScriptToExecuteOnDocumentCreated)。
121
- * 因此它对 HTML 与 URL 两种页面来源**一视同仁**——插件的 jsShim 走的就是这条通道(RFC §5.4 的 v2 方案),
122
- * 不再靠字符串拼 HTML。空串忽略;脚本是普通 JS 片段,不要自带 <script> 标签。
123
- */
124
- func addInitScript(js: String): Unit
125
-
126
- /**
127
- * 重新加载当前页面(整页 reload,对标浏览器刷新)
128
- *
129
- * 页面尚未加载时为空操作;开发者工具里的刷新按钮等价于此。
130
- */
131
- func reload(): Unit
132
-
133
- /**
134
- * 应用窗口配置(标题 / 尺寸);必须在 load() 之前调用,宿主据此创建窗口
135
- */
136
- func setWindowConfig(config: WindowConfig): Unit
137
-
138
- /**
139
- * 是否允许打开开发者工具;必须在 load() 之前调用
140
- */
141
- func setDevTools(enabled: Bool): Unit
142
-
143
- /**
144
- * 运行时打开开发者工具(WebView2 DevTools / WebKit Inspector)
145
- */
146
- func openDevTools(): Unit
147
-
148
- /**
149
- * 设置本窗口的菜单栏(RFC-002 §5.2 的线路文本;空串 = 清空菜单栏)
150
- *
151
- * 调用时机两头都行:`start()` 之前调用会在建窗时一次建好(未就绪的文本由 C 桥记着);
152
- * 之后就投到宿主 UI 线程重建整棵菜单。
153
- * 平台没有菜单能力(`hostCapabilities()` 不含 `CJ_CAP_MENU`)时不影响其它功能——
154
- * 「能力位为 0 就别建菜单」是调用方的约定,宿主不替应用猜。
155
- */
156
- func setMenu(wire: String): Unit
157
-
158
- /**
159
- * 运行期改单个菜单项的状态(按 id 找;找不到只记一行日志,不报错)
160
- *
161
- * 只改状态、不改结构:结构变化走 `setMenu` 整棵重建。状态的真身在平台对象上
162
- * (Windows 的 HMENU、GTK 的 GtkCheckMenuItem),模型本身是只读的——理由见 src/menu.cj。
163
- */
164
- func setMenuItemState(id: String, enabled: Bool, checked: Bool): Unit
165
-
166
- /**
167
- * 设置**窗口顶部副标题**:一行文本画在原生菜单栏**同一行的右侧**
168
- *
169
- * 用途是让应用省掉自己那条页面顶栏——标题 / 保存状态 / 字数挂在原生这一行上,
170
- * 界面高度直接少一层。允许运行期高频更新(每次编辑都可能改),平台自己投递到
171
- * UI 线程,调用方在哪个线程调都行。
172
- * 这是**运行期能力**:建窗之前调用返回 -3(不像 `setMenu` 那样记 pending),
173
- * 应用要么等页面加载后再调,要么按返回值如实处理。
174
- * 平台没有该能力(能力位不含 `CJ_CAP_SUBTITLE`,Windows 即如此)时如实降级:
175
- * 文本不显示、桥打一行 stderr,应用据能力位自己决定画不画页面顶栏。
176
- */
177
- func setTitlebarSubtitle(text: String): Unit
178
-
179
- /**
180
- * 设置拖放策略(RFC-002 §5):enabled=false 平台不注册 drop target。
181
- * 必须在 `load()` 之前调用(Linux 建 view 时决定挂不挂 drag dest);
182
- * 之后调用在未就绪时只记 pending、建窗生效,已就绪平台投 UI 线程重挂。
183
- * 平台没有该能力(能力位不含 `CJ_CAP_DRAG_DROP`)时宿主只记日志,不报错。
184
- */
185
- func setDragDropPolicy(enabled: Bool, files: Bool): Unit
186
-
187
- /**
188
- * 注册**全局快捷键**(RFC-003 第 5 期):wire 一行一条 `<accel>:<id>`(见 `HotkeyModel.toWire`)
189
- *
190
- * 全局 = 焦点不在本应用窗口时也生效(X11 grab / Win32 RegisterHotKey)。
191
- * 允许在 `start()` 之前调(桥记 pending,宿主线程起来后注册);运行期再调 = 整表替换。
192
- * 触发事件以 `{"kind":"hotkey","id":"…"}` 走 shell 回调(按句柄路由)→ 前端 `hotkey:press`。
193
- * 平台不支持时桥打一行 stderr 如实说明,调用不报错、事件不来。
194
- */
195
- func setGlobalHotkeys(wire: String): Unit
196
-
197
- /**
198
- * 设置**托盘**(RFC-002 §7.C):wire 见 `TrayModel.toWire()`,空串 = 移除托盘
199
- *
200
- * 允许在 `start()` 之前调(桥记 pending,宿主线程装配时创建);运行期再调 = 整体重建。
201
- * 菜单项被点以 `{"kind":"tray","id":"…"}` 走 shell 回调(按句柄路由)→ 前端 `tray:click`。
202
- * 平台不支持 / 依赖没编进来时桥打一行 stderr 如实说明,调用不报错、托盘不出现。
203
- */
204
- func setTray(wire: String): Unit
205
-
206
- /**
207
- * 显示 / 隐藏本窗口(对标 Tauri 2 的 `window.show()` / `window.hide()`)
208
- *
209
- * 隐藏 = 从任务栏与屏幕上消失(Windows 走 `ShowWindow(SW_HIDE)`,Linux 走 `gtk_widget_hide`),
210
- * 事件循环照常跑、页面照常活着——所以「最小化到托盘」的还原路径(托盘菜单 / 全局快捷键)才成立。
211
- * 两头都能调:`start()` 之前调只记下(建窗时按它定稿,隐藏态的窗口首次出现前就是隐藏的);
212
- * 之后调用投到宿主 UI 线程执行。显示时顺带把窗口提到前台(Linux 是 `gtk_window_present`,
213
- * Windows 是 `SetForegroundWindow`)。
214
- * 这是**宿主基元**,两平台都实现,不占能力位(没有平台只有一半)。
215
- */
216
- func setWindowVisible(visible: Bool): Unit
217
-
218
- /**
219
- * 最小化到托盘(RFC-003 第 5 期余项,`WindowConfig.minimizeToTray` 的落点)
220
- *
221
- * enabled=true 时,用户在标题栏点「最小化」不再缩到任务栏,而是**隐藏窗口**
222
- * (= `setWindowVisible(false)` 的效果);还原走应用自己的托盘菜单 / 全局快捷键。
223
- * 必须在 `load()` 之前调用(窗口过程要在建窗时就知道这条策略)。
224
- * 真到最小化那一刻,平台会复核托盘是否**真的**装上了(能力位不含 `CJ_CAP_TRAY`,
225
- * 或应用压根没调 `setTray`):没装就如实降级为普通最小化并打一行 stderr——
226
- * 窗口藏起来却没有任何还原入口,是比「不支持」更糟的结果。
227
- */
228
- func setMinimizeToTray(enabled: Bool): Unit
229
-
230
- /**
231
- * 本宿主的静态能力位(`CJ_CAP_*` 的按位或):能不能做菜单 / 托盘 / 文件拖入
232
- *
233
- * 宿主尚未创建(`dispose()` 之后 / `start()` 之前未分配句柄)返回 0。
234
- * 注意与命令级的能力安全模型无关:这里说的是宿主本事,不是页面权限。
235
- */
236
- func hostCapabilities(): Int32
237
-
238
- /**
239
- * 装配本窗口的 shell 事件回调(菜单点击等;载荷是 C 桥的 JSON 信封)
240
- *
241
- * 与 `setMessageHandler` 同一套路,区别是事件源在宿主侧(应用菜单)而不是页面。
242
- * **多窗口下必须按宿主身份路由**:C 侧回调首参是句柄,只有本窗口的句柄才转给本实例的
243
- * 这个回调——这正是「回调带宿主身份」的落地方式(见 docs/RFC-菜单-托盘-拖放.md §5.5)。
244
- */
245
- func setShellHandler(handler: (String) -> Unit): Unit
246
-
247
- /**
248
- * 装配本窗口的销毁回调(窗口关闭 / 退出收尾时恰好触发一次)
249
- *
250
- * 多窗口下按宿主身份路由(C 侧回调首参是句柄),所以每扇窗口装自己的——回调不带参数,
251
- * 因为「是哪扇窗」在装配时就已确定(应用可经 `app.hostOf(label)` 逐窗安装)。
252
- * 与 `setShellHandler` 同为**可变槽位**:在 `load()` 之后再装同样生效(路由闭包读字段,不是快照)。
253
- */
254
- func setDestroyHandler(handler: () -> Unit): Unit
255
-
256
- /**
257
- * 原生文件选择对话框(阻塞到用户关闭):返回选中的路径,取消 / 失败返回空串。
258
- *
259
- * filter 是「描述|模式|描述|模式」拼串(Windows 惯例;GTK 端按同一格式解析),
260
- * 例如 "文本文件|*.txt|所有文件|*.*";空串表示不限制。
261
- * 必须在 load() 之后调用——对话框要拿已创建的应用窗口当父窗口。
262
- */
263
- func showFileDialog(save: Bool, title: String, filter: String): String
264
-
265
- /**
266
- * 带默认文件名的文件对话框:除预置名字外与 `showFileDialog` 完全同语义。
267
- *
268
- * 预置名字的用途是**自动化取证**——探针对原生框发 IDOK 时框里得已有名字才能关,
269
- * 返回值因此可预期(预置什么回什么),多窗并发的「结果各归各窗」才验得动。
270
- * 平台不支持预置(如 GTK 轮未接)时行为退化为 `showFileDialog`,如实降级。
271
- */
272
- func showFileDialogPreset(save: Bool, title: String, filter: String, defaultName: String): String
273
-
274
- /**
275
- * 原生目录选择对话框(阻塞到用户关闭):返回选中的目录路径,取消 / 失败返回空串。
276
- *
277
- * 独立成方法而不是给 `showFileDialog` 加一个开关:选目录在两端都不是「换个动作」——
278
- * GTK 是另一个 chooser action,Windows 更是另一套 API(`IFileOpenDialog` +
279
- * `FOS_PICKFOLDERS`,`GetOpenFileName` 没有这个能力)。桥侧两者共用同名同签名的
280
- * `cj_bridge_show_dialog`,以 kind=6 区分(与文件类 / 消息类并列,见 `dlg_req`)。
281
- * 与 `showFileDialog` 同规矩:必须在 load() 之后调用(要拿应用窗口当父窗口)。
282
- */
283
- func showFolderDialog(title: String): String
284
-
285
- /**
286
- * 原生消息对话框(阻塞到用户关闭):返回用户是否点了「确定」。
287
- *
288
- * kind 取 "info" / "warning" / "error" / "confirm"(其它值按 info 处理)。
289
- * info / warning / error 只有一个「确定」,弹出即成立;只有 confirm 会返回 false。
290
- */
291
- func showMessageDialog(kind: String, title: String, message: String): Bool
292
-
293
- /**
294
- * 写文本进系统剪贴板(上限 8 MiB,C 侧 core 校验)
295
- *
296
- * 与对话框同类:宿主能力,要在 load() 之后调用(未就绪时返回 false)。
297
- */
298
- func clipboardWriteText(text: String): Bool
299
-
300
- /**
301
- * 读系统剪贴板文本:空 / 非文本 / 平台失败都返回 None,不区分(协议层没有可读的失败明细)
302
- */
303
- func clipboardReadText(): Option<String>
304
-
305
- /**
306
- * 发一条系统通知(发后即忘:提交即返回,显示成败不回传)。
307
- * 本平台未实现时返回 false(`system:host` 的能力位会同口径如实上报)。
308
- */
309
- func notify(title: String, body: String): Bool
310
-
311
- /**
312
- * 原生 → JS:在 WebView 中执行脚本
313
- */
314
- func runJs(js: String): Unit
315
-
316
- /**
317
- * 停止本窗口的事件循环(低层原语)
318
- *
319
- * 应用级退出请用 `AppHost.quit()`——它会走所有窗口;单窗口下两者等价。
320
- * 原语留在宿主上是因为「循环归谁所有」只有实现方知道(两平台的收尾方式不同:
321
- * Linux 只收主循环、窗口留给收尾销毁,Windows 直接 DestroyWindow),上层不该自己拼。
322
- */
323
- func quit(): Unit
324
-
325
- /**
326
- * 装配前端消息回调(WebView 送来的 JSON 字符串 → IPC hub)
327
- */
328
- func setMessageHandler(handler: (String) -> Unit): Unit
329
-
330
- /**
331
- * 宿主事件循环是否已退出(窗口关闭等),app.run 据此阻塞与收尾
332
- *
333
- * 同为低层原语:应用级等待用 `AppHost.waitForExit()`,调用方不必自己写轮询。
334
- */
335
- func shouldQuit(): Bool
336
-
337
- /**
338
- * 释放宿主资源(宿主编由 C 侧分配与回收,跨 FFI 不换分配器):幂等、无需检查调用时机
339
- *
340
- * TauriApp.run / runUrl 在 waitForExit() 之后调用它;实现方须把句柄置空——
341
- * 释放后该实例不可再启动(原生窗口已随销毁路径在关窗时收尾)。
342
- * 为什么「关窗」不等于「释放」:见 docs/架构演进-多平台与多窗口.md §6.1。
343
- */
344
- func dispose(): Unit
345
- }
346
-
347
- /**
348
- * 应用级宿主(对标 Tauri 2 的 `AppHandle`)
349
- *
350
- * 与 `WebViewHost` 的分工:`WebViewHost` 是**一个窗口**(创建、投脚本、对话框、本窗口的循环原语),
351
- * `AppHost` 是**整个应用**(退出、按 label 取窗口;将来还有托盘与应用菜单——批次 3 只定模型)。
352
- * 批次 2 由 `TauriApp` 实现:它持有窗口注册表,也只有它知道「哪些窗口还开着、应用该不该退」。
353
- *
354
- * 为什么 quit / waitForExit 收在这一层:它们跨窗口。宿主仍保留 `quit()` / `shouldQuit()` 作低层原语
355
- * (循环归宿主所有),但**应用的公开入口只有这里**——多窗口时代的「退出」必然是「先把各窗口收掉」。
356
- */
357
- public interface AppHost {
358
- /**
359
- * 退出应用:收掉所有窗口的事件循环,`run()` / `runUrl()` 随即返回
360
- */
361
- func quit(): Unit
362
-
363
- /**
364
- * 阻塞到应用退出(窗口关闭 / quit)后返回;`run()` / `runUrl()` 内部调它
365
- */
366
- func waitForExit(): Unit
367
-
368
- /**
369
- * 按 label 取窗口宿主(对标 Tauri 的 `app.get_webview_window(label)`);未注册返回 None
370
- */
371
- func hostOf(label: String): Option<WebViewHost>
372
- }
373
-
374
- /**
375
- * C 桥原生线程 → 仓颉回调的中转。
376
- * 各平台宿主在 prepareHost() 时把「本句柄 → 本实例的回调」登记进路由表,C 侧的 @C 函数按句柄取用。
377
- */
378
- /**
379
- * 一条**宿主级回调路由**:C 侧句柄 → 本窗口的三个回调
380
- *
381
- * C 侧所有回调的入口都是模块级 @C 函数(每个宿主注册的是同一个函数指针),句柄是唯一能分辨
382
- * 「哪个窗口」的凭据——三类回调都按句柄查表,查到谁就派发给谁。
383
- *
384
- * 闭包都是可变的:读的是宿主实例的字段(不是快照),所以应用在 `load()` 之后再调
385
- * `setShellHandler` / `setDestroyHandler` 同样生效。
386
- */
387
- class HostRoute {
388
- let handle: CPointer<Unit>
389
- var onShell: (String) -> Unit
390
- var onDestroy: () -> Unit
391
- var onDialog: (String) -> Unit
392
-
393
- init(handle: CPointer<Unit>, onShell: (String) -> Unit, onDestroy: () -> Unit,
394
- onDialog: (String) -> Unit) {
395
- this.handle = handle
396
- this.onShell = onShell
397
- this.onDestroy = onDestroy
398
- this.onDialog = onDialog
399
- }
400
- }
401
-
402
- class HostGlobals {
403
- // 前端消息:**有意保留单槽**。报文自带发起窗口的 window label(架构文档 §7.3),
404
- // TauriApp 给所有窗口装的是同一只派发器,路由不靠句柄(见 src/app.cj 的 wireHostCallbacks)。
405
- static var onMessage: (String) -> Unit = { _ => }
406
- // 其余三类回调没有「自带身份」的载荷(销毁无载荷、文件框结果只有路径、shell 事件只有信封),
407
- // 只能让 C 侧把句柄带回来。表里每条只认领自己的句柄。
408
- // 单槽时代的症状:后装配的窗口覆盖先装配的——「哪扇窗关了」认错、两窗同时开文件框时结果串台
409
- // (见 docs/架构演进-多平台与多窗口.md §7.5)。
410
- // 登记发生在 prepareHost(run() 之前、宿主线程还没起),派发时才读表 → 登记期没有并发。
411
- static var hostRoutes: ArrayList<HostRoute> = ArrayList<HostRoute>()
412
-
413
- /**
414
- * 登记(或更新)一条宿主级路由
415
- *
416
- * 同一句柄重复登记=换掉旧闭包(宿主被重新 prepare 时不叠加、事件也不会派发两次);
417
- * 句柄比较走 C 侧 `cj_bridge_host_eq`(仓颉的 `CPointer` 不支持 `==`)。
418
- * 顺带解决「旧宿主销毁、新宿主复用同一地址」:新宿主登记时就覆盖了那条陈旧路由。
419
- */
420
- static func registerHostRoute(handle: CPointer<Unit>, onShell: (String) -> Unit,
421
- onDestroy: () -> Unit, onDialog: (String) -> Unit): Unit {
422
- for (r in HostGlobals.hostRoutes) {
423
- if (hostHandleEquals(r.handle, handle)) {
424
- r.onShell = onShell
425
- r.onDestroy = onDestroy
426
- r.onDialog = onDialog
427
- return
428
- }
429
- }
430
- HostGlobals.hostRoutes.add(HostRoute(handle, onShell, onDestroy, onDialog))
431
- }
432
-
433
- /**
434
- * 按句柄取路由;查不到返回 None(句柄不属于本进程,或该宿主已注销)
435
- */
436
- static func hostRouteOf(handle: CPointer<Unit>): Option<HostRoute> {
437
- for (r in HostGlobals.hostRoutes) {
438
- if (hostHandleEquals(r.handle, handle)) {
439
- return Some(r)
440
- }
441
- }
442
- return None
443
- }
444
- }
445
-
446
- /**
447
- * kind 字符串 → C 桥的对话框编号(与 native/bridge_linux.c、bridge_win.c 的取值一一对应)
448
- */
449
- func dialogKindCode(kind: String): Int32 {
450
- match (kind) {
451
- case "warning" => 3
452
- case "error" => 4
453
- case "confirm" => 5
454
- case _ => 2
455
- }
456
- }
457
-
458
- /**
459
- * 对话框结果回调(C 桥 → 仓颉):文件类对话框选中的路径经它送回,首参是**发起这次对话框的宿主句柄**。
460
- *
461
- * 由 C 桥在自己的 UI 线程上调用,与 hostOnMessage 同一套路;按句柄写进对应宿主的结果位,
462
- * 再由桥唤醒阻塞的调用方去读。多窗口下两扇窗可以各自开着文件框,结果不许串台。
463
- * 非 @When:两平台的 C 侧都调它(签名一致,首参不透明)。
464
- */
465
- @C
466
- func dialogOnResult(host: CPointer<Unit>, path: CString): Unit {
467
- try {
468
- match (HostGlobals.hostRouteOf(host)) {
469
- case Some(r) => r.onDialog(path.toString())
470
- case None => eprintln("[cj-tauri] dialog callback: 未登记的宿主句柄(已丢弃)")
471
- }
472
- } catch (e: Exception) {
473
- eprintln("[cj-tauri] dialog callback error: ${e}")
474
- }
475
- }
476
-
477
- /**
478
- * 窗口销毁回调(C 桥 → 仓颉):首参是**被销毁的宿主句柄**(每个宿主恰好触发一次)。
479
- *
480
- * 曾经写进全局单槽 → 多窗口下「哪扇窗关了」认错(后装配的窗口覆盖先装配的);
481
- * 现在与 shell / 对话框一样按句柄认领。非 @When:两平台的 C 侧都调它。
482
- */
483
- @C
484
- func hostOnDestroy(host: CPointer<Unit>): Unit {
485
- try {
486
- match (HostGlobals.hostRouteOf(host)) {
487
- case Some(r) => r.onDestroy()
488
- case None => eprintln("[cj-tauri] destroy callback: 未登记的宿主句柄(已忽略)")
489
- }
490
- } catch (e: Exception) {
491
- eprintln("[cj-tauri] destroy callback error: ${e}")
492
- }
493
- }
494
-
495
- /**
496
- * shell 事件回调(C 桥 → 仓颉):菜单点击等经它送回,首参是**发生事件的宿主句柄**。
497
- *
498
- * 由 C 桥在宿主 UI 线程上调用(与 hostOnMessage 同一套路);句柄→实例的解引用走宿主级路由表,
499
- * 本函数只做「别抛穿 FFI 边界」这一件事。
500
- * 非 @When:两平台的 C 侧都调它(签名一致,首参不透明)。
501
- */
502
- @C
503
- func shellOnEvent(host: CPointer<Unit>, json: CString): Unit {
504
- try {
505
- // 认领自己那条:句柄对不上就静默跳过(C 侧只有一个入口,区分就在这里)
506
- match (HostGlobals.hostRouteOf(host)) {
507
- case Some(r) => r.onShell(json.toString())
508
- case None => () // 未登记(旧宿主已注销 / 非本进程句柄):静默,别在热路径上刷屏
509
- }
510
- } catch (e: Exception) {
511
- eprintln("[cj-tauri] shell callback error: ${e}")
512
- }
513
- }
514
-
515
- /**
516
- * 创建当前平台的 WebView 宿主(对标 Tauri 2 的 runtime 选择)
517
- *
518
- * label 按窗口给定(多窗口):默认窗口传 DEFAULT_WINDOW_LABEL,附加窗口传各自登记的 label。
519
- * 必须是构造参数而不是事后 setter——label 决定页面侧 `window.__CJ_TAURI_LABEL__` 与事件过滤,
520
- * 启动之后再改就晚了(见 docs/架构演进-多平台与多窗口.md §7.3)。
521
- */
522
- // 判据为什么是 `env`(2026-10-19 校正):鸿蒙目标下 `os` 仍取 `Linux`,`arch` 也只有
523
- // `aarch64`——桌面 aarch64 Linux 同样是 `aarch64`,拿它当判据会把「跑在 GTK 上」误判成
524
- // 「跑在 ArkWeb 上」,两边的宿主实现会互相打架(编译期就撞名)。官方条件编译映射表给出
525
- // `aarch64-linux-ohos` → `env == "ohos"`、桌面 Linux → `gnu`,这才是真正互斥的维度。
526
- // 于是桌面 Linux 侧写 `env != "ohos"`:**桌面 aarch64 Linux 因此重新拿回 WebKit 宿主**
527
- // (改判据之前它落到「只能注入宿主」的分支,是同一处误判的另一半)。
528
- @When[os == "Linux" && env != "ohos"]
529
- func createHost(label: String): WebViewHost {
530
- return WebKitHost(label)
531
- }
532
-
533
- // 鸿蒙:内置 ArkWeb 宿主(应用侧只负责建 ArkUI 组件并把 controller 交给它,见 src/host_arkweb.cj)
534
- @When[env == "ohos"]
535
- func createHost(label: String): WebViewHost {
536
- return ArkWebHost(label)
537
- }
538
-
539
- @When[os == "Windows"]
540
- func createHost(label: String): WebViewHost {
541
- return WebView2Host(label)
542
- }
543
-
544
- /**
545
- * 两个宿主句柄是否指向同一宿主
546
- *
547
- * 桌面两平台走 C 桥 `cj_bridge_host_eq`——仓颉的 `CPointer` 既没有 `==` 也没有 `!=`
548
- * (实测报 `invalid binary operator '!=' on type 'CPointer<Unit>'`),也没有 `hashCode`,
549
- * 比较只能交给 C 侧。鸿蒙侧没有 C 桥(`native/platforms/harmonyos/` 尚未产出 `cj_bridge_*`),
550
- * `ArkWebHost` 的句柄在仓颉侧不参与路由(回调是仓颉闭包,不经 C 中转),因此恒为 false
551
- * 并留一行 stderr——**不假装相等**(假装相等会把两扇窗的路由串台)。
552
- */
553
- @When[os == "Windows"]
554
- func hostHandleEquals(a: CPointer<Unit>, b: CPointer<Unit>): Bool {
555
- unsafe {
556
- return cj_bridge_host_eq(a, b) != 0
557
- }
558
- }
559
-
560
- @When[os == "Linux" && env != "ohos"]
561
- func hostHandleEquals(a: CPointer<Unit>, b: CPointer<Unit>): Bool {
562
- unsafe {
563
- return cj_bridge_host_eq(a, b) != 0
564
- }
565
- }
566
-
567
- // 只在无 C 桥的平台用到,声明也放进同一门禁:桌面侧保留会变成 unused variable
568
- @When[env == "ohos"]
569
- private var HOST_ROUTE_WARNED: Bool = false
570
-
571
- @When[env == "ohos"]
572
- func hostHandleEquals(a: CPointer<Unit>, b: CPointer<Unit>): Bool {
573
- let _ = a
574
- let _ = b
575
- if (!HOST_ROUTE_WARNED) {
576
- HOST_ROUTE_WARNED = true
577
- eprintln("[cj-tauri] host route by handle unavailable on this platform: injected host keeps its own callbacks")
578
- }
579
- return false
580
- }
581
-
582
- /**
583
- * 造一个「裸宿主句柄」:只分配 C 侧宿主对象(不 init / 不 start、不碰图形栈)
584
- *
585
- * 只给单元测试用(子包可见,不算对外 API)。为什么需要这层包皮:`cj_bridge_create` 是
586
- * `foreign func`——没有函数体、也就没有跨包符号,子包 `cjTauri.tests` 直接调会在**链接期**炸
587
- * (实测 `ld.lld: error: undefined symbol: cjTauri:cj_bridge_create`);框架包内包一层就有实体符号了。
588
- *
589
- * 只编桌面两平台:鸿蒙没有 C 桥也就没有这个符号,而单测只在桌面跑(`cjpm test` 用桌面目标)。
590
- */
591
- @When[os == "Windows"]
592
- func createBareHostHandle(): CPointer<Unit> {
593
- var h: CPointer<Unit> = CPointer<Unit>()
594
- unsafe {
595
- h = cj_bridge_create()
596
- }
597
- return h
598
- }
599
-
600
- @When[os == "Linux" && env != "ohos"]
601
- func createBareHostHandle(): CPointer<Unit> {
602
- var h: CPointer<Unit> = CPointer<Unit>()
603
- unsafe {
604
- h = cj_bridge_create()
605
- }
606
- return h
607
- }
1
+ package cjTauri
2
+
3
+ import std.collection.*
4
+ import stdx.encoding.json.*
5
+ import cjTauri.platform.*
6
+
7
+ /**
8
+ * WebView 宿主层(对标 Tauri 2 的 tao + wry 抽象)
9
+ *
10
+ * 平台差异由条件编译隔离在各自的宿主文件内,上层完全共用:
11
+ * - Linux 桌面:host_webkit.cj(WebKitGTK,经 native/bridge_linux.c 在原生 pthread 运行)
12
+ * - Windows :host_webview2.cj(WebView2 / Edge Chromium,经 native/bridge_win.c)
13
+ * - 鸿蒙 :ArkWeb(架构预留,条件编译位)
14
+ * IPC 桥(ipc_hub.cj)与能力安全模型(capability.cj)跨平台复用。
15
+ */
16
+
17
+ /**
18
+ * 宿主能力位(与 native/bridge_core.h 的 `CJ_CAP_*` **逐位对齐**,两侧不许各写各的)
19
+ *
20
+ * 由平台在 C 侧 `cj_plat_init` 时填好,经 `WebViewHost.hostCapabilities()` 取回。
21
+ * 「这台机器上的框架支持什么」是**静态事实**(编出来的桥有没有那块),不是运行期探测:
22
+ * 应用据此决定要不要建菜单 / 托盘,拿不到就别骗自己。
23
+ *
24
+ * 与能力安全模型(capability.cj)的 `Capability` 不是一回事:那个管「页面能不能调某个命令」,
25
+ * 这个管「宿主有没有这个本事」——所以名字里带 host 前缀,别混。
26
+ */
27
+ public const CJ_CAP_MENU: Int32 = 1
28
+
29
+ public const CJ_CAP_TRAY: Int32 = 2
30
+
31
+ public const CJ_CAP_DRAG_DROP: Int32 = 4
32
+
33
+ /** 平台通知(与 native/bridge_core.h 的 CJ_CAP_NOTIFICATION 逐位对齐;Linux = freedesktop
34
+ Notifications,Windows 侧同样置位)。这一位曾漏在仓颉侧:C 侧置了、`system:host` 却看不到,
35
+ 两侧「逐位对齐」的契约当场破了——`scripts/check-static.sh` 现在按名字比对两边的常量表,
36
+ 加位时漏了哪一侧都会在门禁上红。 */
37
+ public const CJ_CAP_NOTIFICATION: Int32 = 8
38
+
39
+ /** 全局快捷键(与 native/bridge_core.h 的 CJ_CAP_HOTKEY 逐位对齐;Linux = 编进了 X11 支持) */
40
+ public const CJ_CAP_HOTKEY: Int32 = 16
41
+
42
+ /** 标题栏副标题(与 native/bridge_core.h 的 CJ_CAP_SUBTITLE 逐位对齐;Linux = 菜单栏同排的 label,
43
+ Windows 的系统菜单没有自绘区、不置位)。应用据它决定要不要自己画页面顶栏。 */
44
+ public const CJ_CAP_SUBTITLE: Int32 = 32
45
+
46
+ /**
47
+ * 宿主能力位的**可读形态**(RFC-002 §5.3):`CJ_CAP_*` 位域 → 具名布尔 + `toJson`。
48
+ *
49
+ * 为什么要有它:`hostCapabilities()` 给的是位域,应用与 `system:host` 的调用方要的是
50
+ * 「menu 有没有」这种具名答案;位域 ↔ 名字的对应只留在这一个地方,
51
+ * 免得每个消费点各写各的 `(caps & CJ_CAP_MENU) != 0`(写错一位就是一个静默的假答案)。
52
+ *
53
+ * `platform` 不在位域里,由调用方(`currentPlatform()`)单独给——位域来自 C 桥,平台标识来自
54
+ * 仓颉侧的条件编译,两者本就是两个来源。
55
+ */
56
+ public class HostCapabilities {
57
+ public let platform: String
58
+ public let menu: Bool
59
+ public let tray: Bool
60
+ public let dragDrop: Bool
61
+ public let notification: Bool
62
+ public let hotkey: Bool
63
+ public let subtitle: Bool
64
+
65
+ /** 字段顺序按 `native/bridge_core.h` 的位序(menu / tray / dragDrop / notification / hotkey / subtitle),
66
+ 不是为了好看:位置参数一旦与位序错开,读的人就得来回对表,漏位也就更难发现。 */
67
+ public init(platform: String, menu: Bool, tray: Bool, dragDrop: Bool, notification: Bool, hotkey: Bool, subtitle: Bool) {
68
+ this.platform = platform
69
+ this.menu = menu
70
+ this.tray = tray
71
+ this.dragDrop = dragDrop
72
+ this.notification = notification
73
+ this.hotkey = hotkey
74
+ this.subtitle = subtitle
75
+ }
76
+
77
+ /** 位域 → 具名布尔(`platform` 由调用方给,见类注释) */
78
+ public static func fromBits(platform: String, caps: Int32): HostCapabilities {
79
+ return HostCapabilities(
80
+ platform,
81
+ (caps & CJ_CAP_MENU) != 0,
82
+ (caps & CJ_CAP_TRAY) != 0,
83
+ (caps & CJ_CAP_DRAG_DROP) != 0,
84
+ (caps & CJ_CAP_NOTIFICATION) != 0,
85
+ (caps & CJ_CAP_HOTKEY) != 0,
86
+ (caps & CJ_CAP_SUBTITLE) != 0
87
+ )
88
+ }
89
+
90
+ /** `system:host` 的返回形状:
91
+ `{"platform":"linux","menu":true,"tray":true,"dragDrop":true,"notification":true,"hotkey":true,"subtitle":true}` */
92
+ public func toJson(): JsonValue {
93
+ var obj = JsonObject()
94
+ obj.put("platform", JsonString(this.platform))
95
+ obj.put("menu", JsonBool(this.menu))
96
+ obj.put("tray", JsonBool(this.tray))
97
+ obj.put("dragDrop", JsonBool(this.dragDrop))
98
+ obj.put("notification", JsonBool(this.notification))
99
+ obj.put("hotkey", JsonBool(this.hotkey))
100
+ obj.put("subtitle", JsonBool(this.subtitle))
101
+ return obj
102
+ }
103
+ }
104
+
105
+ public interface WebViewHost {
106
+ /**
107
+ * 本窗口的 label(单窗口阶段恒为 "main")
108
+ *
109
+ * 与 `WindowRecord.label` 是同一个值:注册表是路由的权威来源,宿主自己也留一份——
110
+ * 给前端注入 label(`window.__CJ_TAURI_LABEL__`)以及将来的窗口内自省都用它。
111
+ */
112
+ func label(): String
113
+
114
+ /**
115
+ * 加载页面(唯一入口,RFC §4.3):Inline / Url / DevServer / Asset 四态
116
+ *
117
+ * 原 start(html) / startUrl(url) 双入口在此收敛。Asset 怎么落地是 L0 的事(RFC §6.2):
118
+ * 桌面经注入的 AssetResolver 读盘(未注入时按 cwd 相对路径读,与批次 1 行为等价),
119
+ * 鸿蒙由胶水印化的 rawfile 解析成 HTML 再落沙箱以 file:// 加载。
120
+ * URL 页面有真实来源,相对路径资源、动态 import、前端路由才成立(file:// 下禁止 fetch/XHR)。
121
+ */
122
+ func load(source: PageSource): Unit
123
+
124
+ /**
125
+ * 注入资产解析器(装配层在 load 之前调用;None = 各端默认读盘策略)
126
+ */
127
+ func setAssetResolver(resolver: Option<AssetResolver>): Unit
128
+
129
+ /**
130
+ * 注册一段「页面脚本执行前」注入的初始化脚本(多次调用按注册顺序注入)
131
+ *
132
+ * 必须在 load() 之前调用:宿主创建 WebView 时一次性交给 C 桥,桥以
133
+ * document-start 注入(WebKit user script / WebView2 AddScriptToExecuteOnDocumentCreated)。
134
+ * 因此它对 HTML 与 URL 两种页面来源**一视同仁**——插件的 jsShim 走的就是这条通道(RFC §5.4 的 v2 方案),
135
+ * 不再靠字符串拼 HTML。空串忽略;脚本是普通 JS 片段,不要自带 <script> 标签。
136
+ */
137
+ func addInitScript(js: String): Unit
138
+
139
+ /**
140
+ * 重新加载当前页面(整页 reload,对标浏览器刷新)
141
+ *
142
+ * 页面尚未加载时为空操作;开发者工具里的刷新按钮等价于此。
143
+ */
144
+ func reload(): Unit
145
+
146
+ /**
147
+ * 应用窗口配置(标题 / 尺寸);必须在 load() 之前调用,宿主据此创建窗口
148
+ */
149
+ func setWindowConfig(config: WindowConfig): Unit
150
+
151
+ /**
152
+ * 是否允许打开开发者工具;必须在 load() 之前调用
153
+ */
154
+ func setDevTools(enabled: Bool): Unit
155
+
156
+ /**
157
+ * 运行时打开开发者工具(WebView2 DevTools / WebKit Inspector)
158
+ */
159
+ func openDevTools(): Unit
160
+
161
+ /**
162
+ * 设置本窗口的菜单栏(RFC-002 §5.2 的线路文本;空串 = 清空菜单栏)
163
+ *
164
+ * 调用时机两头都行:`start()` 之前调用会在建窗时一次建好(未就绪的文本由 C 桥记着);
165
+ * 之后就投到宿主 UI 线程重建整棵菜单。
166
+ * 平台没有菜单能力(`hostCapabilities()` 不含 `CJ_CAP_MENU`)时不影响其它功能——
167
+ * 「能力位为 0 就别建菜单」是调用方的约定,宿主不替应用猜。
168
+ */
169
+ func setMenu(wire: String): Unit
170
+
171
+ /**
172
+ * 运行期改单个菜单项的状态(按 id 找;找不到只记一行日志,不报错)
173
+ *
174
+ * 只改状态、不改结构:结构变化走 `setMenu` 整棵重建。状态的真身在平台对象上
175
+ * (Windows 的 HMENU、GTK 的 GtkCheckMenuItem),模型本身是只读的——理由见 src/menu.cj。
176
+ */
177
+ func setMenuItemState(id: String, enabled: Bool, checked: Bool): Unit
178
+
179
+ /**
180
+ * 设置**窗口顶部副标题**:一行文本画在原生菜单栏**同一行的右侧**
181
+ *
182
+ * 用途是让应用省掉自己那条页面顶栏——标题 / 保存状态 / 字数挂在原生这一行上,
183
+ * 界面高度直接少一层。允许运行期高频更新(每次编辑都可能改),平台自己投递到
184
+ * UI 线程,调用方在哪个线程调都行。
185
+ * 这是**运行期能力**:建窗之前调用返回 -3(不像 `setMenu` 那样记 pending),
186
+ * 应用要么等页面加载后再调,要么按返回值如实处理。
187
+ * 平台没有该能力(能力位不含 `CJ_CAP_SUBTITLE`,Windows 即如此)时如实降级:
188
+ * 文本不显示、桥打一行 stderr,应用据能力位自己决定画不画页面顶栏。
189
+ */
190
+ func setTitlebarSubtitle(text: String): Unit
191
+
192
+ /**
193
+ * 设置拖放策略(RFC-002 §5):enabled=false 平台不注册 drop target。
194
+ * 必须在 `load()` 之前调用(Linux 建 view 时决定挂不挂 drag dest);
195
+ * 之后调用在未就绪时只记 pending、建窗生效,已就绪平台投 UI 线程重挂。
196
+ * 平台没有该能力(能力位不含 `CJ_CAP_DRAG_DROP`)时宿主只记日志,不报错。
197
+ */
198
+ func setDragDropPolicy(enabled: Bool, files: Bool): Unit
199
+
200
+ /**
201
+ * 注册**全局快捷键**(RFC-003 第 5 期):wire 一行一条 `<accel>:<id>`(见 `HotkeyModel.toWire`)
202
+ *
203
+ * 全局 = 焦点不在本应用窗口时也生效(X11 grab / Win32 RegisterHotKey)。
204
+ * 允许在 `start()` 之前调(桥记 pending,宿主线程起来后注册);运行期再调 = 整表替换。
205
+ * 触发事件以 `{"kind":"hotkey","id":"…"}` 走 shell 回调(按句柄路由)→ 前端 `hotkey:press`。
206
+ * 平台不支持时桥打一行 stderr 如实说明,调用不报错、事件不来。
207
+ */
208
+ func setGlobalHotkeys(wire: String): Unit
209
+
210
+ /**
211
+ * 设置**托盘**(RFC-002 §7.C):wire 见 `TrayModel.toWire()`,空串 = 移除托盘
212
+ *
213
+ * 允许在 `start()` 之前调(桥记 pending,宿主线程装配时创建);运行期再调 = 整体重建。
214
+ * 菜单项被点以 `{"kind":"tray","id":"…"}` 走 shell 回调(按句柄路由)→ 前端 `tray:click`。
215
+ * 平台不支持 / 依赖没编进来时桥打一行 stderr 如实说明,调用不报错、托盘不出现。
216
+ */
217
+ func setTray(wire: String): Unit
218
+
219
+ /**
220
+ * 显示 / 隐藏本窗口(对标 Tauri 2 的 `window.show()` / `window.hide()`)
221
+ *
222
+ * 隐藏 = 从任务栏与屏幕上消失(Windows 走 `ShowWindow(SW_HIDE)`,Linux 走 `gtk_widget_hide`),
223
+ * 事件循环照常跑、页面照常活着——所以「最小化到托盘」的还原路径(托盘菜单 / 全局快捷键)才成立。
224
+ * 两头都能调:`start()` 之前调只记下(建窗时按它定稿,隐藏态的窗口首次出现前就是隐藏的);
225
+ * 之后调用投到宿主 UI 线程执行。显示时顺带把窗口提到前台(Linux 是 `gtk_window_present`,
226
+ * Windows 是 `SetForegroundWindow`)。
227
+ * 这是**宿主基元**,两平台都实现,不占能力位(没有平台只有一半)。
228
+ */
229
+ func setWindowVisible(visible: Bool): Unit
230
+
231
+ /**
232
+ * 最小化到托盘(RFC-003 第 5 期余项,`WindowConfig.minimizeToTray` 的落点)
233
+ *
234
+ * enabled=true 时,用户在标题栏点「最小化」不再缩到任务栏,而是**隐藏窗口**
235
+ * (= `setWindowVisible(false)` 的效果);还原走应用自己的托盘菜单 / 全局快捷键。
236
+ * 必须在 `load()` 之前调用(窗口过程要在建窗时就知道这条策略)。
237
+ * 真到最小化那一刻,平台会复核托盘是否**真的**装上了(能力位不含 `CJ_CAP_TRAY`,
238
+ * 或应用压根没调 `setTray`):没装就如实降级为普通最小化并打一行 stderr——
239
+ * 窗口藏起来却没有任何还原入口,是比「不支持」更糟的结果。
240
+ */
241
+ func setMinimizeToTray(enabled: Bool): Unit
242
+
243
+ /**
244
+ * 把本窗口挂到另一扇窗下(**附庸窗 / owned window**):不重复占任务栏、跟随属主显隐
245
+ *
246
+ * 为什么需要它:像 ai-chat 的「网页版」这类**入口聚合窗**,本质是主窗延伸出来的一块,
247
+ * 独立在任务栏再占一格、还能飘到主窗背后去,用户就会把它当成第二个应用。
248
+ *
249
+ * 时机:**在 `load()` 之前**调(与 `setWindowConfig` 同段)——C 桥把属主记进宿主状态,
250
+ * 平台建窗装配期按它定稿,于是任务栏从头到尾只占一格;`load()` 之后再调也能用
251
+ * (投 UI 线程执行),代价是任务栏按钮会先出现再消失。Windows 落 `GWLP_HWNDPARENT`
252
+ * 并补 `WS_EX_TOOLWINDOW`,Linux 落 `gtk_window_set_transient_for` + `skip_taskbar_hint`;
253
+ * 鸿蒙没有系统任务栏语义,走默认实现即可(应用内窗口)。属主未登记 / 本窗尚未创建时
254
+ * **只打一行 stderr 并跳过**——这是外观修饰,不该让开窗失败。
255
+ *
256
+ * 默认实现是空:只有「有系统窗口系统」的平台才需要覆盖它。
257
+ */
258
+ func setOwnerWindow(ownerLabel: String): Unit {
259
+ // 默认实现是空(鸿蒙等无系统任务栏语义的平台):显式弃用参数,保住零警告
260
+ let _ = ownerLabel
261
+ }
262
+
263
+ /**
264
+ * 在本窗口内创建一个内嵌视图(同窗真标签页,PLT-006),返回视图句柄(> 0 有效,0 = 不支持)
265
+ *
266
+ * 为什么需要它:像「网页版」这类入口聚合,把每个站点开成一个**独立窗口**会让任务栏越点越乱、
267
+ * 还会飘到主窗背后去。内嵌视图是同一扇窗里的真标签页——每个 tab 一个 WebView,
268
+ * 切换只是换谁露出来,不重载、也不丢滚动位置(vs「单视图 navigate」要等 1–3 秒重新加载)。
269
+ *
270
+ * 时机:**页面加载之后**(宿主窗口与主 WebView 都已经在)。命令 worker 线程里调也安全——
271
+ * 平台会把建视图的动作投回自己的 UI 线程(Windows 同步建 controller 会死锁,这是官方 known issue)。
272
+ * 返回值**当场**给出,真正的 WebView 稍后才建,所以「句柄有效」不等于「已经能看见内容」。
273
+ *
274
+ * **不注入桥**:内嵌视图装的是第三方站点,它的脚本不该拿到 `window.__CJ_TAURI__`,
275
+ * 更不该能调宿主命令(能力清单管不到外部页面,一旦注入就等于把整份能力开放给任意网站)。
276
+ * 要与站点通信只能走 URL 参数 / 服务端。
277
+ *
278
+ * 坐标是页面 client 坐标(与 `getBoundingClientRect()` 同量纲):视图是**宿主原生控件**,
279
+ * 盖在页面之上、不参与页面排版与裁剪,所以应用要按自己的布局持续上报矩形。
280
+ * 默认实现返回 0:没有窗口内嵌能力的平台(鸿蒙走应用内窗口)如实降级,不假装成功。
281
+ */
282
+ func createEmbeddedView(url: String, x: Int32, y: Int32, width: Int32, height: Int32): Int64 {
283
+ let _ = url
284
+ let _ = x
285
+ let _ = y
286
+ let _ = width
287
+ let _ = height
288
+ return 0
289
+ }
290
+
291
+ /**
292
+ * 显示 / 隐藏内嵌视图——切换 tab 就是这一步(隐藏的视图照常停在原页面,不重载)
293
+ */
294
+ func setEmbeddedViewVisible(view: Int64, visible: Bool): Unit {
295
+ let _ = view
296
+ let _ = visible
297
+ }
298
+
299
+ /**
300
+ * 重排内嵌视图:父窗 resize、DPI 变化、页面自己的布局变化之后都要重报
301
+ *
302
+ * 原生视图**不会**跟随父窗缩放(与页面元素不同),少了这一路同步就会出现露白边 / 盖住侧栏。
303
+ */
304
+ func setEmbeddedViewBounds(view: Int64, x: Int32, y: Int32, width: Int32, height: Int32): Unit {
305
+ let _ = view
306
+ let _ = x
307
+ let _ = y
308
+ let _ = width
309
+ let _ = height
310
+ }
311
+
312
+ /**
313
+ * 让内嵌视图导航到新地址(同一 tab 内跳转用;开新 tab 请再调 createEmbeddedView)
314
+ */
315
+ func navigateEmbeddedView(view: Int64, url: String): Unit {
316
+ let _ = view
317
+ let _ = url
318
+ }
319
+
320
+ /**
321
+ * 销毁内嵌视图(关掉那个 tab):平台会连带收掉它背后的渲染进程
322
+ *
323
+ * 窗口销毁时名下的视图由平台自己收,应用不必逐个 destroy——但**上限淘汰(LRU)必须显式调**,
324
+ * 否则关掉的 tab 只是看不见,内存还占着(一个站点少则几十 MB)。
325
+ */
326
+ func destroyEmbeddedView(view: Int64): Unit {
327
+ let _ = view
328
+ }
329
+
330
+ /**
331
+ * 本宿主的静态能力位(`CJ_CAP_*` 的按位或):能不能做菜单 / 托盘 / 文件拖入
332
+ *
333
+ * 宿主尚未创建(`dispose()` 之后 / `start()` 之前未分配句柄)返回 0。
334
+ * 注意与命令级的能力安全模型无关:这里说的是宿主本事,不是页面权限。
335
+ */
336
+ func hostCapabilities(): Int32
337
+
338
+ /**
339
+ * 装配本窗口的 shell 事件回调(菜单点击等;载荷是 C 桥的 JSON 信封)
340
+ *
341
+ * 与 `setMessageHandler` 同一套路,区别是事件源在宿主侧(应用菜单)而不是页面。
342
+ * **多窗口下必须按宿主身份路由**:C 侧回调首参是句柄,只有本窗口的句柄才转给本实例的
343
+ * 这个回调——这正是「回调带宿主身份」的落地方式(见 docs/RFC-菜单-托盘-拖放.md §5.5)。
344
+ */
345
+ func setShellHandler(handler: (String) -> Unit): Unit
346
+
347
+ /**
348
+ * 装配本窗口的销毁回调(窗口关闭 / 退出收尾时恰好触发一次)
349
+ *
350
+ * 多窗口下按宿主身份路由(C 侧回调首参是句柄),所以每扇窗口装自己的——回调不带参数,
351
+ * 因为「是哪扇窗」在装配时就已确定(应用可经 `app.hostOf(label)` 逐窗安装)。
352
+ * 与 `setShellHandler` 同为**可变槽位**:在 `load()` 之后再装同样生效(路由闭包读字段,不是快照)。
353
+ */
354
+ func setDestroyHandler(handler: () -> Unit): Unit
355
+
356
+ /**
357
+ * 原生文件选择对话框(阻塞到用户关闭):返回选中的路径,取消 / 失败返回空串。
358
+ *
359
+ * filter 是「描述|模式|描述|模式」拼串(Windows 惯例;GTK 端按同一格式解析),
360
+ * 例如 "文本文件|*.txt|所有文件|*.*";空串表示不限制。
361
+ * 必须在 load() 之后调用——对话框要拿已创建的应用窗口当父窗口。
362
+ */
363
+ func showFileDialog(save: Bool, title: String, filter: String): String
364
+
365
+ /**
366
+ * 带默认文件名的文件对话框:除预置名字外与 `showFileDialog` 完全同语义。
367
+ *
368
+ * 预置名字的用途是**自动化取证**——探针对原生框发 IDOK 时框里得已有名字才能关,
369
+ * 返回值因此可预期(预置什么回什么),多窗并发的「结果各归各窗」才验得动。
370
+ * 平台不支持预置(如 GTK 轮未接)时行为退化为 `showFileDialog`,如实降级。
371
+ */
372
+ func showFileDialogPreset(save: Bool, title: String, filter: String, defaultName: String): String
373
+
374
+ /**
375
+ * 原生目录选择对话框(阻塞到用户关闭):返回选中的目录路径,取消 / 失败返回空串。
376
+ *
377
+ * 独立成方法而不是给 `showFileDialog` 加一个开关:选目录在两端都不是「换个动作」——
378
+ * GTK 是另一个 chooser action,Windows 更是另一套 API(`IFileOpenDialog` +
379
+ * `FOS_PICKFOLDERS`,`GetOpenFileName` 没有这个能力)。桥侧两者共用同名同签名的
380
+ * `cj_bridge_show_dialog`,以 kind=6 区分(与文件类 / 消息类并列,见 `dlg_req`)。
381
+ * 与 `showFileDialog` 同规矩:必须在 load() 之后调用(要拿应用窗口当父窗口)。
382
+ */
383
+ func showFolderDialog(title: String): String
384
+
385
+ /**
386
+ * 原生消息对话框(阻塞到用户关闭):返回用户是否点了「确定」。
387
+ *
388
+ * kind 取 "info" / "warning" / "error" / "confirm"(其它值按 info 处理)。
389
+ * info / warning / error 只有一个「确定」,弹出即成立;只有 confirm 会返回 false。
390
+ */
391
+ func showMessageDialog(kind: String, title: String, message: String): Bool
392
+
393
+ /**
394
+ * 写文本进系统剪贴板(上限 8 MiB,C 侧 core 校验)
395
+ *
396
+ * 与对话框同类:宿主能力,要在 load() 之后调用(未就绪时返回 false)。
397
+ */
398
+ func clipboardWriteText(text: String): Bool
399
+
400
+ /**
401
+ * 读系统剪贴板文本:空 / 非文本 / 平台失败都返回 None,不区分(协议层没有可读的失败明细)
402
+ */
403
+ func clipboardReadText(): Option<String>
404
+
405
+ /**
406
+ * 发一条系统通知(发后即忘:提交即返回,显示成败不回传)。
407
+ * 本平台未实现时返回 false(`system:host` 的能力位会同口径如实上报)。
408
+ */
409
+ func notify(title: String, body: String): Bool
410
+
411
+ /**
412
+ * 原生 → JS:在 WebView 中执行脚本
413
+ */
414
+ func runJs(js: String): Unit
415
+
416
+ /**
417
+ * 停止本窗口的事件循环(低层原语)
418
+ *
419
+ * 应用级退出请用 `AppHost.quit()`——它会走所有窗口;单窗口下两者等价。
420
+ * 原语留在宿主上是因为「循环归谁所有」只有实现方知道(两平台的收尾方式不同:
421
+ * Linux 只收主循环、窗口留给收尾销毁,Windows 直接 DestroyWindow),上层不该自己拼。
422
+ */
423
+ func quit(): Unit
424
+
425
+ /**
426
+ * 装配前端消息回调(WebView 送来的 JSON 字符串 → IPC hub)
427
+ */
428
+ func setMessageHandler(handler: (String) -> Unit): Unit
429
+
430
+ /**
431
+ * 宿主事件循环是否已退出(窗口关闭等),app.run 据此阻塞与收尾
432
+ *
433
+ * 同为低层原语:应用级等待用 `AppHost.waitForExit()`,调用方不必自己写轮询。
434
+ */
435
+ func shouldQuit(): Bool
436
+
437
+ /**
438
+ * 释放宿主资源(宿主编由 C 侧分配与回收,跨 FFI 不换分配器):幂等、无需检查调用时机
439
+ *
440
+ * TauriApp.run / runUrl 在 waitForExit() 之后调用它;实现方须把句柄置空——
441
+ * 释放后该实例不可再启动(原生窗口已随销毁路径在关窗时收尾)。
442
+ * 为什么「关窗」不等于「释放」:见 docs/架构演进-多平台与多窗口.md §6.1。
443
+ */
444
+ func dispose(): Unit
445
+ }
446
+
447
+ /**
448
+ * 应用级宿主(对标 Tauri 2 的 `AppHandle`)
449
+ *
450
+ * 与 `WebViewHost` 的分工:`WebViewHost` 是**一个窗口**(创建、投脚本、对话框、本窗口的循环原语),
451
+ * `AppHost` 是**整个应用**(退出、按 label 取窗口;将来还有托盘与应用菜单——批次 3 只定模型)。
452
+ * 批次 2 由 `TauriApp` 实现:它持有窗口注册表,也只有它知道「哪些窗口还开着、应用该不该退」。
453
+ *
454
+ * 为什么 quit / waitForExit 收在这一层:它们跨窗口。宿主仍保留 `quit()` / `shouldQuit()` 作低层原语
455
+ * (循环归宿主所有),但**应用的公开入口只有这里**——多窗口时代的「退出」必然是「先把各窗口收掉」。
456
+ */
457
+ public interface AppHost {
458
+ /**
459
+ * 退出应用:收掉所有窗口的事件循环,`run()` / `runUrl()` 随即返回
460
+ */
461
+ func quit(): Unit
462
+
463
+ /**
464
+ * 阻塞到应用退出(窗口关闭 / quit)后返回;`run()` / `runUrl()` 内部调它
465
+ */
466
+ func waitForExit(): Unit
467
+
468
+ /**
469
+ * 按 label 取窗口宿主(对标 Tauri 的 `app.get_webview_window(label)`);未注册返回 None
470
+ */
471
+ func hostOf(label: String): Option<WebViewHost>
472
+ }
473
+
474
+ /**
475
+ * C 桥原生线程 → 仓颉回调的中转。
476
+ * 各平台宿主在 prepareHost() 时把「本句柄 → 本实例的回调」登记进路由表,C 侧的 @C 函数按句柄取用。
477
+ */
478
+ /**
479
+ * 一条**宿主级回调路由**:C 侧句柄 → 本窗口的三个回调
480
+ *
481
+ * C 侧所有回调的入口都是模块级 @C 函数(每个宿主注册的是同一个函数指针),句柄是唯一能分辨
482
+ * 「哪个窗口」的凭据——三类回调都按句柄查表,查到谁就派发给谁。
483
+ *
484
+ * 闭包都是可变的:读的是宿主实例的字段(不是快照),所以应用在 `load()` 之后再调
485
+ * `setShellHandler` / `setDestroyHandler` 同样生效。
486
+ */
487
+ class HostRoute {
488
+ let handle: CPointer<Unit>
489
+ var onShell: (String) -> Unit
490
+ var onDestroy: () -> Unit
491
+ var onDialog: (String) -> Unit
492
+
493
+ init(handle: CPointer<Unit>, onShell: (String) -> Unit, onDestroy: () -> Unit,
494
+ onDialog: (String) -> Unit) {
495
+ this.handle = handle
496
+ this.onShell = onShell
497
+ this.onDestroy = onDestroy
498
+ this.onDialog = onDialog
499
+ }
500
+ }
501
+
502
+ /**
503
+ * 一条「窗口 label → 系统句柄」登记:附庸窗按 label 找属主窗的句柄
504
+ *
505
+ * 与 `HostRoute` 分开是因为用途不同:路由表按**句柄**查回调(C 侧把句柄带回来),
506
+ * 这里按**label** 查句柄(应用侧只知道主窗的 label)。
507
+ */
508
+ class HostHandleEntry {
509
+ let label: String
510
+ let handle: CPointer<Unit>
511
+
512
+ init(label: String, handle: CPointer<Unit>) {
513
+ this.label = label
514
+ this.handle = handle
515
+ }
516
+ }
517
+
518
+ class HostGlobals {
519
+ // 前端消息:**有意保留单槽**。报文自带发起窗口的 window label(架构文档 §7.3),
520
+ // TauriApp 给所有窗口装的是同一只派发器,路由不靠句柄(见 src/app.cj 的 wireHostCallbacks)。
521
+ static var onMessage: (String) -> Unit = { _ => }
522
+ // 其余三类回调没有「自带身份」的载荷(销毁无载荷、文件框结果只有路径、shell 事件只有信封),
523
+ // 只能让 C 侧把句柄带回来。表里每条只认领自己的句柄。
524
+ // 单槽时代的症状:后装配的窗口覆盖先装配的——「哪扇窗关了」认错、两窗同时开文件框时结果串台
525
+ // (见 docs/架构演进-多平台与多窗口.md §7.5)。
526
+ // 登记发生在 prepareHost(run() 之前、宿主线程还没起),派发时才读表 → 登记期没有并发。
527
+ static var hostRoutes: ArrayList<HostRoute> = ArrayList<HostRoute>()
528
+ // label → 系统句柄:附庸窗(`setOwnerWindow`)要按 label 找属主窗的句柄。
529
+ // 与上面同理——登记发生在 prepareHost(宿主线程还没起),读取发生在 load() 之后,
530
+ // 登记期没有并发;句柄随窗口重建而更新(同 label 覆盖)。
531
+ static var hostHandles: ArrayList<HostHandleEntry> = ArrayList<HostHandleEntry>()
532
+
533
+ /**
534
+ * 登记(或更新)一条宿主级路由
535
+ *
536
+ * 同一句柄重复登记=换掉旧闭包(宿主被重新 prepare 时不叠加、事件也不会派发两次);
537
+ * 句柄比较走 C 侧 `cj_bridge_host_eq`(仓颉的 `CPointer` 不支持 `==`)。
538
+ * 顺带解决「旧宿主销毁、新宿主复用同一地址」:新宿主登记时就覆盖了那条陈旧路由。
539
+ */
540
+ static func registerHostRoute(handle: CPointer<Unit>, onShell: (String) -> Unit,
541
+ onDestroy: () -> Unit, onDialog: (String) -> Unit): Unit {
542
+ for (r in HostGlobals.hostRoutes) {
543
+ if (hostHandleEquals(r.handle, handle)) {
544
+ r.onShell = onShell
545
+ r.onDestroy = onDestroy
546
+ r.onDialog = onDialog
547
+ return
548
+ }
549
+ }
550
+ HostGlobals.hostRoutes.add(HostRoute(handle, onShell, onDestroy, onDialog))
551
+ }
552
+
553
+ /**
554
+ * 按句柄取路由;查不到返回 None(句柄不属于本进程,或该宿主已注销)
555
+ */
556
+ static func hostRouteOf(handle: CPointer<Unit>): Option<HostRoute> {
557
+ for (r in HostGlobals.hostRoutes) {
558
+ if (hostHandleEquals(r.handle, handle)) {
559
+ return Some(r)
560
+ }
561
+ }
562
+ return None
563
+ }
564
+
565
+ /**
566
+ * 登记(或更新)某窗口的系统句柄;同一 label 重复登记=覆盖(窗口重建后句柄会变)
567
+ */
568
+ static func registerHostHandle(label: String, handle: CPointer<Unit>): Unit {
569
+ for (i in 0..HostGlobals.hostHandles.size) {
570
+ if (HostGlobals.hostHandles[i].label == label) {
571
+ HostGlobals.hostHandles[i] = HostHandleEntry(label, handle)
572
+ return
573
+ }
574
+ }
575
+ HostGlobals.hostHandles.add(HostHandleEntry(label, handle))
576
+ }
577
+
578
+ /**
579
+ * 按 label 取句柄;查不到返回 None(该窗口尚未 prepare,或 label 写错)
580
+ */
581
+ static func handleOfLabel(label: String): Option<CPointer<Unit>> {
582
+ for (h in HostGlobals.hostHandles) {
583
+ if (h.label == label) {
584
+ return Some(h.handle)
585
+ }
586
+ }
587
+ return None
588
+ }
589
+ }
590
+
591
+ /**
592
+ * kind 字符串 → C 桥的对话框编号(与 native/bridge_linux.c、bridge_win.c 的取值一一对应)
593
+ */
594
+ func dialogKindCode(kind: String): Int32 {
595
+ match (kind) {
596
+ case "warning" => 3
597
+ case "error" => 4
598
+ case "confirm" => 5
599
+ case _ => 2
600
+ }
601
+ }
602
+
603
+ /**
604
+ * 对话框结果回调(C 桥 → 仓颉):文件类对话框选中的路径经它送回,首参是**发起这次对话框的宿主句柄**。
605
+ *
606
+ * 由 C 桥在自己的 UI 线程上调用,与 hostOnMessage 同一套路;按句柄写进对应宿主的结果位,
607
+ * 再由桥唤醒阻塞的调用方去读。多窗口下两扇窗可以各自开着文件框,结果不许串台。
608
+ * 非 @When:两平台的 C 侧都调它(签名一致,首参不透明)。
609
+ */
610
+ @C
611
+ func dialogOnResult(host: CPointer<Unit>, path: CString): Unit {
612
+ try {
613
+ match (HostGlobals.hostRouteOf(host)) {
614
+ case Some(r) => r.onDialog(path.toString())
615
+ case None => eprintln("[cj-tauri] dialog callback: 未登记的宿主句柄(已丢弃)")
616
+ }
617
+ } catch (e: Exception) {
618
+ eprintln("[cj-tauri] dialog callback error: ${e}")
619
+ }
620
+ }
621
+
622
+ /**
623
+ * 窗口销毁回调(C 桥 → 仓颉):首参是**被销毁的宿主句柄**(每个宿主恰好触发一次)。
624
+ *
625
+ * 曾经写进全局单槽 → 多窗口下「哪扇窗关了」认错(后装配的窗口覆盖先装配的);
626
+ * 现在与 shell / 对话框一样按句柄认领。非 @When:两平台的 C 侧都调它。
627
+ */
628
+ @C
629
+ func hostOnDestroy(host: CPointer<Unit>): Unit {
630
+ try {
631
+ match (HostGlobals.hostRouteOf(host)) {
632
+ case Some(r) => r.onDestroy()
633
+ case None => eprintln("[cj-tauri] destroy callback: 未登记的宿主句柄(已忽略)")
634
+ }
635
+ } catch (e: Exception) {
636
+ eprintln("[cj-tauri] destroy callback error: ${e}")
637
+ }
638
+ }
639
+
640
+ /**
641
+ * shell 事件回调(C 桥 → 仓颉):菜单点击等经它送回,首参是**发生事件的宿主句柄**。
642
+ *
643
+ * 由 C 桥在宿主 UI 线程上调用(与 hostOnMessage 同一套路);句柄→实例的解引用走宿主级路由表,
644
+ * 本函数只做「别抛穿 FFI 边界」这一件事。
645
+ * 非 @When:两平台的 C 侧都调它(签名一致,首参不透明)。
646
+ */
647
+ @C
648
+ func shellOnEvent(host: CPointer<Unit>, json: CString): Unit {
649
+ try {
650
+ // 认领自己那条:句柄对不上就静默跳过(C 侧只有一个入口,区分就在这里)
651
+ match (HostGlobals.hostRouteOf(host)) {
652
+ case Some(r) => r.onShell(json.toString())
653
+ case None => () // 未登记(旧宿主已注销 / 非本进程句柄):静默,别在热路径上刷屏
654
+ }
655
+ } catch (e: Exception) {
656
+ eprintln("[cj-tauri] shell callback error: ${e}")
657
+ }
658
+ }
659
+
660
+ /**
661
+ * 创建当前平台的 WebView 宿主(对标 Tauri 2 的 runtime 选择)
662
+ *
663
+ * label 按窗口给定(多窗口):默认窗口传 DEFAULT_WINDOW_LABEL,附加窗口传各自登记的 label。
664
+ * 必须是构造参数而不是事后 setter——label 决定页面侧 `window.__CJ_TAURI_LABEL__` 与事件过滤,
665
+ * 启动之后再改就晚了(见 docs/架构演进-多平台与多窗口.md §7.3)。
666
+ */
667
+ // 判据为什么是 `env`(2026-10-19 校正):鸿蒙目标下 `os` 仍取 `Linux`,`arch` 也只有
668
+ // `aarch64`——桌面 aarch64 Linux 同样是 `aarch64`,拿它当判据会把「跑在 GTK 上」误判成
669
+ // 「跑在 ArkWeb 上」,两边的宿主实现会互相打架(编译期就撞名)。官方条件编译映射表给出
670
+ // `aarch64-linux-ohos` → `env == "ohos"`、桌面 Linux → `gnu`,这才是真正互斥的维度。
671
+ // 于是桌面 Linux 侧写 `env != "ohos"`:**桌面 aarch64 Linux 因此重新拿回 WebKit 宿主**
672
+ // (改判据之前它落到「只能注入宿主」的分支,是同一处误判的另一半)。
673
+ @When[os == "Linux" && env != "ohos"]
674
+ func createHost(label: String): WebViewHost {
675
+ return WebKitHost(label)
676
+ }
677
+
678
+ // 鸿蒙:内置 ArkWeb 宿主(应用侧只负责建 ArkUI 组件并把 controller 交给它,见 src/host_arkweb.cj)
679
+ @When[env == "ohos"]
680
+ func createHost(label: String): WebViewHost {
681
+ return ArkWebHost(label)
682
+ }
683
+
684
+ @When[os == "Windows"]
685
+ func createHost(label: String): WebViewHost {
686
+ return WebView2Host(label)
687
+ }
688
+
689
+ /**
690
+ * 两个宿主句柄是否指向同一宿主
691
+ *
692
+ * 桌面两平台走 C 桥 `cj_bridge_host_eq`——仓颉的 `CPointer` 既没有 `==` 也没有 `!=`
693
+ * (实测报 `invalid binary operator '!=' on type 'CPointer<Unit>'`),也没有 `hashCode`,
694
+ * 比较只能交给 C 侧。鸿蒙侧没有 C 桥(`native/platforms/harmonyos/` 尚未产出 `cj_bridge_*`),
695
+ * `ArkWebHost` 的句柄在仓颉侧不参与路由(回调是仓颉闭包,不经 C 中转),因此恒为 false
696
+ * 并留一行 stderr——**不假装相等**(假装相等会把两扇窗的路由串台)。
697
+ */
698
+ @When[os == "Windows"]
699
+ func hostHandleEquals(a: CPointer<Unit>, b: CPointer<Unit>): Bool {
700
+ unsafe {
701
+ return cj_bridge_host_eq(a, b) != 0
702
+ }
703
+ }
704
+
705
+ @When[os == "Linux" && env != "ohos"]
706
+ func hostHandleEquals(a: CPointer<Unit>, b: CPointer<Unit>): Bool {
707
+ unsafe {
708
+ return cj_bridge_host_eq(a, b) != 0
709
+ }
710
+ }
711
+
712
+ // 只在无 C 桥的平台用到,声明也放进同一门禁:桌面侧保留会变成 unused variable
713
+ @When[env == "ohos"]
714
+ private var HOST_ROUTE_WARNED: Bool = false
715
+
716
+ @When[env == "ohos"]
717
+ func hostHandleEquals(a: CPointer<Unit>, b: CPointer<Unit>): Bool {
718
+ let _ = a
719
+ let _ = b
720
+ if (!HOST_ROUTE_WARNED) {
721
+ HOST_ROUTE_WARNED = true
722
+ eprintln("[cj-tauri] host route by handle unavailable on this platform: injected host keeps its own callbacks")
723
+ }
724
+ return false
725
+ }
726
+
727
+ /**
728
+ * 造一个「裸宿主句柄」:只分配 C 侧宿主对象(不 init / 不 start、不碰图形栈)
729
+ *
730
+ * 只给单元测试用(子包可见,不算对外 API)。为什么需要这层包皮:`cj_bridge_create` 是
731
+ * `foreign func`——没有函数体、也就没有跨包符号,子包 `cjTauri.tests` 直接调会在**链接期**炸
732
+ * (实测 `ld.lld: error: undefined symbol: cjTauri:cj_bridge_create`);框架包内包一层就有实体符号了。
733
+ *
734
+ * 只编桌面两平台:鸿蒙没有 C 桥也就没有这个符号,而单测只在桌面跑(`cjpm test` 用桌面目标)。
735
+ */
736
+ @When[os == "Windows"]
737
+ func createBareHostHandle(): CPointer<Unit> {
738
+ var h: CPointer<Unit> = CPointer<Unit>()
739
+ unsafe {
740
+ h = cj_bridge_create()
741
+ }
742
+ return h
743
+ }
744
+
745
+ @When[os == "Linux" && env != "ohos"]
746
+ func createBareHostHandle(): CPointer<Unit> {
747
+ var h: CPointer<Unit> = CPointer<Unit>()
748
+ unsafe {
749
+ h = cj_bridge_create()
750
+ }
751
+ return h
752
+ }