xbintsc 0.3.69 → 0.3.79

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 (99) hide show
  1. package/README.md +59 -4
  2. package/README.zh-CN.md +52 -2
  3. package/dist/tests/e2e/gui-advanced.test.d.ts +6 -0
  4. package/dist/tests/e2e/gui-advanced.test.js +261 -0
  5. package/dist/tests/e2e/gui-advanced.test.js.map +1 -0
  6. package/dist/tests/e2e/gui-canvas.test.d.ts +9 -0
  7. package/dist/tests/e2e/gui-canvas.test.js +233 -0
  8. package/dist/tests/e2e/gui-canvas.test.js.map +1 -0
  9. package/dist/tests/e2e/gui-example.test.d.ts +1 -1
  10. package/dist/tests/e2e/gui-example.test.js +2 -2
  11. package/dist/tests/e2e/gui-helpers.d.ts +3 -0
  12. package/dist/tests/e2e/gui-helpers.js +5 -0
  13. package/dist/tests/e2e/gui-helpers.js.map +1 -1
  14. package/dist/tests/e2e/gui-layout.test.js +73 -0
  15. package/dist/tests/e2e/gui-layout.test.js.map +1 -1
  16. package/dist/tests/e2e/gui-media.test.d.ts +8 -0
  17. package/dist/tests/e2e/gui-media.test.js +119 -0
  18. package/dist/tests/e2e/gui-media.test.js.map +1 -0
  19. package/dist/tests/e2e/gui-paint.test.js +40 -0
  20. package/dist/tests/e2e/gui-paint.test.js.map +1 -1
  21. package/doc/ai/extensions.md +16 -3
  22. package/doc/ai/zh-CN/extensions.md +10 -3
  23. package/doc/gui/animation.md +46 -0
  24. package/doc/gui/api.md +92 -0
  25. package/doc/gui/build.md +91 -0
  26. package/doc/gui/canvas.md +98 -0
  27. package/doc/gui/dom.md +123 -0
  28. package/doc/gui/input.md +22 -0
  29. package/doc/gui/known-issues.md +66 -0
  30. package/doc/gui/layout.md +72 -0
  31. package/doc/gui/media.md +87 -0
  32. package/doc/gui/rendering.md +98 -0
  33. package/doc/gui/runtime.md +88 -0
  34. package/doc/gui-scripts.md +7 -3
  35. package/doc/gui.md +80 -612
  36. package/doc/implemented.md +1 -0
  37. package/doc/zh-CN/gui/animation.md +46 -0
  38. package/doc/zh-CN/gui/api.md +88 -0
  39. package/doc/zh-CN/gui/build.md +84 -0
  40. package/doc/zh-CN/gui/canvas.md +80 -0
  41. package/doc/zh-CN/gui/dom.md +114 -0
  42. package/doc/zh-CN/gui/input.md +21 -0
  43. package/doc/zh-CN/gui/known-issues.md +50 -0
  44. package/doc/zh-CN/gui/layout.md +60 -0
  45. package/doc/zh-CN/gui/media.md +71 -0
  46. package/doc/zh-CN/gui/rendering.md +81 -0
  47. package/doc/zh-CN/gui/runtime.md +76 -0
  48. package/doc/zh-CN/gui-scripts.md +6 -2
  49. package/doc/zh-CN/gui.md +64 -532
  50. package/doc/zh-CN/implemented.md +1 -0
  51. package/llms.txt +15 -2
  52. package/package.json +1 -1
  53. package/runtime/ext_gui/canvas.cpp +580 -0
  54. package/runtime/ext_gui/canvas.h +170 -0
  55. package/runtime/ext_gui/canvas_internal.h +59 -0
  56. package/runtime/ext_gui/canvas_raster.cpp +254 -0
  57. package/runtime/ext_gui/document.cpp +24 -1
  58. package/runtime/ext_gui/document.h +11 -0
  59. package/runtime/ext_gui/dom_api.cpp +3 -0
  60. package/runtime/ext_gui/dom_api_canvas.cpp +548 -0
  61. package/runtime/ext_gui/dom_api_internal.h +73 -0
  62. package/runtime/ext_gui/dom_api_media.cpp +274 -0
  63. package/runtime/ext_gui/dom_api_proto.cpp +80 -0
  64. package/runtime/ext_gui/gui.cpp +6 -0
  65. package/runtime/ext_gui/gui_canvas.cpp +95 -0
  66. package/runtime/ext_gui/gui_engine.h +49 -0
  67. package/runtime/ext_gui/gui_events.cpp +21 -5
  68. package/runtime/ext_gui/gui_media.cpp +193 -0
  69. package/runtime/ext_gui/image.cpp +31 -12
  70. package/runtime/ext_gui/image.h +20 -7
  71. package/runtime/ext_gui/layout.cpp +52 -5
  72. package/runtime/ext_gui/layout.h +33 -1
  73. package/runtime/ext_gui/layout_flex.cpp +263 -99
  74. package/runtime/ext_gui/layout_flow.cpp +85 -9
  75. package/runtime/ext_gui/layout_flow_internal.h +84 -5
  76. package/runtime/ext_gui/layout_inline.cpp +54 -37
  77. package/runtime/ext_gui/media.cpp +514 -0
  78. package/runtime/ext_gui/media.h +104 -0
  79. package/runtime/ext_gui/nanosvg.h +3278 -0
  80. package/runtime/ext_gui/nanosvgrast.h +1472 -0
  81. package/runtime/ext_gui/paint.cpp +123 -38
  82. package/runtime/ext_gui/paint.h +36 -6
  83. package/runtime/ext_gui/renderer.cpp +113 -10
  84. package/runtime/ext_gui/renderer.h +49 -9
  85. package/runtime/ext_gui/renderer_image.cpp +203 -77
  86. package/runtime/ext_gui/renderer_internal.h +6 -8
  87. package/runtime/ext_gui/renderer_text.cpp +11 -26
  88. package/runtime/ext_gui/style.h +11 -0
  89. package/runtime/ext_gui/style_print.cpp +11 -0
  90. package/runtime/ext_gui/style_ua.cpp +7 -0
  91. package/runtime/ext_gui/style_values.cpp +40 -1
  92. package/runtime/ext_gui/svg.cpp +182 -0
  93. package/runtime/ext_gui/svg.h +46 -0
  94. package/runtime/ext_gui/text.cpp +156 -25
  95. package/runtime/ext_gui/text.h +11 -2
  96. package/runtime/ext_gui/window.cpp +4 -1
  97. package/scripts/build-gui.ts +148 -6
  98. package/scripts/check-file-length.ts +5 -1
  99. package/scripts/install-gui-deps.sh +28 -0
package/doc/zh-CN/gui.md CHANGED
@@ -2,7 +2,26 @@
2
2
 
3
3
  > 语言 / Language:[English](../gui.md) | **简体中文**
4
4
 
5
- 状态:**M12** —— 功能(M1–M11)已全部完成:HTML 解析、CSS 选择器匹配、层叠(cascade)、计算样式与布局(块级、行内、Flexbox 与 CSS 定位)均已就位,引擎还会**绘制**:它构建一个由矩形、图像与排版后的文本 run 组成的显示列表,并通过 SDL_GPU 渲染。输入事件会经过命中测试并投递给原生 TS 处理器,`:hover`/`:focus` 会被动态匹配,`<img>` 依据其固有尺寸确定大小并从纹理绘制,CSS transition 会为绘制属性做动画。**M8** 增加了交互式 DOM:具有稳定标识的元素句柄、变更操作(`appendChild`、`textContent`、`classList`、`style` 等)以及支持捕获/冒泡的元素级事件。**M9** 会提前编译 `<script>` 主体(内联与 `<script src>`)——参见 `gui-scripts.md`。**M10** 增加 `requestAnimationFrame` 以及若干 DOM 辅助方法。**M11** 增加 `position: relative`/`absolute`/`fixed` 及其偏移,`examples/gui/pelican-bike`(一个可玩的 2D 游戏)就构建在它之上。**M12** 明确了 DOM 节点生命周期(哪些变更释放、哪些保留),并让引擎内存有界:参见"节点生命周期"与"内存与 GC"。本文档记录了一个跨平台 GUI 扩展的锁定决策、架构、里程碑计划与当前进度;该扩展使用自有的 GPU 加速引擎渲染 HTML/CSS UI。
5
+ 状态:**M14** —— 功能(M1–M13)已全部完成:HTML 解析、CSS 选择器匹配、层叠(cascade)、计算样式与布局(块级、行内、Flexbox 与 CSS 定位)均已就位,引擎还会**绘制**:它构建一个由矩形、图像与排版后的文本 run 组成的显示列表,并通过 SDL_GPU 渲染。输入事件会经过命中测试并投递给原生 TS 处理器,`:hover`/`:focus` 会被动态匹配,`<img>` 依据其固有尺寸确定大小并从纹理绘制——位图(PNG/JPEG/…)与 SVG 文件皆可,后者按绘制尺寸光栅化,因此矢量图保持清晰——CSS transition 会为绘制属性做动画。**M8** 增加了交互式 DOM:具有稳定标识的元素句柄、变更操作(`appendChild`、`textContent`、`classList`、`style` 等)以及支持捕获/冒泡的元素级事件。**M9** 会提前编译 `<script>` 主体(内联与 `<script src>`)——参见 `gui-scripts.md`。**M10** 增加 `requestAnimationFrame` 以及若干 DOM 辅助方法。**M11** 增加 `position: relative`/`absolute`/`fixed` 及其偏移,`examples/gui/pelican-bike`(一个可玩的 2D 游戏)就构建在它之上。**M12** 明确了 DOM 节点生命周期(哪些变更释放、哪些保留),并让引擎内存有界:参见[节点生命周期](./gui/dom.md)与[内存与 GC](./gui/runtime.md)。**M13** 真正播放
6
+ `<video>`/`<audio>`——一个 vendored 的最小 FFmpeg 在主线程解码,视频帧走图像
7
+ 纹理路径、音频进入 SDL 流,DOM 媒体 API 暴露 `play`/`pause`/`currentTime`/…
8
+ 与媒体事件。**M14** 增加了 `<canvas>`,配一个自研的 2D 软件光栅器(抗锯齿
9
+ 填充、路径、文本与 `drawImage`),并经图像纹理路径上传——参见
10
+ [Canvas](./gui/canvas.md)。本总览记录了锁定决策、架构、里程碑计划与当前进度;引擎本身按功能拆分在以上各页中。
11
+
12
+ ## 目录
13
+
14
+ - [API](./gui/api.md) —— 窗口句柄、事件与诊断
15
+ - [DOM、HTML 与 CSS](./gui/dom.md) —— 解析、层叠、交互式 DOM 与 AOT 脚本
16
+ - [布局](./gui/layout.md) —— 块级、行内、Flexbox 与定位
17
+ - [绘制](./gui/rendering.md) —— 显示列表、GPU 管线与文本
18
+ - [媒体](./gui/media.md) —— 图像、SVG、视频与音频
19
+ - [Canvas](./gui/canvas.md) —— `<canvas>` 与 2D 上下文
20
+ - [输入](./gui/input.md) —— 命中测试、事件与 `:hover`/`:focus`
21
+ - [动画](./gui/animation.md) —— `requestAnimationFrame` 与 CSS transition
22
+ - [运行时集成](./gui/runtime.md) —— 事件循环、原生桥接与内存
23
+ - [打包与构建](./gui/build.md) —— 构建 `gui.a` 与运行示例
24
+ - [已知问题与待决问题](./gui/known-issues.md)
6
25
 
7
26
  ## 目标
8
27
 
@@ -49,7 +68,7 @@
49
68
  ▼
50
69
  窗口 + 输入(SDL3)── 多窗口
51
70
  文本(HarfBuzz + FreeType)── 字形图集 / SDF
52
- 图像(stb_image)
71
+ 图像(stb_image;SVG 经 NanoSVG)
53
72
  ```
54
73
 
55
74
  引擎从不与编译器通信。编译器只看到一个带有 `nativeObjects()`、`linkerFlags()` 和 `modules()` 的 `Extension`。
@@ -62,10 +81,12 @@
62
81
  | GPU | **SDL_GPU**(SDL3) | 一条覆盖 Metal / Vulkan / D3D12 的渲染路径;避免三套后端 |
63
82
  | 文本整形 | **HarfBuzz** | 正确处理复杂文种的整形 |
64
83
  | 字形光栅化 | **FreeType** | 字形轮廓 → GPU 图集 / SDF |
65
- | 图像 | **stb_image**(vendored 头文件) | 起步阶段用单头文件 |
84
+ | 图像 | 位图用 **stb_image**(vendored 头文件),SVG 用 **NanoSVG**(vendored 头文件) | 起步阶段用单头文件;SVG 文件按需光栅化 |
85
+ | 视频 + 音频 | **FFmpeg**(静态 vendored,最小化构建) | 解码常用容器/编码;不依赖 WebView 或平台媒体框架 |
66
86
 
67
- 固定版本:SDL3 `release-3.2.10`、FreeType `2.13.3`、HarfBuzz `10.1.0`
68
- (可用 `SDL3_TAG` / `FREETYPE_VERSION` / `HARFBUZZ_VERSION` 覆盖)。
87
+ 固定版本:SDL3 `release-3.2.10`、FreeType `2.13.3`、HarfBuzz `10.1.0`、
88
+ FFmpeg `7.1`(可用 `SDL3_TAG` / `FREETYPE_VERSION` / `HARFBUZZ_VERSION` /
89
+ `FFMPEG_VERSION` 覆盖)。
69
90
 
70
91
  如果否决 SDL3,备选是 **GLFW + OpenGL 3.3**(更简单,但 OpenGL 在 macOS 上
71
92
  已弃用,且不提供现代 GPU 抽象)。如果引擎用 Rust 编写,则用 **wgpu-native**。
@@ -73,458 +94,6 @@
73
94
  > 这些 vendored 库会**静态链接进 `gui.a`**,因此发布的 xbintsc 保持
74
95
  > “下载即用”;`linkerFlags()` 只添加操作系统框架。
75
96
 
76
- ## 面向 TS 的 API
77
-
78
- ```ts
79
- import { createWindow, run, quit } from "gui";
80
-
81
- const win = createWindow({ title: "Demo", width: 900, height: 600 });
82
- win.setBackground("#14161c");
83
- win.loadHTML(INDEX_HTML); // 解析 HTML/CSS 并计算样式
84
- win.on("ready", () => console.log("first frame presented"));
85
- win.on("close", () => console.log("window closed"));
86
-
87
- run(); // 驱动主循环,直到所有窗口关闭
88
- ```
89
-
90
- 窗口句柄上已实现的方法:`setTitle` / `setSize` / `loadHTML` /
91
- `getHTML` / `setBackground` / `close` / `isOpen` / `on` / `off`,以及
92
- `driver()`——它报告当前使用的 SDL_GPU 后端(`"vulkan"`、`"direct3d12"`、
93
- `"metal"` 等,由 SDL 挑选设备支持的最佳后端)。同时也提供 Web 写法:
94
- `addEventListener` / `removeEventListener` 是 `on` / `off` 的别名(其
95
- `options` 参数会被接受并忽略),`closed` getter 对应 `isOpen()`,因此按浏览器
96
- API 编写的文档可以原样运行。
97
-
98
- 在 Windows 上,GUI 程序会被链接为 **Windows 子系统**可执行文件,因此启动时不会闪出控制台
99
- 窗口;这也意味着从资源管理器启动时 stdout/stderr 无处可去,调试时可在
100
- `xbintsc.config.json` 里设置 `app.console: true` 保留控制台子系统。
101
-
102
- 触发的事件:
103
- `ready`(首个呈现帧之后)、`load`、`close`、`resize`,以及输入事件
104
- `mousemove`、`mousedown`、`mouseup`、`click`、`wheel`、`keydown`、`keyup`。
105
-
106
- 输入处理器接收单个负载对象(生命周期处理器不接收任何参数):
107
-
108
- ```ts
109
- win.on("click", (e) => console.log(e.x, e.y, e.button, e.target));
110
- win.on("wheel", (e) => console.log(e.deltaX, e.deltaY));
111
- win.on("keydown", (e) => console.log(e.key, e.code, e.ctrl, e.shift, e.alt, e.meta));
112
- ```
113
-
114
- | 字段 | 事件 | 含义 |
115
- | --- | --- | --- |
116
- | `x`、`y` / `clientX`、`clientY` | pointer、wheel | 相对于视口的逻辑像素 |
117
- | `button` | pointer | `0` 左键、`1` 中键、`2` 右键(移动时为 `-1`) |
118
- | `clicks` | pointer | 操作系统报告的点击次数 |
119
- | `deltaX`、`deltaY` | wheel | 滚动量(`deltaY` 为正表示向下) |
120
- | `key`、`code` | keyboard | 键名与物理扫描码名 |
121
- | `repeat`、`ctrl`、`shift`、`alt`、`meta` | keyboard | 修饰键 |
122
- | `target` | pointer、wheel | 该点下最深的元素,表示为类 CSS 描述符(`div#main.card`),可能缺省 |
123
-
124
- ### 已实现的 HTML/CSS 子集(M3a)
125
-
126
- **HTML 解析器**(`runtime/ext_gui/dom.{h,cpp}`):标签/属性/文本,注释与
127
- doctype 跳过,实体解码(命名 + 十进制/十六进制),空元素,原始文本元素
128
- (`<style>`/`<script>`),以及常见的隐式闭合规则(`li`、`dt`/`dd`、`option`、
129
- `p`、标题、表格单元格/行)。
130
-
131
- **CSS 解析器**(`runtime/ext_gui/css.{h,cpp}`):注释与 at-rule 暂时跳过,
132
- 支持多选择器的规则以及声明。选择器:类型、`.class`、`#id`、属性
133
- (`=`、`~=`、`|=`、`^=`、`$=`、`*=`)、四种组合器(后代、子、相邻兄弟与
134
- 通用兄弟)以及伪类 `:first-child`、`:last-child`、`:only-child`、`:empty`、
135
- `:root`、`:not(...)`、`:nth-child(an+b)`、`:disabled`、`:checked`,再加上
136
- 有状态的 `:hover` 与 `:focus`(见 *已实现的输入*)。值:长度
137
- (`px`、`%`、`em`、`rem`、`vw`、`vh`、`pt`、`pc`、`in`、`cm`、`mm`、`q`)、
138
- 颜色(十六进制、`rgb()`/`rgba()`、一个命名子集)、数字、关键字与简写
139
- (`margin`/`padding`/`border`/`flex`)。
140
-
141
- **层叠与计算样式**(`runtime/ext_gui/style.{h,cpp}`):一份小的内置 UA
142
- 样式表,作者规则按 `(!important, 特异性, 源码顺序)` 排序,然后是内联
143
- `style=""`(特异性最高,但非 `!important` 的内联会输给 `!important`),
144
- 再加上文本属性的 CSS 继承。相对长度保持未解析状态直到布局阶段,`font-size`
145
- 除外(相对*父级*字号解析)以及 `line-height`。
146
-
147
- ### 诊断
148
-
149
- 为了让 HTML/CSS/绘制流水线在没有 GPU 的情况下也可测试,窗口句柄暴露只读钩子:
150
-
151
- ```ts
152
- win.computedStyle(selector, property) // e.g. ("#main", "width") -> "60%"
153
- win.queryCount(selector) // 匹配元素的数量
154
- win.getBoundingClientRect(selector) // { x, y, width, height }(边框盒)
155
- win.documentTree() // 序列化的 DOM(调试用)
156
- win.layoutTree() // 序列化的布局盒(调试用)
157
- win.paintCount() // 显示列表中图形的数量
158
- win.paintList() // 序列化的显示列表(调试用)
159
- win.measureText(text, fontSize?, family?) // 整形后的推进宽度(像素)
160
- win.fontMetrics(fontSize?, family?) // { ascent, descent, lineHeight, ready }
161
- win.hitTest(x, y) // 最深的元素描述符,或 ""
162
- win.sendEvent(type, options?) // 合成输入(测试用)
163
- win.advance(ms) // 推进 CSS transition 时钟(测试用)
164
- win.stats() // 引擎/运行时计数器(内存排查用)
165
- ```
166
-
167
- `win.stats()` 返回 `{ heapBytes, heapAllocations, treeNodes, detachedRoots,`
168
- `detachedNodes, handles, handleSlots, listeners }`:运行时存活堆、DOM 树规模、
169
- 分离节点池(detached pool)以及元素句柄表。稳定状态下它们都应保持常量——若
170
- `detachedNodes` 或 `handles` 持续上升,说明程序移除节点的速度快于释放速度
171
- (见下文"节点生命周期")。
172
-
173
- 它们被 `tests/e2e/gui-*.test.ts` 用来断言解析、选择器匹配、特异性、继承、
174
- `!important` 与布局几何。它们在之后仍可用于调试。
175
-
176
- ### 交互式 DOM(M8)
177
-
178
- `win.document` 返回文档句柄;元素句柄具有稳定标识,并提供可读写的属性、
179
- 特性(attribute)、遍历与几何信息:
180
-
181
- ```ts
182
- const doc = win.document;
183
- const box = doc.querySelector("#box");
184
- const inner = doc.querySelector("#inner");
185
- inner.textContent = "hi"; // 写入
186
- inner.classList.add("hot");
187
- inner.style.setProperty("color", "#0f0");
188
- inner.setAttribute("data-role", "lead");
189
- console.log(inner.id, inner.tagName, doc.querySelector("#box") === box);
190
-
191
- const created = doc.createElement("div");
192
- created.textContent = "added";
193
- box.appendChild(created);
194
- box.removeChild(created);
195
-
196
- inner.addEventListener("click", (e) => console.log(e.target.id, e.currentTarget.id));
197
- inner.click(); // 在该元素处合成一次点击
198
- console.log(box.offsetWidth, box.offsetHeight); // 取整后的边框盒
199
- console.log(box.contains(inner)); // 后代判断
200
- ```
201
-
202
- 变更操作会把文档标记为脏;引擎会延迟地(在下一次读取或下一帧之前)重新计算
203
- 样式并重新布局。`inner = …`/`innerHTML = …` **不会**执行脚本。元素事件支持
204
- 捕获与冒泡阶段、`stopPropagation`、`once`,并向上冒泡到 `document`/`window`。
205
- 旧式 `win.on(type, fn)` 负载保留其**字符串** `e.target`(`div#id.class`);
206
- 元素 `Event.target` 是一个句柄,其描述符与同一字符串匹配。
207
-
208
- #### 节点生命周期
209
-
210
- 一次变更会决定它移除的节点是否仍可到达,引擎只释放那些确实不可到达的节点:
211
-
212
- | 变更 | 被移除的节点 |
213
- | --- | --- |
214
- | `innerHTML = …`、`inner = …`、`textContent = …`、`replaceChild` | **被释放**——脚本再也无法引用它们 |
215
- | `removeChild`、`remove` | **保留**在文档的分离节点池中——DOM 语义要求被移除的节点继续存活且可用,以便重新插入 |
216
-
217
- 被释放的节点会同时释放其 C++ 子树与句柄,因此 `win.stats()` 的计数器
218
- (`detachedNodes`、`handles`、`handleSlots`)会回到先前的数值。这正是常见游戏
219
- 写法——每帧用 `innerHTML` 重写精灵——能保持常量内存的原因;在此之前,每个被
220
- 替换的子树都会永久留在池中,每帧写入十个节点的循环会以每秒数 MB 的速度泄漏。
221
-
222
- 引擎唯一无法察觉的情况是:脚本创建了一个节点,随后从未插入就丢弃它。
223
- `document.createElement("div")` 会把节点放进池中,**并且**让它的句柄保持可达,
224
- 因为标记-清扫收集器没有弱引用,引擎无法知道脚本已经丢弃了该句柄。因此,
225
- 只创建、不插入的循环会让池持续增长。请复用元素,或用 `innerHTML` 构建内容
226
- (它会释放被替换的节点),而不要每帧新建元素。
227
-
228
- ### AOT 脚本(M9)
229
-
230
- 导入一个包含内联 `<script lang="ts">` 主体的 `.html` 文件;加载器会把每个
231
- 主体编译为一个原生函数,`win.loadHTML(page)` 会在文档解析完成后运行它们
232
- (没有 JavaScript 引擎,没有运行时 `eval`):
233
-
234
- ```ts
235
- import { createWindow, run } from "gui";
236
- import page from "./page.html";
237
-
238
- const win = createWindow({ title: "counter", width: 320, height: 240 });
239
- win.loadHTML(page);
240
- run();
241
- ```
242
-
243
- ```html
244
- <button id="b">0</button>
245
- <script lang="ts">
246
- const b = document.getElementById("b");
247
- let n = 0;
248
- b.addEventListener("click", () => { b.textContent = String(++n); });
249
- </script>
250
- ```
251
-
252
- 这两个参数(`window`、`document`)只是普通的函数参数,因此脚本的局部变量
253
- 无需任何全局对象机制。外部脚本同样可用:`<script src="./counter.ts">` 会在
254
- 编译期读取,其 import 会被提升(重写为从 HTML 文件解析),其主体以同样方式
255
- 包装——因此脚本模块可以 `import` 辅助函数,同时仍能看到 `document`。脚本是
256
- **编译期资产**:运行时创建的 HTML(`innerHTML`、网络获取)永不执行,`src`
257
- URL(`https://…`、`data:…`)会被忽略。一切都按文档顺序在解析后运行(实际上
258
- 是 deferred)。完整设计参见 `gui-scripts.md`。
259
-
260
- ### 动画帧(M10)
261
-
262
- `requestAnimationFrame` 在下一帧运行一次回调;回调接收帧时间戳(毫秒),
263
- 并且可以修改 DOM,DOM 会在同一帧内重新计算样式并重绘。在回调内部再次排队
264
- 即可实现动画:
265
-
266
- ```ts
267
- const win = createWindow({ title: "anim", width: 320, height: 240 });
268
- let n = 0;
269
- const tick = (t: number) => {
270
- win.document.getElementById("label").textContent = String(n++);
271
- if (n < 120) win.requestAnimationFrame(tick); // 返回 id;cancelAnimationFrame(id) 可取消
272
- };
273
- win.on("ready", () => win.requestAnimationFrame(tick));
274
- win.loadHTML("<div id='label'>0</div>");
275
- run();
276
- ```
277
-
278
- ### 已实现的布局(M3)
279
-
280
- `runtime/ext_gui/layout.{h,cpp}` 把带样式的 DOM 转换为具有绝对(相对视口)
281
- 几何信息的 `LayoutBox` 树:
282
-
283
- - **块级流** —— 块级子元素垂直堆叠(暂不支持外边距折叠);`display: none`
284
- 不生成盒;`width: auto` 填满包含块,`height: auto` 包裹内容。盒模型
285
- (margin/padding/border)会被解析,包括相对包含块宽度的百分比。
286
- - **行内流** —— 连续的行内级子元素构成一个匿名行内格式化上下文,采用贪心、
287
- 基于单词的断行、`text-align` 与 `line-height`。行内元素获得其后代几何信息
288
- 的并集;`display: inline-block` 以 shrink-to-fit 宽度原子化布局。每个文本
289
- 片段会记住它覆盖的 run,以便绘制时对它整形。文本使用 HarfBuzz/FreeType
290
- 技术栈测量(见 *已实现的文本*)。
291
- - **Flexbox** —— 单行 `row`/`column`(以及 `-reverse` 变体),支持 `gap`、
292
- `flex-basis`/`flex-grow`/`flex-shrink`、`justify-content` 与 `align-items`
293
- (当交叉轴尺寸确定时包括 `stretch`)。flex 项的外边距参与其外框尺寸,因此
294
- 负的 `margin-top` 会把该项上移,而不是被忽略。
295
- - **定位** —— `position: relative` 会按 `top`/`right`/`bottom`/`left` 偏移该盒子
296
- 及其子树,但不改变它在流中占用的空间;`position: absolute`(以及针对视口解析
297
- 的 `fixed`)使其脱离文档流,并相对最近的已定位祖先的 padding box 定位(没有
298
- 这样的祖先时则相对视口)。`auto` 的 inset 在该轴上保留静态位置;宽度 `auto`
299
- 时使用 shrink-to-fit,除非 `left` 与 `right` 同时给出(此时填满两者之间的空间);
300
- 百分比相对包含块解析。目前还没有 `z-index` 层叠,绘制仍按文档顺序进行。
301
- 注意该 shrink-to-fit 规则的后果:一个 `width: auto` 且没有显式尺寸的绝对定位
302
- 盒子,其内部行内内容得到的**可用宽度为 0**(它的首选宽度只来自文本与
303
- 非绝对定位的子元素),因此其中的行内 run 会各自换到独立的一行。请为此类盒子
304
- 给出显式 `width`,或用 `left`/`width` 摆放其子元素(`examples/gui/pelican-bike`
305
- 的精灵 run 就是这样做的),而不要依赖其内部的行流。
306
-
307
- 尚未实现:外边距折叠、多行 flex 换行、`z-index` 层叠、`overflow` 裁剪与浮动。
308
-
309
- **Document**(`runtime/ext_gui/document.{h,cpp}`):持有 DOM 树,把 `<style>`
310
- 文本汇总为一份样式表,为某个视口计算样式与布局,并提供
311
- `querySelector`/`querySelectorAll`/`styleOf`/`boxOf`。
312
-
313
- ### 已实现的绘制(M4a/M4b)
314
-
315
- `runtime/ext_gui/paint.{h,cpp}` 按画家顺序遍历布局树,生成一个与后端无关的
316
- `DisplayList`,其中有两个并列的列表:**矩形**(背景与四条实心边框,带
317
- `border-radius`)与**文本 run**(每个携带其文本、颜色与已解析的 `FontSpec`)。
318
- 把它们分开可以让渲染器先绘制所有矩形,再在其上绘制所有文本,每个列表一次
319
- 绘制调用。`DisplayList::dump()` 为 `paintList()`/`paintCount()` 提供数据。
320
-
321
- `opacity` 只在两端生效:计算值为 `0` 的盒子(及其整棵子树)不绘制任何内容,
322
- 大于 `0` 的一律按完全不透明绘制。图形只携带其颜色自身的 alpha,因此没有部分
323
- 透明盒子的合成——CSS 解析器接受的 `opacity` 过渡因此也看不到效果。把隐藏态写成
324
- `opacity: 0` 来隐藏/显示一层,正是 `examples/gui/pelican-bike` 游戏结束遮罩的做法。
325
-
326
- `runtime/ext_gui/renderer.{h,cpp}` 把该列表转换为每个窗口两个批处理顶点缓冲
327
- (一个用于图形,一个用于字形四边形),通过两条 SDL_GPU 图形管线绘制:
328
-
329
- - 共享管线会依据设备支持的着色器格式延迟创建。SDL_GPU 的三个后端各自只接受一种
330
- 二进制格式,且都不会在运行时编译 GLSL/HLSL,因此引擎三种都随包提供,由
331
- `selectShader`(`renderer_shaders.h`)挑选设备可用的那一种:Metal 用 **MSL**
332
- (SDL 从内嵌源码编译)、Vulkan 用 **SPIR-V**
333
- (`runtime/ext_gui/spirv/*.{vert,frag}`,由 `glslc` 编译)、Direct3D 12 用
334
- **DXIL**(`shaders.hlsl`,由 `dxc` 编译)。
335
- `scripts/build-gui-shaders.mjs` 重新生成内嵌二进制块,产物已提交,因此构建引擎
336
- 无需额外工具。各格式的描述符绑定不同——SPIR-V 遵循 `SDL_gpu_vulkan.c` 的布局
337
- (uniform 在 `set=1, binding=0`,采样器在 `set=2, binding=0`,纹理在
338
- `set=2, binding=1`)——且 glslc 会把所有入口点命名为 `main`,而 MSL/DXIL 保留
339
- 各自的描述性名称。
340
- 需注意:SDL 3.2.10 的 Direct3D 12 后端无法创建着色器中声明了 uniform buffer 的
341
- 图形管线(返回 `E_INVALIDARG`);引擎正是用这种方式传视口,因此 DXIL 这条路已
342
- 构建但在上游修复前不可用。
343
- - 图形片元着色器中的**圆角矩形距离场**提供抗锯齿填充;顶点携带
344
- `position`、`local`、`half extents`、`radius` 与颜色,一个视口尺寸的
345
- push constant 负责投影。启用 alpha 混合。
346
- - 文本管线采样一张**单一共享的灰度字形图集**(`R8_UNORM`,2048²,
347
- shelf 打包,LINEAR 过滤),把每个字形绘制为带纹理的四边形
348
- (`position`、`uv`、颜色),按覆盖率调制 alpha。
349
- - 只有在文档或视口变化(`geometry.dirty`)时才上传几何数据,因此稳态帧
350
- 只需绑定并绘制。
351
-
352
- 窗口背景(`setBackground`)是渲染通道的清除色。
353
-
354
- M4 仍需完成:渐变。
355
-
356
- ### 已实现的输入(M5)
357
-
358
- `LayoutTree::hitTest` 返回包含某点的最深盒(先探测靠后的兄弟,使最上层元素
359
- 胜出),`xt_dom_describe` 把元素转换为事件负载携带的 `div#id.class` 描述符。
360
-
361
- - SDL 指针/滚轮/键盘事件会被路由到所属窗口,进行命中测试,并投递给用 `on`
362
- 注册的处理器。`mousedown` 还会移动焦点。
363
- - `:hover` 匹配悬停元素**及其祖先**(因此悬停子元素会点亮其父元素);
364
- `:focus` 匹配聚焦元素。当任一改变时,`XtDocument::setHover`/`setFocus`
365
- 重新计算样式与布局,并把窗口几何标记为脏,因此改动会在下一帧绘制。
366
- - `win.hitTest(x, y)` 与 `win.sendEvent(type, options)` 暴露命中测试与合成
367
- 输入,使整条路径可无头测试(e2e 套件在没有真实鼠标的情况下驱动点击、滚轮
368
- 与按键)。
369
-
370
- 完整输入仍需完成:文本选择、拖拽、IME 与剪贴板。
371
-
372
- ### 已实现的图像(M6a)
373
-
374
- `runtime/ext_gui/image.{h,cpp}` 封装 vendored **stb_image** 头文件,把
375
- PNG/JPEG/BMP/GIF/TGA 解码为 RGBA8,并带有解码缓存与仅头文件的尺寸缓存
376
- (`stbi_info`)。路径接受 `file://` 前缀与百分号编码。
377
-
378
- - `<img>` 是一个替换元素(`display: inline-block`):设置了 CSS 尺寸时布局
379
- 采用该尺寸,否则采用固有像素尺寸;当只约束一个轴时保持宽高比。构建盒树时
380
- 从文件头(无需完整解码)读取固有尺寸。
381
- - 绘制时为每个 `<img>` 生成一个 `PaintImage`;渲染器把每个唯一 `src` 解码/
382
- 上传到 RGBA 纹理(按路径缓存)并绘制带纹理的四边形,把共享同一纹理的连续
383
- 四边形批处理。
384
-
385
- 图像仍需完成:CSS `background-image: url(...)`、`data:` URI、`object-fit`
386
- 与九宫格边框。
387
-
388
- ### 已实现的 transition(M6b)
389
-
390
- `XtDocument` 在*目标*计算样式(当前 `:hover`/`:focus` 状态下的层叠结果)与
391
- 用于布局和绘制的*显示*样式之间运行 CSS transition。当状态变化改变了某个参与
392
- transition 的属性时,会记录一个正在运行的 transition,并在每帧重新应用直到
393
- 结束;引擎用真实帧间隔推进时钟(`XtDocument::advance`),`win.advance(ms)`
394
- 让测试可以确定性地推进。
395
-
396
- - 支持的属性:`background-color`、`color`、`border-color`、
397
- `border-radius`;`transition: all` 覆盖它们。影响结构/布局的属性暂不做
398
- 动画(那会每帧重新布局)。
399
- - `transition` 简写与 `transition-property` / `-duration` / `-delay` /
400
- `-timing-function` 长写都会被解析;时间值接受 `s` 与 `ms`;缓动函数为
401
- `linear`、`ease`、`ease-in`、`ease-out` 与 `ease-in-out`(`ease` 是
402
- smoothstep 近似)。
403
- - 在动画中途重定向会从当前插值启动一个新 transition,因此反转悬停会从它
404
- 当前所在位置平滑动画。
405
- - `computedStyle()` 报告显示(已插值)的值,因此 transition 在测试中可直接
406
- 观察。
407
-
408
- 动画仍需完成:`@keyframes` 动画与 `cubic-bezier(...)`。
409
-
410
- ### 已实现的文本技术栈(M4b)
411
-
412
- `runtime/ext_gui/text.{h,cpp}` 封装 **HarfBuzz**(整形)与 **FreeType**(度量
413
- 与最终光栅化)。两者都静态构建,并由 `scripts/build-gui.ts` 链接进 `gui.a`。
414
-
415
- - 字体从常见系统路径解析(macOS 上的 Helvetica/Arial、Linux 上的
416
- DejaVu/Liberation、Windows 上的 Segoe UI/Arial),可用 `XT_GUI_FONT`
417
- (以及 `XT_GUI_FONT_MONO`)覆盖,并按 `(字体类别, 字号)` 缓存。目前只使用
418
- 常规直立字面;字重/斜体选择是后续细化。
419
- - `xt_text_measure_width` 用 HarfBuzz 对 run 整形(因此字距调整与连字会生效),
420
- `xt_text_metrics` 返回 FreeType 的 ascent / descent / 正常行高。当找不到
421
- 字体文件时,模块回退到确定性的逐字节近似,因此布局仍可工作。
422
- - `xt_text_shape_run` 返回定位后的字形,`xt_text_rasterize` 渲染 8 位位图;
423
- 渲染器把它们打包进图集。在 HiDPI 显示器上,字形以
424
- `font_size * SDL_GetWindowPixelDensity` 光栅化,而四边形以逻辑像素定位,
425
- 因此文本保持清晰。
426
- - 布局使用这些真实度量进行文本宽度、断行与 `line-height: normal` 计算;
427
- `measureText`/`fontMetrics` 把它们暴露给测试。
428
-
429
- 多窗口由对象模型自然得出:`createWindow` 返回一个原生对象句柄;每个句柄
430
- 拥有自己的 `SDL_Window`/GPU 表面与自己的 DOM 树。`run()` 驱动一个共享主循环,
431
- 它每帧 tick 所有窗口,并在最后一个窗口关闭时退出。
432
-
433
- ## 事件循环集成
434
-
435
- 运行时改动(已落地):
436
-
437
- - `int xt_loop_poll(int timeout_ms)` —— 一次 reactor 迭代;`0` 非阻塞轮询,
438
- `< 0` 阻塞。
439
- - `void xt_loop_set_main(xt_main_loop_fn fn)` —— 宿主可以接管主循环。
440
- `xt_run_event_loop()` 会委托给它;生成的 `main` 不变。
441
- - `xt_loop_set_main(NULL)` 恢复默认的 `select(2)` 循环。
442
-
443
- GUI 引擎既可以通过 `xt_loop_set_main` 注册自己的循环,也可以暴露显式的
444
- `run()`;**M2 使用显式的 `run()`**,因此窗口是 TypeScript 程序可以控制的
445
- 普通原生调用。每次 tick 它都会:
446
-
447
- 1. 为每个窗口泵送 SDL 窗口/输入事件,
448
- 2. 当显示列表变化时构建/上传它,并渲染每个打开的窗口(清除通道 + 图形几何
449
- + 文本几何),
450
- 3. 调用 `xt_loop_poll(0)` 与 `xt_drain_microtasks()`,使 socket/定时器与
451
- `await` 续体持续取得进展,
452
- 4. 重复直到所有窗口关闭或调用 `quit()`。
453
-
454
- `xt_loop_set_main` 钩子仍保留给希望自行掌控循环的宿主。
455
-
456
- 这使网络 I/O、定时器与 `await` 在 GUI 程序内部继续工作。
457
-
458
- ## 原生 ↔ TS 桥接
459
-
460
- - **TS → 引擎**:直接的 `xt_gui_*` 调用 / 窗口方法。
461
- - **引擎 → TS**:`xt_call_with_this(fn, thisValue, argc, argv)`,其中函数值
462
- 从 TS 捕获(例如用 `win.on(...)` 注册的事件处理器)。
463
- - **未来的页面→原生 RPC**:在没有页面 JS 时不需要;原生 TS 就是控制器。
464
- 如果日后加入声明式层,它将使用相同的 `xt_call_*` 入口。
465
-
466
- ## 打包与构建
467
-
468
- - 源码位于 `runtime/ext_gui/`(C/C++),外加 `vendor/` 下的 vendored 库
469
- (被 gitignore;按需获取)。
470
- - `npm run gui`(`scripts/build-gui.ts`)获取固定版本的 SDL3(`SDL3_TAG`,
471
- 默认 `release-3.2.10`),构建静态 SDL3,获取并构建静态 FreeType
472
- (`FREETYPE_VERSION`)与 HarfBuzz(`HARFBUZZ_VERSION`),编译引擎,并把
473
- 所有内容合并到 `runtime/lib/<os>-<arch>/gui.a`(Windows 上 MSVC ABI 则为
474
- `gui.lib`),与 `core.a` 采用相同约定,静态打包三者。合并使用 macOS 上的
475
- `libtool` 与其他平台上的 `ar -M`(GNU/LLVM);CMake 归档会在构建根目录或
476
- `Release/` 下按任一生成器风格被发现。
477
- - `src/extensions/gui/index.ts` 通过 `nativeObjects()` 暴露该归档(经由
478
- `findRuntimeLibrary`,因此 `.a`/`.lib` 都可用),并通过 `linkerFlags()`
479
- 暴露操作系统框架。
480
- - CI 在组装发布归档之前构建 `gui.a`,因此它会随 `runtime/lib/<slug>/` 一起
481
- 发布(发布 tarball 会复制整个 `runtime/` 树)。归档在 Linux(Xvfb 下,
482
- 使用 lavapipe 软件 Vulkan 驱动)与 macOS 上构建并运行示例;Windows 为
483
- 试验性。
484
- - 现有的缓存指纹已经会对 `nativeObjects()` 的内容做哈希,因此重建 `gui.a`
485
- 会自动使缓存二进制失效。
486
-
487
- ### 运行示例
488
-
489
- ```sh
490
- npm run runtime # runtime 改动后重建 core.a
491
- npm run gui # 构建 runtime/lib/<os>-<arch>/gui.a(首次会获取 SDL3)
492
- xbintsc run examples/gui/hello.ts --ext gui
493
- ```
494
-
495
- 设置 `XT_GUI_AUTOCLOSE_MS=<n>` 可在 `n` 毫秒后关闭所有窗口,e2e 测试
496
- (`tests/e2e/gui-*.test.ts`)用它来无头运行。
497
-
498
- 这里有两个示例:`hello.ts`(HTML/CSS + 响应式计数器)与
499
- [`pelican-bike/`](../examples/gui/pelican-bike)(一个可玩的 2D 游戏,使用分层
500
- 定位、CSS 像素精灵、`requestAnimationFrame` 与键盘输入)——操作方式与它输出的
501
- 几何报告见该目录的 README。
502
-
503
- 这些测试套件在 Windows 上默认跳过(该平台会构建归档,但 CI 未验证运行)。
504
- 如果本机有 `gui.lib` 与可用的 GPU,可用 `xbintsc_GUI_TESTS=1` 选择启用:
505
-
506
- ```bat
507
- set xbintsc_GUI_TESTS=1
508
- npx vitest run tests/e2e/gui-layout.test.ts tests/e2e/gui-example.test.ts
509
- ```
510
-
511
- 在无头 Linux 机器上,安装 SDL3 构建头文件,并在 Xvfb 与软件 Vulkan 驱动下
512
- 运行:
513
-
514
- ```sh
515
- sudo apt-get install -y clang cmake libx11-dev libxext-dev libxrandr-dev \
516
- libxcursor-dev libxi-dev libxinerama-dev libxfixes-dev libxkbcommon-dev \
517
- libwayland-dev wayland-protocols libdecor-0-dev libasound2-dev libpulse-dev \
518
- libdbus-1-dev libudev-dev libdrm-dev libgbm-dev libgl1-mesa-dev \
519
- libegl1-mesa-dev libvulkan-dev mesa-vulkan-drivers xvfb
520
- npm run runtime && npm run gui
521
- xvfb-run -a --server-args="-screen 0 1280x720x24" \
522
- npx tsx src/cli/main.ts run examples/gui/hello.ts --ext gui
523
- ```
524
-
525
- 这些就是 CI 的 `compile-examples` job 安装的包。默认使用 X11 后端;Wayland
526
- 也已启用,但尚未实际演练。
527
-
528
97
  ## 里程碑
529
98
 
530
99
  1. **M1 — 基础** ✅
@@ -554,8 +123,9 @@ xvfb-run -a --server-args="-screen 0 1280x720x24" \
554
123
  (`css.*`、`style.*`、`document.*`、`gui.cpp`),以及
555
124
  `hitTest`/`sendEvent` 测试钩子。
556
125
  6. **M6 — 图像,然后是 CSS transition/动画**
557
- - **M6a — 图像** ✅ stb_image 解码、`<img>` 替换元素布局、按文件 GPU 纹理
558
- 与带纹理四边形(`image.*`、`paint.*`、`renderer.*`)。
126
+ - **M6a — 图像** ✅ stb_image 解码(位图)与 NanoSVG 光栅化(SVG 文件,按
127
+ 绘制像素尺寸)、`<img>` 替换元素布局、按文件 GPU 纹理与带纹理四边形
128
+ (`image.*`、`svg.*`、`paint.*`、`renderer.*`)。
559
129
  - **M6b — transition/动画** ✅ `transition` 简写 + 长写、对
560
130
  `background-color`/`color`/`border-color`/`border-radius` 的动画、重定向
561
131
  与 `win.advance(ms)`。`@keyframes` 仍待完成。
@@ -569,7 +139,7 @@ xvfb-run -a --server-args="-screen 0 1280x720x24" \
569
139
  - `vendor/`(SDL3/FreeType/HarfBuzz,缓慢的部分)按 OS/arch 缓存,以
570
140
  `scripts/build-gui.ts` 为键。
571
141
  - Windows(构建 + 运行)保持试验性,直到验证 MSVC 兼容的 `gui.lib` 与
572
- D3D12/DXIL 着色器路径;失败只记录日志而不标注运行(见 *待决问题*)。
142
+ D3D12/DXIL 着色器路径;失败只记录日志而不标注运行(见[待决问题](./gui/known-issues.md))。
573
143
  8. **M8 — 交互式 DOM** ✅
574
144
  - 具有稳定标识的元素/文档句柄、通过运行时访问器读写属性、遍历/特性/查询、
575
145
  带有延迟重算样式/重新布局的变更
@@ -611,7 +181,22 @@ xvfb-run -a --server-args="-screen 0 1280x720x24" \
611
181
  - 收集器触发阈值改为跟随存活集而非只升不降,避免泄漏或存活集尖峰演变成
612
182
  内存失控(`runtime/xt_alloc.c`)。`XT_GC_TRACE=1` 会报告每次回收。
613
183
  - `win.stats()` 暴露引擎计数器;e2e 覆盖见
614
- `tests/e2e/gui-retention.test.ts`(见"内存与 GC")。
184
+ `tests/e2e/gui-retention.test.ts`(见[内存与 GC](./gui/runtime.md))。
185
+ 13. **M13 — 视频与音频** ✅
186
+ - `<video>` / `<audio>` 通过静态 vendored 的最小 FFmpeg 解码
187
+ (`media.*`、`gui_media.cpp`),帧沿用现有图像管线上传到 GPU 纹理,
188
+ 音频在主线程推送进 SDL 流,并在元素句柄上暴露 DOM 媒体 API 与
189
+ `loadedmetadata`/`play`/`pause`/`ended`/`timeupdate`/`error` 事件
190
+ (`dom_api_media.cpp`、`dom_api_proto.cpp`)。由 TS + CSS 控制;没有
191
+ 内置控件。示例见 `examples/gui/media.*`,e2e 见
192
+ `tests/e2e/gui-media.test.ts`。
193
+ 14. **M14 — `<canvas>` 2D** ✅
194
+ - `<canvas>` 是一个替换元素(`xtgui::Canvas2D`、`layout.*`),
195
+ `getContext("2d")` 句柄与绘制 API 位于 `dom_api_canvas.cpp`,自研的抗锯齿
196
+ 软件光栅器位于 `canvas_raster.cpp`,按窗口的生命周期位于 `gui_canvas.cpp`。
197
+ 位图经图像路径上传为 GPU 纹理(`renderer_image.cpp`),显示列表携带
198
+ `canvas_id`(`paint.*`)。参见 [Canvas](./gui/canvas.md);e2e 见
199
+ `tests/e2e/gui-canvas.test.ts`。
615
200
 
616
201
  ## 进度日志
617
202
 
@@ -635,7 +220,8 @@ xvfb-run -a --server-args="-screen 0 1280x720x24" \
635
220
  `hitTest`/`sendEvent` 测试钩子、e2e 覆盖。
636
221
  - **M6a** ✅ 图像解码(`image.*`、vendored stb_image)、布局中的 `<img>`
637
222
  固有尺寸、显示列表中的 `PaintImage`、按文件 RGBA 纹理与 `renderer.*` 中的
638
- 图像管线、e2e 覆盖。
223
+ 图像管线、e2e 覆盖。SVG 文件同样可解码(`svg.*`、vendored NanoSVG):矢量源
224
+ 按绘制设备像素尺寸光栅化并按该尺寸缓存,因此缩放与 HiDPI 保持清晰。
639
225
  - **M6b** ✅ CSS transition(在 `style.*` 中解析、在 `document.*` 中维护动画
640
226
  时钟与 transition 状态、`advance`/`advance(ms)` 钩子、e2e 覆盖)。
641
227
  - **M7** ✅ 在 CI 中于 Linux 与 macOS 上构建 `gui.a`(必需),并在 Linux 的
@@ -671,74 +257,20 @@ xvfb-run -a --server-args="-screen 0 1280x720x24" \
671
257
  保留其文档化的 DOM 语义;句柄槽位会带"每槽代次"地复用,使过期句柄保持惰性
672
258
  失效;收集器的触发阈值改为跟随存活集,而不再只升不降。`win.stats()` 暴露
673
259
  引擎计数器,`tests/e2e/gui-retention.test.ts` 对其做断言;每帧重写精灵的游戏
674
- 循环现在内存恒定(见"内存与 GC")。
675
-
676
- ## 内存与 GC
677
-
678
- GUI 程序的大部分内存不在运行时堆里:DOM 树、分离节点池、元素句柄表、布局盒树
679
- 与显示列表都是 C++,标记-清扫收集器看不到它们。`stats()`(见"诊断")报告其中
680
- 可能增长的部分:
681
-
682
- | 计数器 | 稳定状态下的期望值 |
683
- | --- | --- |
684
- | `treeNodes` | 常量——即编写的文档 |
685
- | `detachedRoots`、`detachedNodes` | 0;若脚本自己移除了节点并仍持有,则为常量 |
686
- | `handles`、`handleSlots` | 常量——每个脚本仍引用的元素一个 |
687
- | `listeners` | 常量——每个注册了监听器的元素一个 |
688
- | `heapBytes`、`heapAllocations` | 在收集器阈值附近呈锯齿波动 |
689
-
690
- 其余都是每帧用完即释放的临时数据(盒、显示列表、顶点暂存)。若某个计数器随帧数
691
- 持续上升,那就是最该先查的泄漏特征;`tests/e2e/gui-retention.test.ts` 断言常见
692
- 变更模式不会改变这些计数。
693
-
694
- 读这些数字时有两处引擎细节需要注意:
695
-
696
- - **谁会释放节点。** `innerHTML`/`inner`/`textContent`/`replaceChild` 会释放它们
697
- 丢弃的子树;`removeChild`/`remove` 会保留(见"节点生命周期")。因此用
698
- `innerHTML` 每帧重建精灵的游戏内存恒定。
699
- - **什么仍会累积。** 用 `createElement` 创建却从未插入的节点会留在池中:收集器
700
- 没有弱引用,引擎无法知道脚本已丢弃其句柄。脚本移除后又不再引用的节点同理。
701
- 两者都以*存活句柄*数量为界,但每帧创建元素却不插入的程序会让池随帧数增长。
702
-
703
- `heapBytes` 背后的收集器是 `runtime/xt_alloc.c` 中的非移动标记-清扫。它的触发
704
- 阈值跟随存活集:每次回收后阈值变为存活堆的 2 倍(下限 1 MiB),因此存活集小
705
- 就意味着回收频繁而轻量、常驻内存有界。用 `XT_GC_THRESHOLD=<bytes>` 可以固定
706
- 阈值;`XT_GC_TRACE=1` 会为每次回收向 stderr 打印一行(回收前后的堆与存活分配数、
707
- 下一次触发阈值,以及按对象类型统计的存活集)——DOM 泄漏正是这样被定位到节点池的。
708
-
709
- ## 已知问题
710
-
711
- - **自动关闭时的偶发崩溃。** 设置 `XT_GUI_AUTOCLOSE_MS` 结束运行时,约五次中有两次会以原生
712
- 访问违例(`0xC0000005`)退出,其余运行与正常退出无异。该问题只在**动画帧循环运行中**复现
713
- (静态文档可以正常关闭),且**不是示例游戏自身**的代码:没有任何异常信息,崩溃多发生在
714
- 启动后一秒内、与截止时间无关,而提高 `XT_GC_THRESHOLD` 可以掩盖它,因此看起来是
715
- GC/析构竞态。交互式运行(不设自动关闭)可稳定运行数分钟。此前的一个已知成因是引擎在标记-清扫
716
- 收集器看不见的 C++ 容器里缓存 `xt_value`(AOT `<script>` 注册表、元素句柄、元素监听器、
717
- 排队/正在运行的 `requestAnimationFrame` 回调),现在它们由 GC 根提供者标记
718
- (`xt_gui_script_gc_scan` 与 `xt_gui_gc_scan_roots`),该类 use-after-free 已消除。
719
- 在 M12 的节点生命周期改动之后它已完全无法复现:对
720
- `examples/gui/pelican-bike` 做了 44 次自动关闭运行(截止 800 ms–4 s,分别使用默认、
721
- 1 MiB 与 64 MiB 的收集阈值,而旧记录称约五次两次崩溃),全部正常退出,指向与内存问题
722
- 相同的根因——分离节点池与句柄表随帧数增长,同时收集器阈值只升不降。此处仍保留该条目,
723
- 因为该归档按平台构建且故障与时机相关。`tests/e2e/gui-example.test.ts`
724
- 因此把该退出码视为可接受结果,行为断言放在不依赖 GPU 的
725
- `tests/e2e/gui-example-browser.test.ts` 中。
726
- - **`display: none -> flex` 的重排。** 通过类名切换让隐藏元素显示在 flex 容器上时,也曾观察
727
- 到同样的崩溃;`examples/gui/pelican-bike` 改用 `opacity` 显示游戏结束遮罩。
728
- - **SVG:** 引擎不支持也不计划支持 SVG 元素。HTML 解析器只认识元素与属性,布局只认识盒子,
729
- 绘制只认识矩形/文本/图像;`<svg>` 子树需要另一套几何与绘制路径。SVG *文件* 也只能走图像
730
- 加载器(`<img src="logo.svg">`),而这需要 SVG 光栅化器(`stb_image` 不解码 SVG)。因此
731
- 矢量美术要么以位图(PNG)形式提供,要么像 `examples/gui/pelican-bike` 那样用盒子绘制。
732
-
733
- ## 待决问题
260
+ 循环现在内存恒定(见[内存与 GC](./gui/runtime.md))。
261
+ - **M13** ✅ `<video>` / `<audio>` 支持。一个最小化的静态 FFmpeg 被 vendored
262
+ 并合并进 `gui.a`(`scripts/build-gui.ts`),`media.*` 在主线程把视频解码为
263
+ RGBA 帧、把音频解码为 48 kHz 立体声 PCM,`gui_media.cpp` 持有播放器并在每帧
264
+ 推进媒体时钟与事件,`renderer_image.cpp` 把每个新视频帧沿图像纹理路径上传,
265
+ `dom_api_media.cpp` 在元素句柄上暴露 `play`/`pause`/`load` 与媒体属性。
266
+ `examples/gui/media.*` 演示 TS + CSS 控件的模式,
267
+ `tests/e2e/gui-media.test.ts` 对其做断言。
268
+ - **M14** ✅ `<canvas>` 支持。新的替换元素(`layout.*`)从 `width`/`height`
269
+ 属性获得固有尺寸(默认 `300x150`),`xtgui::Canvas2D`(`canvas.{h,cpp}`)
270
+ 持有 RGBA8 位图与绘制状态,`canvas_raster.cpp` 是自研的抗锯齿软件光栅器
271
+ (4 条子扫描线 + 水平解析覆盖率、非零环绕),`getContext("2d")` 句柄及其 API
272
+ 在 `dom_api_canvas.cpp` 绑定,`gui_canvas.cpp` 持有按窗口的生命周期。位图经
273
+ 图像纹理路径上传(`renderer_image.cpp`),显示列表携带 `canvas_id`
274
+ (`paint.*`)。参见 [Canvas](./gui/canvas.md);e2e 见
275
+ `tests/e2e/gui-canvas.test.ts`。
734
276
 
735
- - Linux 首发是只发布 X11、Wayland,还是两者都发布。(决定:先 X11,
736
- Wayland 随后。)
737
- - Windows:`scripts/build-gui.ts` 会生成 MSVC 兼容的 `gui.lib`
738
- (COFF 对象 + `ar -M`/`llvm-ar`),但尚未在 CI 中验证,因此 Windows 步骤
739
- 为试验性(job 保持绿色),`package` 会跳过它。它还需要一个带 D3D12/DXIL
740
- 后端的 SDL3 构建(DXIL 需要 `dxc`)。
741
- - **D3D12 着色器:** SPIR-V(Vulkan)与 DXIL(Direct3D 12)二进制块均已构建并
742
- 内嵌,非 Metal 路径不再跳过几何绘制。但在 **SDL 3.2.10** 上 DXIL 仍不可用:
743
- 其 D3D12 后端会拒绝任何着色器中声明了 uniform buffer 的图形管线,而引擎正是
744
- 用这种方式传视口。SDL 升级后需重新确认。