xbintsc 0.3.68 → 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 (100) 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/doc/zh-CN/post/xbintsc-build-pelican.md +2 -2
  52. package/llms.txt +15 -2
  53. package/package.json +1 -1
  54. package/runtime/ext_gui/canvas.cpp +580 -0
  55. package/runtime/ext_gui/canvas.h +170 -0
  56. package/runtime/ext_gui/canvas_internal.h +59 -0
  57. package/runtime/ext_gui/canvas_raster.cpp +254 -0
  58. package/runtime/ext_gui/document.cpp +24 -1
  59. package/runtime/ext_gui/document.h +11 -0
  60. package/runtime/ext_gui/dom_api.cpp +3 -0
  61. package/runtime/ext_gui/dom_api_canvas.cpp +548 -0
  62. package/runtime/ext_gui/dom_api_internal.h +73 -0
  63. package/runtime/ext_gui/dom_api_media.cpp +274 -0
  64. package/runtime/ext_gui/dom_api_proto.cpp +80 -0
  65. package/runtime/ext_gui/gui.cpp +6 -0
  66. package/runtime/ext_gui/gui_canvas.cpp +95 -0
  67. package/runtime/ext_gui/gui_engine.h +49 -0
  68. package/runtime/ext_gui/gui_events.cpp +21 -5
  69. package/runtime/ext_gui/gui_media.cpp +193 -0
  70. package/runtime/ext_gui/image.cpp +31 -12
  71. package/runtime/ext_gui/image.h +20 -7
  72. package/runtime/ext_gui/layout.cpp +52 -5
  73. package/runtime/ext_gui/layout.h +33 -1
  74. package/runtime/ext_gui/layout_flex.cpp +263 -99
  75. package/runtime/ext_gui/layout_flow.cpp +85 -9
  76. package/runtime/ext_gui/layout_flow_internal.h +84 -5
  77. package/runtime/ext_gui/layout_inline.cpp +54 -37
  78. package/runtime/ext_gui/media.cpp +514 -0
  79. package/runtime/ext_gui/media.h +104 -0
  80. package/runtime/ext_gui/nanosvg.h +3278 -0
  81. package/runtime/ext_gui/nanosvgrast.h +1472 -0
  82. package/runtime/ext_gui/paint.cpp +123 -38
  83. package/runtime/ext_gui/paint.h +36 -6
  84. package/runtime/ext_gui/renderer.cpp +113 -10
  85. package/runtime/ext_gui/renderer.h +49 -9
  86. package/runtime/ext_gui/renderer_image.cpp +203 -77
  87. package/runtime/ext_gui/renderer_internal.h +6 -8
  88. package/runtime/ext_gui/renderer_text.cpp +11 -26
  89. package/runtime/ext_gui/style.h +11 -0
  90. package/runtime/ext_gui/style_print.cpp +11 -0
  91. package/runtime/ext_gui/style_ua.cpp +7 -0
  92. package/runtime/ext_gui/style_values.cpp +40 -1
  93. package/runtime/ext_gui/svg.cpp +182 -0
  94. package/runtime/ext_gui/svg.h +46 -0
  95. package/runtime/ext_gui/text.cpp +156 -25
  96. package/runtime/ext_gui/text.h +11 -2
  97. package/runtime/ext_gui/window.cpp +4 -1
  98. package/scripts/build-gui.ts +148 -6
  99. package/scripts/check-file-length.ts +5 -1
  100. package/scripts/install-gui-deps.sh +28 -0
@@ -366,6 +366,7 @@ Location: `tests/` (`lexer` / `parser` / `binder` / `codegen` / `driver` / `exte
366
366
  | Value model | 64-bit NaN-boxing, uniform function ABI (including `this`), closure environments, object prototype chains |
367
367
  | Runtime | Strings / objects / arrays / typed arrays / closures / arithmetic / comparison / catchable exceptions / Promise / collections / symbols / generators / `fetch` / `console` |
368
368
  | Extensions | Extension registry, `core` (print), `node` (fs / path / os / process / buffer / stream / net / dgram / http imported by specifier) |
369
+ | GUI (`gui` extension) | Self-hosted HTML/CSS renderer window (SDL3 + SDL_GPU): DOM/CSS/layout, `<img>`/SVG, `<video>`/`<audio>`, `<canvas>` 2D, events, transitions |
369
370
  | Toolchain | clang compiles IR/C, linking, incremental cache |
370
371
  | Project config | `xbintsc.config.json` (`entry` / `outDir` / `optimize` / `extensions` / `app`), discovery + CLI precedence |
371
372
  | Application icon | compile-time embed (PNG/ICO/ICNS), PE resource on Windows, `.app` bundle on macOS, runtime window/Dock icon via `gui` |
@@ -0,0 +1,46 @@
1
+ # 动画:帧与 transition
2
+
3
+ > 语言 / Language:[English](../../gui/animation.md) | **简体中文**
4
+
5
+ > 属于 [GUI 扩展](../gui.md) 的一部分;引擎总览、锁定决策与路线图在那里。
6
+
7
+ ## 动画帧(M10)
8
+
9
+ `requestAnimationFrame` 在下一帧运行一次回调;回调接收帧时间戳(毫秒),
10
+ 并且可以修改 DOM,DOM 会在同一帧内重新计算样式并重绘。在回调内部再次排队
11
+ 即可实现动画:
12
+
13
+ ```ts
14
+ const win = createWindow({ title: "anim", width: 320, height: 240 });
15
+ let n = 0;
16
+ const tick = (t: number) => {
17
+ win.document.getElementById("label").textContent = String(n++);
18
+ if (n < 120) win.requestAnimationFrame(tick); // 返回 id;cancelAnimationFrame(id) 可取消
19
+ };
20
+ win.on("ready", () => win.requestAnimationFrame(tick));
21
+ win.loadHTML("<div id='label'>0</div>");
22
+ run();
23
+ ```
24
+
25
+ ## CSS transition(M6b)
26
+
27
+ `XtDocument` 在*目标*计算样式(当前 `:hover`/`:focus` 状态下的层叠结果)与
28
+ 用于布局和绘制的*显示*样式之间运行 CSS transition。当状态变化改变了某个参与
29
+ transition 的属性时,会记录一个正在运行的 transition,并在每帧重新应用直到
30
+ 结束;引擎用真实帧间隔推进时钟(`XtDocument::advance`),`win.advance(ms)`
31
+ 让测试可以确定性地推进。
32
+
33
+ - 支持的属性:`background-color`、`color`、`border-color`、
34
+ `border-radius`;`transition: all` 覆盖它们。影响结构/布局的属性暂不做
35
+ 动画(那会每帧重新布局)。
36
+ - `transition` 简写与 `transition-property` / `-duration` / `-delay` /
37
+ `-timing-function` 长写都会被解析;时间值接受 `s` 与 `ms`;缓动函数为
38
+ `linear`、`ease`、`ease-in`、`ease-out` 与 `ease-in-out`(`ease` 是
39
+ smoothstep 近似)。
40
+ - 在动画中途重定向会从当前插值启动一个新 transition,因此反转悬停会从它
41
+ 当前所在位置平滑动画。
42
+ - `computedStyle()` 报告显示(已插值)的值,因此 transition 在测试中可直接
43
+ 观察。
44
+
45
+ 动画仍需完成:`@keyframes` 动画与 `cubic-bezier(...)`。
46
+
@@ -0,0 +1,88 @@
1
+ # GUI API
2
+
3
+ > 语言 / Language:[English](../../gui/api.md) | **简体中文**
4
+
5
+ > 属于 [GUI 扩展](../gui.md) 的一部分;引擎总览、锁定决策与路线图在那里。
6
+
7
+ ## 面向 TS 的 API
8
+
9
+ ```ts
10
+ import { createWindow, run, quit } from "gui";
11
+
12
+ const win = createWindow({ title: "Demo", width: 900, height: 600 });
13
+ win.setBackground("#14161c");
14
+ win.loadHTML(INDEX_HTML); // 解析 HTML/CSS 并计算样式
15
+ win.on("ready", () => console.log("first frame presented"));
16
+ win.on("close", () => console.log("window closed"));
17
+
18
+ run(); // 驱动主循环,直到所有窗口关闭
19
+ ```
20
+
21
+ 窗口句柄上已实现的方法:`setTitle` / `setSize` / `loadHTML` /
22
+ `getHTML` / `setBackground` / `close` / `isOpen` / `on` / `off`,以及
23
+ `driver()`——它报告当前使用的 SDL_GPU 后端(`"vulkan"`、`"direct3d12"`、
24
+ `"metal"` 等,由 SDL 挑选设备支持的最佳后端)。同时也提供 Web 写法:
25
+ `addEventListener` / `removeEventListener` 是 `on` / `off` 的别名(其
26
+ `options` 参数会被接受并忽略),`closed` getter 对应 `isOpen()`,因此按浏览器
27
+ API 编写的文档可以原样运行。
28
+
29
+ 在 Windows 上,GUI 程序会被链接为 **Windows 子系统**可执行文件,因此启动时不会闪出控制台
30
+ 窗口;这也意味着从资源管理器启动时 stdout/stderr 无处可去,调试时可在
31
+ `xbintsc.config.json` 里设置 `app.console: true` 保留控制台子系统。
32
+
33
+ 触发的事件:
34
+ `ready`(首个呈现帧之后)、`load`、`close`、`resize`,以及输入事件
35
+ `mousemove`、`mousedown`、`mouseup`、`click`、`wheel`、`keydown`、`keyup`。
36
+
37
+ 输入处理器接收单个负载对象(生命周期处理器不接收任何参数):
38
+
39
+ ```ts
40
+ win.on("click", (e) => console.log(e.x, e.y, e.button, e.target));
41
+ win.on("wheel", (e) => console.log(e.deltaX, e.deltaY));
42
+ win.on("keydown", (e) => console.log(e.key, e.code, e.ctrl, e.shift, e.alt, e.meta));
43
+ ```
44
+
45
+ | 字段 | 事件 | 含义 |
46
+ | --- | --- | --- |
47
+ | `x`、`y` / `clientX`、`clientY` | pointer、wheel | 相对于视口的逻辑像素 |
48
+ | `button` | pointer | `0` 左键、`1` 中键、`2` 右键(移动时为 `-1`) |
49
+ | `clicks` | pointer | 操作系统报告的点击次数 |
50
+ | `deltaX`、`deltaY` | wheel | 滚动量(`deltaY` 为正表示向下) |
51
+ | `key`、`code` | keyboard | 键名与物理扫描码名 |
52
+ | `repeat`、`ctrl`、`shift`、`alt`、`meta` | keyboard | 修饰键 |
53
+ | `target` | pointer、wheel | 该点下最深的元素,表示为类 CSS 描述符(`div#main.card`),可能缺省 |
54
+
55
+ ## 诊断
56
+
57
+ 为了让 HTML/CSS/绘制流水线在没有 GPU 的情况下也可测试,窗口句柄暴露只读钩子:
58
+
59
+ ```ts
60
+ win.computedStyle(selector, property) // e.g. ("#main", "width") -> "60%"
61
+ win.queryCount(selector) // 匹配元素的数量
62
+ win.getBoundingClientRect(selector) // { x, y, width, height }(边框盒)
63
+ win.documentTree() // 序列化的 DOM(调试用)
64
+ win.layoutTree() // 序列化的布局盒(调试用)
65
+ win.paintCount() // 显示列表中图形的数量
66
+ win.paintList() // 序列化的显示列表(调试用)
67
+ win.measureText(text, fontSize?, family?) // 整形后的推进宽度(像素)
68
+ win.fontMetrics(fontSize?, family?) // { ascent, descent, lineHeight, ready }
69
+ win.hitTest(x, y) // 最深的元素描述符,或 ""
70
+ win.sendEvent(type, options?) // 合成输入(测试用)
71
+ win.advance(ms) // 推进 CSS transition 时钟(测试用)
72
+ win.stats() // 引擎/运行时计数器(内存排查用)
73
+ ```
74
+
75
+ `win.stats()` 返回 `{ heapBytes, heapAllocations, treeNodes, detachedRoots,`
76
+ `detachedNodes, handles, handleSlots, listeners }`:运行时存活堆、DOM 树规模、
77
+ 分离节点池(detached pool)以及元素句柄表。稳定状态下它们都应保持常量——若
78
+ `detachedNodes` 或 `handles` 持续上升,说明程序移除节点的速度快于释放速度
79
+ (见[节点生命周期](./dom.md))。
80
+
81
+ 它们被 `tests/e2e/gui-*.test.ts` 用来断言解析、选择器匹配、特异性、继承、
82
+ `!important` 与布局几何。它们在之后仍可用于调试。
83
+
84
+ ## 多窗口
85
+
86
+ 多窗口由对象模型自然得出:`createWindow` 返回一个原生对象句柄;每个句柄
87
+ 拥有自己的 `SDL_Window`/GPU 表面与自己的 DOM 树。`run()` 驱动一个共享主循环,
88
+ 它每帧 tick 所有窗口,并在最后一个窗口关闭时退出。
@@ -0,0 +1,84 @@
1
+ # 打包与构建
2
+
3
+ > 语言 / Language:[English](../../gui/build.md) | **简体中文**
4
+
5
+ > 属于 [GUI 扩展](../gui.md) 的一部分;引擎总览、锁定决策与路线图在那里。
6
+
7
+ - 源码位于 `runtime/ext_gui/`(C/C++),外加 `vendor/` 下的 vendored 库
8
+ (被 gitignore;按需获取)。
9
+ - `npm run gui`(`scripts/build-gui.ts`)获取固定版本的 SDL3(`SDL3_TAG`,
10
+ 默认 `release-3.2.10`),构建静态 SDL3,获取并构建静态 FreeType
11
+ (`FREETYPE_VERSION`)与 HarfBuzz(`HARFBUZZ_VERSION`),编译引擎,并把
12
+ 所有内容合并到 `runtime/lib/<os>-<arch>/gui.a`(Windows 上 MSVC ABI 则为
13
+ `gui.lib`),与 `core.a` 采用相同约定,静态打包三者。它还会获取、配置并构建
14
+ 一个最小化的静态 **FFmpeg**(`FFMPEG_VERSION`,默认 `7.1`),只启用媒体元素
15
+ 所需的 demuxer/parser/decoder,并把那些归档合并进 `gui.a`,同时为引擎定义
16
+ `XT_GUI_WITH_FFMPEG`。合并使用 macOS 上的
17
+ `libtool` 与其他平台上的 `ar -M`(GNU/LLVM);CMake 归档会在构建根目录或
18
+ `Release/` 下按任一生成器风格被发现。
19
+ - `src/extensions/gui/index.ts` 通过 `nativeObjects()` 暴露该归档(经由
20
+ `findRuntimeLibrary`,因此 `.a`/`.lib` 都可用),并通过 `linkerFlags()`
21
+ 暴露操作系统框架。
22
+ - CI 在组装发布归档之前构建 `gui.a`,因此它会随 `runtime/lib/<slug>/` 一起
23
+ 发布(发布 tarball 会复制整个 `runtime/` 树)。归档在 Linux(Xvfb 下,
24
+ 使用 lavapipe 软件 Vulkan 驱动)与 macOS 上构建并运行示例;Windows 为
25
+ 试验性。
26
+ - 现有的缓存指纹已经会对 `nativeObjects()` 的内容做哈希,因此重建 `gui.a`
27
+ 会自动使缓存二进制失效。
28
+ - CMake 构建树位于其源码**旁边**(`vendor/sdl-build`、`vendor/freetype-build`、
29
+ `vendor/harfbuzz-build`),绝不放在源码内部:`CMakeCache.txt` 记录的是配置它
30
+ 的那台机器,而 SDL 的视频后端是 `option()`,因此先运行的那次配置会一直生效。
31
+ 所以一个同时携带 `build/` 的 `vendor/SDL` 缓存树,会把某台机器的
32
+ `SDL_X11`/`SDL_WAYLAND` 选择带进另一台机器的配置——即使头文件已安装,SDL 也会
33
+ 以 "SDL could not find X11 or Wayland development libraries" 中止。CI 缓存按
34
+ OS/arch 区分键值,且不包含构建树;`npm run gui` 还会在 Debian/Ubuntu 上提前
35
+ 检查 X11/Wayland 头文件,并按名称报告缺失的开发包(当头文件位于该检查未知的
36
+ 前缀下时,可设置 `xbintsc_SKIP_HEADER_CHECK=1`)。
37
+
38
+ ## 运行示例
39
+
40
+ ```sh
41
+ npm run runtime # runtime 改动后重建 core.a
42
+ npm run gui # 构建 runtime/lib/<os>-<arch>/gui.a(首次会获取 SDL3)
43
+ xbintsc run examples/gui/hello.ts --ext gui
44
+ ```
45
+
46
+ 设置 `XT_GUI_AUTOCLOSE_MS=<n>` 可在 `n` 毫秒后关闭所有窗口,e2e 测试
47
+ (`tests/e2e/gui-*.test.ts`)用它来无头运行。
48
+
49
+ 这里有两个示例:`hello.ts`(HTML/CSS + 响应式计数器)与
50
+ [`pelican-bike/`](../../../examples/gui/pelican-bike)(一个可玩的 2D 游戏,使用分层
51
+ 定位、CSS 像素精灵、`requestAnimationFrame` 与键盘输入)——操作方式与它输出的
52
+
53
+ `examples/gui/media.ts`(配合 `media.html`)演示 `<video>`/`<audio>`:它在
54
+ `<video>` 上叠一个 CSS 进度条,并用 `play()`/`pause()`/`currentTime`/
55
+ `volume` 驱动播放。媒体 `src` 相对于进程工作目录解析,因此从仓库根目录运行:
56
+ `xbintsc run examples/gui/media.ts --ext gui`。几何报告见该目录的 README。
57
+
58
+ 这些测试套件在 Windows 上默认跳过(该平台会构建归档,但 CI 未验证运行)。
59
+ 如果本机有 `gui.lib` 与可用的 GPU,可用 `xbintsc_GUI_TESTS=1` 选择启用:
60
+
61
+ ```bat
62
+ set xbintsc_GUI_TESTS=1
63
+ npx vitest run tests/e2e/gui-layout.test.ts tests/e2e/gui-example.test.ts
64
+ ```
65
+
66
+ 在无头 Linux 机器上,安装 SDL3 构建头文件,并在 Xvfb 与软件 Vulkan 驱动下
67
+ 运行:
68
+
69
+ ```sh
70
+ sudo sh scripts/install-gui-deps.sh # clang、cmake、X11/Wayland 头文件、Vulkan、Xvfb
71
+ npm run runtime && npm run gui
72
+ xvfb-run -a --server-args="-screen 0 1280x720x24" \
73
+ npx tsx src/cli/main.ts run examples/gui/hello.ts --ext gui
74
+ ```
75
+
76
+ 包清单集中在 `scripts/install-gui-deps.sh`(`apt-get install -y clang cmake
77
+ libx11-dev libxext-dev libxrandr-dev libxcursor-dev libxi-dev libxinerama-dev
78
+ libxfixes-dev libxkbcommon-dev libwayland-dev wayland-protocols libdecor-0-dev
79
+ libasound2-dev libpulse-dev libdbus-1-dev libudev-dev libdrm-dev libgbm-dev
80
+ libgl1-mesa-dev libegl1-mesa-dev libvulkan-dev mesa-vulkan-drivers xvfb`),
81
+ 这样每个构建 `gui.a` 的 Linux job 都安装同一套依赖:默认使用 X11 后端,
82
+ Wayland 也已启用但尚未实际演练。任何构建该归档的地方都要先装它——没有这些
83
+ 头文件,SDL3 找不到窗口后端。
84
+
@@ -0,0 +1,80 @@
1
+ # Canvas:`<canvas>` 与 2D 上下文(M14)
2
+
3
+ > 属于 [GUI 扩展](../gui.md);引擎总览、锁定决策与路线图见该文。
4
+
5
+ `<canvas>` 是一个**替换元素(replaced element)**,布局方式与 `<img>` 相同:其固有
6
+ 尺寸来自 `width`/`height` 属性(默认 `300x150`,即 CSS 默认画布尺寸),显式的 CSS
7
+ `width`/`height` 优先;当只约束一个轴时保持宽高比。绘制阶段为每个 canvas 产出一个
8
+ `PaintImage`,其中携带的是 `canvas_id` 而非 `src`/`media_id`;渲染器通过与位图、视频帧
9
+ 相同的图像路径(`renderer_image.cpp`)把位图上传为 RGBA 纹理,因此由 GPU 把它缩放到
10
+ CSS 盒中。
11
+
12
+ 引擎侧的对象是 `xtgui::Canvas2D`(`runtime/ext_gui/canvas.{h,cpp}`)——一块 RGBA8 位图
13
+ 加上 2D 绘制状态。光栅器位于 `canvas_raster.cpp`;DOM 绑定位于 `dom_api_canvas.cpp`;
14
+ 按窗口的生命周期位于 `gui_canvas.cpp`。
15
+
16
+ ## 获取上下文
17
+
18
+ ```ts
19
+ const canvas: any = document.getElementById("c");
20
+ const ctx = canvas.getContext("2d"); // 其他上下文类型返回 null
21
+ ```
22
+
23
+ `getContext("2d")` 返回一个上下文句柄,其方法与访问器位于共享原型上。该句柄会缓存在
24
+ 元素上,因此每次都返回**同一个**对象,绘制状态(变换、样式、路径)按 canvas 而非按调用
25
+ 保存。`ctx.canvas` 是所属元素的句柄。
26
+
27
+ ## 绘制模型
28
+
29
+ 光栅器刻意保持精简且为自研(不依赖 Skia/Cairo)。它按每条像素行 **4 条垂直子扫描线**
30
+ 进行抗锯齿,并做一次**水平解析覆盖率**计算;路径采用**非零环绕规则**填充(因此当弧线
31
+ 以相反方向环绕时能形成空洞)。轴对齐矩形(变换中 `b == 0 && c == 0`)走直接的覆盖率
32
+ 快速路径。混合为非预乘 source-over,并按 `globalAlpha` 缩放。
33
+
34
+ Canvas 像素**不参与垃圾回收**:canvas 由窗口拥有、以 DOM 节点为键(与
35
+ `<video>`/`<audio>` 解码表结构相同)。当节点被丢弃(`loadHTML`、窗口关闭/退出,或丢弃
36
+ 子树的变更操作)时随之释放,并释放对应的 GPU 纹理。
37
+
38
+ ## 支持的 API
39
+
40
+ 状态与变换:`save` / `restore`(会恢复变换、线宽、透明度、字体/文本设置**以及**
41
+ 填充/描边样式)、`translate`、`rotate`、`scale`、`transform(a,b,c,d,e,f)`、
42
+ `setTransform`、`resetTransform`。
43
+
44
+ 样式与设置:`fillStyle` / `strokeStyle`(层叠所用的颜色解析器能识别的 CSS 颜色)、
45
+ `lineWidth`、`globalAlpha`、`font`(CSS 字体简写,解析出字体族、字号与粗体/斜体——
46
+ 默认 `10px sans-serif`)、`textAlign`(`start`/`left`、`center`、`right`/`end`)与
47
+ `textBaseline`(`alphabetic`、`top`、`middle`、`bottom`、`hanging`、`ideographic`)。
48
+ 读取属性返回上次赋予的值;赋予无法解析的颜色会保留原值。
49
+
50
+ 路径:`beginPath`、`closePath`、`moveTo`、`lineTo`、`rect`、`arc`、
51
+ `quadraticCurveTo`、`bezierCurveTo`,然后 `fill`(多环、非零环绕)或 `stroke`。贝塞尔
52
+ 与二次曲线段会被展平为线段;弧线同样被展平,每条子路径按线段四边形描边,并在每个连接
53
+ 处补一个圆盘。
54
+
55
+ 矩形:`clearRect`、`fillRect`、`strokeRect`。
56
+
57
+ 文本:`measureText(text)`(返回 `{ width }`)、`fillText(text, x, y)` 与
58
+ `strokeText(text, x, y)`。文本经 HarfBuzz 排版、FreeType 光栅化(与 HTML 文本相同的
59
+ 技术栈),按变换的近似缩放绘制。**仅支持平移与均匀缩放**——旋转或斜切变换仍会把每个
60
+ 字形放到其变换后的基线点,但不会旋转字形位图。`strokeText` 目前用描边样式执行填充,
61
+ 而非生成轮廓。
62
+
63
+ 图像:`drawImage(source, …)` 支持 3 参数(`dx, dy`)、5 参数(`dx, dy, dw, dh`)与
64
+ 9 参数(`dx, dy, dw, dh, sx, sy, sw, sh`)形式。`source` 可以是 `<img>` 元素(经图像
65
+ 模块解码)或另一个 `<canvas>` 元素(取其当前位图)。采样为最近邻,因此把小图大幅放大
66
+ 会有块状感——适合图标与像素画,不适合照片管线。
67
+
68
+ 测试钩子:`getImageData(x, y, w, h)` 把区域复制为 `{ width, height, data }`,其中
69
+ `data` 是按行主序的 RGBA8 字节数组。越界像素为透明。
70
+
71
+ ## 尚未实现
72
+
73
+ 渐变与图案、阴影、`clip()`、`globalCompositeOperation`、线段连接/端点/虚线/斜接、
74
+ `setLineDash`、`createLinearGradient`/`createPattern`、`putImageData`、
75
+ `createImageData`、`isPointInPath`、`getTransform`、以 `DOMMatrix` 调用的
76
+ `setTransform`、可切换图像平滑的 `drawImage`、`toDataURL`/`toBlob`、把 `<canvas>` 用作
77
+ CSS `background-image`,以及离屏画布(`OffscreenCanvas`、
78
+ `transferControlToOffscreen`)。
79
+
80
+ 另见[媒体](./media.md):`drawImage` 与纹理上传所共享的图像/视频/音频解码栈。
@@ -0,0 +1,114 @@
1
+ # DOM、HTML、CSS 与 AOT 脚本
2
+
3
+ > 语言 / Language:[English](../../gui/dom.md) | **简体中文**
4
+
5
+ > 属于 [GUI 扩展](../gui.md) 的一部分;引擎总览、锁定决策与路线图在那里。
6
+ > 脚本接口另有文档:[gui-scripts.md](../gui-scripts.md)。
7
+
8
+ ## HTML/CSS 子集(M3a)
9
+
10
+ **HTML 解析器**(`runtime/ext_gui/dom.{h,cpp}`):标签/属性/文本,注释与
11
+ doctype 跳过,实体解码(命名 + 十进制/十六进制),空元素,原始文本元素
12
+ (`<style>`/`<script>`),以及常见的隐式闭合规则(`li`、`dt`/`dd`、`option`、
13
+ `p`、标题、表格单元格/行)。
14
+
15
+ **CSS 解析器**(`runtime/ext_gui/css.{h,cpp}`):注释与 at-rule 暂时跳过,
16
+ 支持多选择器的规则以及声明。选择器:类型、`.class`、`#id`、属性
17
+ (`=`、`~=`、`|=`、`^=`、`$=`、`*=`)、四种组合器(后代、子、相邻兄弟与
18
+ 通用兄弟)以及伪类 `:first-child`、`:last-child`、`:only-child`、`:empty`、
19
+ `:root`、`:not(...)`、`:nth-child(an+b)`、`:disabled`、`:checked`,再加上
20
+ 有状态的 `:hover` 与 `:focus`(见[输入](./input.md))。值:长度
21
+ (`px`、`%`、`em`、`rem`、`vw`、`vh`、`pt`、`pc`、`in`、`cm`、`mm`、`q`)、
22
+ 颜色(十六进制、`rgb()`/`rgba()`、一个命名子集)、数字、关键字与简写
23
+ (`margin`/`padding`/`border`/`flex`)。
24
+
25
+ **层叠与计算样式**(`runtime/ext_gui/style.{h,cpp}`):一份小的内置 UA
26
+ 样式表,作者规则按 `(!important, 特异性, 源码顺序)` 排序,然后是内联
27
+ `style=""`(特异性最高,但非 `!important` 的内联会输给 `!important`),
28
+ 再加上文本属性的 CSS 继承。相对长度保持未解析状态直到布局阶段,`font-size`
29
+ 除外(相对*父级*字号解析)以及 `line-height`。
30
+
31
+ ## 交互式 DOM(M8)
32
+
33
+ `win.document` 返回文档句柄;元素句柄具有稳定标识,并提供可读写的属性、
34
+ 特性(attribute)、遍历与几何信息:
35
+
36
+ ```ts
37
+ const doc = win.document;
38
+ const box = doc.querySelector("#box");
39
+ const inner = doc.querySelector("#inner");
40
+ inner.textContent = "hi"; // 写入
41
+ inner.classList.add("hot");
42
+ inner.style.setProperty("color", "#0f0");
43
+ inner.setAttribute("data-role", "lead");
44
+ console.log(inner.id, inner.tagName, doc.querySelector("#box") === box);
45
+
46
+ const created = doc.createElement("div");
47
+ created.textContent = "added";
48
+ box.appendChild(created);
49
+ box.removeChild(created);
50
+
51
+ inner.addEventListener("click", (e) => console.log(e.target.id, e.currentTarget.id));
52
+ inner.click(); // 在该元素处合成一次点击
53
+ console.log(box.offsetWidth, box.offsetHeight); // 取整后的边框盒
54
+ console.log(box.contains(inner)); // 后代判断
55
+ ```
56
+
57
+ 变更操作会把文档标记为脏;引擎会延迟地(在下一次读取或下一帧之前)重新计算
58
+ 样式并重新布局。`inner = …`/`innerHTML = …` **不会**执行脚本。元素事件支持
59
+ 捕获与冒泡阶段、`stopPropagation`、`once`,并向上冒泡到 `document`/`window`。
60
+ 旧式 `win.on(type, fn)` 负载保留其**字符串** `e.target`(`div#id.class`);
61
+ 元素 `Event.target` 是一个句柄,其描述符与同一字符串匹配。
62
+
63
+ ### 节点生命周期
64
+
65
+ 一次变更会决定它移除的节点是否仍可到达,引擎只释放那些确实不可到达的节点:
66
+
67
+ | 变更 | 被移除的节点 |
68
+ | --- | --- |
69
+ | `innerHTML = …`、`inner = …`、`textContent = …`、`replaceChild` | **被释放**——脚本再也无法引用它们 |
70
+ | `removeChild`、`remove` | **保留**在文档的分离节点池中——DOM 语义要求被移除的节点继续存活且可用,以便重新插入 |
71
+
72
+ 被释放的节点会同时释放其 C++ 子树与句柄,因此 `win.stats()` 的计数器
73
+ (`detachedNodes`、`handles`、`handleSlots`)会回到先前的数值。这正是常见游戏
74
+ 写法——每帧用 `innerHTML` 重写精灵——能保持常量内存的原因;在此之前,每个被
75
+ 替换的子树都会永久留在池中,每帧写入十个节点的循环会以每秒数 MB 的速度泄漏。
76
+
77
+ 引擎唯一无法察觉的情况是:脚本创建了一个节点,随后从未插入就丢弃它。
78
+ `document.createElement("div")` 会把节点放进池中,**并且**让它的句柄保持可达,
79
+ 因为标记-清扫收集器没有弱引用,引擎无法知道脚本已经丢弃了该句柄。因此,
80
+ 只创建、不插入的循环会让池持续增长。请复用元素,或用 `innerHTML` 构建内容
81
+ (它会释放被替换的节点),而不要每帧新建元素。
82
+
83
+ ## AOT 脚本(M9)
84
+
85
+ 导入一个包含内联 `<script lang="ts">` 主体的 `.html` 文件;加载器会把每个
86
+ 主体编译为一个原生函数,`win.loadHTML(page)` 会在文档解析完成后运行它们
87
+ (没有 JavaScript 引擎,没有运行时 `eval`):
88
+
89
+ ```ts
90
+ import { createWindow, run } from "gui";
91
+ import page from "./page.html";
92
+
93
+ const win = createWindow({ title: "counter", width: 320, height: 240 });
94
+ win.loadHTML(page);
95
+ run();
96
+ ```
97
+
98
+ ```html
99
+ <button id="b">0</button>
100
+ <script lang="ts">
101
+ const b = document.getElementById("b");
102
+ let n = 0;
103
+ b.addEventListener("click", () => { b.textContent = String(++n); });
104
+ </script>
105
+ ```
106
+
107
+ 这两个参数(`window`、`document`)只是普通的函数参数,因此脚本的局部变量
108
+ 无需任何全局对象机制。外部脚本同样可用:`<script src="./counter.ts">` 会在
109
+ 编译期读取,其 import 会被提升(重写为从 HTML 文件解析),其主体以同样方式
110
+ 包装——因此脚本模块可以 `import` 辅助函数,同时仍能看到 `document`。脚本是
111
+ **编译期资产**:运行时创建的 HTML(`innerHTML`、网络获取)永不执行,`src`
112
+ URL(`https://…`、`data:…`)会被忽略。一切都按文档顺序在解析后运行(实际上
113
+ 是 deferred)。完整设计参见 `gui-scripts.md`。
114
+
@@ -0,0 +1,21 @@
1
+ # 输入与事件(M5)
2
+
3
+ > 语言 / Language:[English](../../gui/input.md) | **简体中文**
4
+
5
+ > 属于 [GUI 扩展](../gui.md) 的一部分;引擎总览、锁定决策与路线图在那里。
6
+ > 公开的事件 API 见 [api.md](./api.md)。
7
+
8
+ `LayoutTree::hitTest` 返回包含某点的最深盒(先探测靠后的兄弟,使最上层元素
9
+ 胜出),`xt_dom_describe` 把元素转换为事件负载携带的 `div#id.class` 描述符。
10
+
11
+ - SDL 指针/滚轮/键盘事件会被路由到所属窗口,进行命中测试,并投递给用 `on`
12
+ 注册的处理器。`mousedown` 还会移动焦点。
13
+ - `:hover` 匹配悬停元素**及其祖先**(因此悬停子元素会点亮其父元素);
14
+ `:focus` 匹配聚焦元素。当任一改变时,`XtDocument::setHover`/`setFocus`
15
+ 重新计算样式与布局,并把窗口几何标记为脏,因此改动会在下一帧绘制。
16
+ - `win.hitTest(x, y)` 与 `win.sendEvent(type, options)` 暴露命中测试与合成
17
+ 输入,使整条路径可无头测试(e2e 套件在没有真实鼠标的情况下驱动点击、滚轮
18
+ 与按键)。
19
+
20
+ 完整输入仍需完成:文本选择、拖拽、IME 与剪贴板。
21
+
@@ -0,0 +1,50 @@
1
+ # 已知问题与待决问题
2
+
3
+ > 语言 / Language:[English](../../gui/known-issues.md) | **简体中文**
4
+
5
+ > 属于 [GUI 扩展](../gui.md) 的一部分;引擎总览、锁定决策与路线图在那里。
6
+
7
+ ## 已知问题
8
+
9
+ - **自动关闭时的偶发崩溃。** 设置 `XT_GUI_AUTOCLOSE_MS` 结束运行时,约五次中有两次会以原生
10
+ 访问违例(`0xC0000005`)退出,其余运行与正常退出无异。该问题只在**动画帧循环运行中**复现
11
+ (静态文档可以正常关闭),且**不是示例游戏自身**的代码:没有任何异常信息,崩溃多发生在
12
+ 启动后一秒内、与截止时间无关,而提高 `XT_GC_THRESHOLD` 可以掩盖它,因此看起来是
13
+ GC/析构竞态。交互式运行(不设自动关闭)可稳定运行数分钟。此前的一个已知成因是引擎在标记-清扫
14
+ 收集器看不见的 C++ 容器里缓存 `xt_value`(AOT `<script>` 注册表、元素句柄、元素监听器、
15
+ 排队/正在运行的 `requestAnimationFrame` 回调),现在它们由 GC 根提供者标记
16
+ (`xt_gui_script_gc_scan` 与 `xt_gui_gc_scan_roots`),该类 use-after-free 已消除。
17
+ 在 M12 的节点生命周期改动之后它已完全无法复现:对
18
+ `examples/gui/pelican-bike` 做了 44 次自动关闭运行(截止 800 ms–4 s,分别使用默认、
19
+ 1 MiB 与 64 MiB 的收集阈值,而旧记录称约五次两次崩溃),全部正常退出,指向与内存问题
20
+ 相同的根因——分离节点池与句柄表随帧数增长,同时收集器阈值只升不降。此处仍保留该条目,
21
+ 因为该归档按平台构建且故障与时机相关。`tests/e2e/gui-example.test.ts`
22
+ 因此把该退出码视为可接受结果,行为断言放在不依赖 GPU 的
23
+ `tests/e2e/gui-example-browser.test.ts` 中。
24
+ - **`display: none -> flex` 的重排。** 通过类名切换让隐藏元素显示在 flex 容器上时,也曾观察
25
+ 到同样的崩溃;`examples/gui/pelican-bike` 改用 `opacity` 显示游戏结束遮罩。
26
+ - **SVG:** 支持 SVG *文件*,经图像加载器处理(`<img src="logo.svg">`):
27
+ `runtime/ext_gui/svg.cpp` 封装 vendored **NanoSVG** 解析/光栅化器,
28
+ `image.{h,cpp}` 把 SVG 视为矢量图像并按绘制像素尺寸光栅化。文档中的内联
29
+ `<svg>` **子树**仍不支持:HTML 解析器只认识元素与属性,布局只认识盒子,
30
+ 绘制只认识矩形/文本/图像;`<svg>` 子树需要另一套几何与绘制路径。因此矢量
31
+ 美术要么以 SVG/PNG 文件形式提供,要么像 `examples/gui/pelican-bike` 那样用
32
+ 盒子绘制。
33
+
34
+ ## 待决问题
35
+
36
+ - Linux 首发是只发布 X11、Wayland,还是两者都发布。(决定:先 X11,
37
+ Wayland 随后。)
38
+ - Windows:`scripts/build-gui.ts` 会生成 MSVC 兼容的 `gui.lib`
39
+ (COFF 对象 + `ar -M`/`llvm-ar`),但尚未在 CI 中验证,因此 Windows 步骤
40
+ 为试验性(job 保持绿色),`package` 会跳过它。它还需要一个带 D3D12/DXIL
41
+ 后端的 SDL3 构建(DXIL 需要 `dxc`)。
42
+ - **Windows 上的媒体:** 媒体解码器(FFmpeg)只构建并合并进 POSIX 的
43
+ `gui.a`;Windows 的 `gui.lib` 不含它们,因此那里的 `<video>`/`<audio>` 为空
44
+ 操作(失败会触发 `error`)。启用它需要 MSVC 兼容的 FFmpeg 构建,推迟到
45
+ Windows GUI 路径本身验证之后。`<video controls>` 同样推迟——预期模式是 TS +
46
+ CSS 叠层,如 `examples/gui/media.ts`。
47
+ - **D3D12 着色器:** SPIR-V(Vulkan)与 DXIL(Direct3D 12)二进制块均已构建并
48
+ 内嵌,非 Metal 路径不再跳过几何绘制。但在 **SDL 3.2.10** 上 DXIL 仍不可用:
49
+ 其 D3D12 后端会拒绝任何着色器中声明了 uniform buffer 的图形管线,而引擎正是
50
+ 用这种方式传视口。SDL 升级后需重新确认。
@@ -0,0 +1,60 @@
1
+ # 布局
2
+
3
+ > 语言 / Language:[English](../../gui/layout.md) | **简体中文**
4
+
5
+ > 属于 [GUI 扩展](../gui.md) 的一部分;引擎总览、锁定决策与路线图在那里。
6
+
7
+ ## 布局引擎(M3)
8
+
9
+ `runtime/ext_gui/layout.{h,cpp}` 把带样式的 DOM 转换为具有绝对(相对视口)
10
+ 几何信息的 `LayoutBox` 树:
11
+
12
+ - **块级流** —— 块级子元素垂直堆叠;`display: none`
13
+ 不生成盒;`width: auto` 填满包含块,`height: auto` 包裹内容。盒模型
14
+ (margin/padding/border)会被解析,包括相对包含块宽度的百分比。相邻兄弟的
15
+ 垂直外边距会折叠为 `max(正值) + min(负值)`;父元素的下外边距不会与其最后一个
16
+ 子元素折叠(见下方限制)。
17
+ - **行内流** —— 连续的行内级子元素构成一个匿名行内格式化上下文,采用贪心、
18
+ 基于单词的断行、`text-align` 与 `line-height`。行内元素获得其后代几何信息
19
+ 的并集;`display: inline-block` 以 shrink-to-fit 宽度原子化布局。每一行会收缩到
20
+ 与之重叠的浮动(`float: left|right`)所留出的空白带,因此后续行会绕开它们。
21
+ 每个文本片段会记住它覆盖的 run,以便绘制时对它整形。文本使用 HarfBuzz/FreeType
22
+ 技术栈测量(见[文本技术栈](./rendering.md))。
23
+ - **Flexbox** —— `row`/`column`(以及 `-reverse` 变体),支持 `gap`、
24
+ `flex-basis`/`flex-grow`/`flex-shrink`、`justify-content` 与 `align-items`
25
+ (当交叉轴尺寸确定时包括 `stretch`)。`flex-wrap: wrap` 会把项断成多行,
26
+ `align-content`(`stretch`/`flex-start`/`flex-end`/`center`/`space-between`/
27
+ `space-around`)在交叉轴上分配这些行,`wrap-reverse` 则翻转交叉轴。
28
+ flex 项的外边距参与其外框尺寸,因此
29
+ 负的 `margin-top` 会把该项上移,而不是被忽略。项之间的纯空白文本
30
+ (源码缩进)不会成为 flex 项。
31
+ - **定位** —— `position: relative` 会按 `top`/`right`/`bottom`/`left` 偏移该盒子
32
+ 及其子树,但不改变它在流中占用的空间;`position: absolute`(以及针对视口解析
33
+ 的 `fixed`)使其脱离文档流,并相对最近的已定位祖先的 padding box 定位(没有
34
+ 这样的祖先时则相对视口)。`auto` 的 inset 在该轴上保留静态位置;宽度 `auto`
35
+ 时使用 shrink-to-fit,除非 `left` 与 `right` 同时给出(此时填满两者之间的空间);
36
+ 百分比相对包含块解析。层叠通过绘制前按 `z-index` 对每个父元素的子元素排序
37
+ (负值 → `auto` → 正值)来近似实现;这只是一个层级,并非完整的层叠上下文,
38
+ 因此子元素无法脱离父元素的层级。
39
+ 注意该 shrink-to-fit 规则的后果:一个 `width: auto` 且没有显式尺寸的绝对定位
40
+ 盒子,其内部行内内容得到的**可用宽度为 0**(它的首选宽度只来自文本与
41
+ 非绝对定位的子元素),因此其中的行内 run 会各自换到独立的一行。请为此类盒子
42
+ 给出显式 `width`,或用 `left`/`width` 摆放其子元素(`examples/gui/pelican-bike`
43
+ 的精灵 run 就是这样做的),而不要依赖其内部的行流。
44
+
45
+ - **浮动与清除** —— `float: left|right` 使盒子脱离文档流,将其宽度设为
46
+ shrink-to-fit,放在当前行旁,并让后续行内行绕开它;`clear: left|right|both`
47
+ 会把后续块推到相关浮动的下方,而只包含浮动的块会增高以包裹它们。
48
+ - **`overflow` 裁剪** —— `overflow: hidden`(或 `scroll`/`auto`)的盒子会将其
49
+ 子树裁剪到自身的 padding box,并与任一祖先裁剪区域取交集;渲染器用 GPU
50
+ scissor 矩形实现该结果。目前还没有滚动,因此 `scroll`/`auto` 与 `hidden`
51
+ 一样只做裁剪。
52
+
53
+ 尚未实现:父/子与空块的外边距折叠、由非空白文本产生的匿名 flex 项、完整的
54
+ `z-index` 层叠上下文,以及 `float` 对块级子元素的影响(只有行内内容会绕排,
55
+ 块必须 `clear`)。
56
+
57
+ **Document**(`runtime/ext_gui/document.{h,cpp}`):持有 DOM 树,把 `<style>`
58
+ 文本汇总为一份样式表,为某个视口计算样式与布局,并提供
59
+ `querySelector`/`querySelectorAll`/`styleOf`/`boxOf`。
60
+
@@ -0,0 +1,71 @@
1
+ # 媒体:图像、视频与音频
2
+
3
+ > 语言 / Language:[English](../../gui/media.md) | **简体中文**
4
+
5
+ > 属于 [GUI 扩展](../gui.md) 的一部分;引擎总览、锁定决策与路线图在那里。
6
+
7
+ ## 图像(M6a)
8
+
9
+ `runtime/ext_gui/image.{h,cpp}` 封装 vendored **stb_image** 头文件,把
10
+ PNG/JPEG/BMP/GIF/TGA 解码为 RGBA8,并带有解码缓存与仅头文件的尺寸缓存
11
+ (`stbi_info`)。路径接受 `file://` 前缀与百分号编码。
12
+ `runtime/ext_gui/svg.{h,cpp}` 在其上增加 SVG:它封装 vendored **NanoSVG**
13
+ 库(解析 + 抗锯齿扫描线光栅化),按扩展名或内容识别 SVG,并产出与既有管线
14
+ 相同的 RGBA8 `Image`。`stb_image` 无法解码 SVG,这正是
15
+ `<img src="logo.svg">` 能工作的原因。
16
+
17
+ - `<img>` 是一个替换元素(`display: inline-block`):设置了 CSS 尺寸时布局
18
+ 采用该尺寸,否则采用固有像素尺寸;当只约束一个轴时保持宽高比。构建盒树时
19
+ 从文件头(无需完整解码)读取固有尺寸;对 SVG 则来自解析后的 `width`/`height`
20
+ 或 `viewBox`(两者都缺失时回退到 300x150)。
21
+ - 绘制时为每个 `<img>` 生成一个 `PaintImage`;渲染器把每个唯一 `src` 上传到
22
+ RGBA 纹理并绘制带纹理的四边形,把共享同一纹理的连续四边形批处理。
23
+ **位图**按固有分辨率解码并按路径缓存;**矢量图**(SVG)按设备像素下的绘制
24
+ 尺寸(逻辑尺寸 × 窗口像素密度)光栅化并保持 viewBox 宽高比,再按该尺寸缓存,
25
+ 因此缩放或 HiDPI 得到的是清晰渲染而非放大的位图。
26
+ - 解析结果按路径缓存;解析与光栅化器状态在 `xt_svg_shutdown`
27
+ (`xt_image_shutdown`)中释放。
28
+
29
+ 图像仍需完成:CSS `background-image: url(...)`、`data:` URI、`object-fit`、
30
+ 九宫格边框与内联 `<svg>` 元素(仅支持 SVG *文件*,参见[待决问题](./known-issues.md))。
31
+
32
+ ## 视频与音频(M13)
33
+
34
+ `runtime/ext_gui/media.{h,cpp}` 封装一个静态 vendored 的 **FFmpeg** 构建,
35
+ `runtime/ext_gui/gui_media.cpp` 把 DOM 树接到它上面。`<video>` 与 `<audio>`
36
+ 真正解码并播放;路径中没有 WebView,也没有平台媒体框架。
37
+
38
+ - **解码。** `avformat_open_input` 打开 `src`(是路径——接受 `file://`,但与
39
+ 图像不同,媒体路径*不做*百分号解码),为每种媒体选取最佳流,软件解码器
40
+ 产出 `yuv/rgb` 帧,`sws_scale` 按编码尺寸转换为 RGBA8。支持的容器/编码即
41
+ `scripts/build-gui.ts` 的最小 FFmpeg 配置:MP4/MOV/MKV/AVI/FLV/MPEG-TS/M4V/
42
+ ASF,MP3/OGG/WAV/FLAC/AAC;视频 H.264、HEVC、VP8、VP9、AV1、MPEG-4、MJPEG;
43
+ 音频 AAC、MP3、Opus、Vorbis、FLAC、ALAC、AC3/E-AC3、WMAv2 及原始 PCM 格式。
44
+ - **视频。** `<video>` 与 `<img>` 一样是替换元素(UA 默认 `300x150`,解码
45
+ 尺寸通过 `setMediaSize` 覆盖)。绘制产出与图像相同的 `PaintImage`;渲染器把
46
+ 每个新解码帧上传到 RGBA 纹理(`ensureMediaTexture`)并绘制带纹理四边形,
47
+ 因此由 GPU 缩放到 CSS 盒,而非 CPU 缩放。播放计时基于墙钟
48
+ (`SDL_GetTicks`),不是帧间隔。
49
+ - **音频。** 没有音频回调:解码器随主线程 tick 被推送,PCM 以 push 模式
50
+ (`NULL` 回调)通过 `SDL_OpenAudioDeviceStream` 排队,在前方缓冲约 0.25 秒,
51
+ 格式为 48 kHz 立体声 `SDL_AUDIO_F32`。`volume` 映射到
52
+ `SDL_SetAudioStreamGain`;音频是尽力而为,无设备时(无头 CI)跳过。
53
+ - **线程。** 引擎是单线程且非线程感知,因此所有解码都在主线程的
54
+ `xt_gui_media_tick` 内完成——没有解码线程,也没有跨线程 GC 暴露。
55
+ - **DOM API。** 元素句柄直接暴露媒体接口(见
56
+ `runtime/ext_gui/dom_api_media.cpp` 与 `dom_api_proto.cpp` 中的原型):
57
+ `play()` / `pause()` / `load()`,以及 `paused` / `ended` / `duration` /
58
+ `currentTime` / `volume` / `muted` / `loop` / `autoplay` / `videoWidth` /
59
+ `videoHeight` / `readyState` / `src` 属性。媒体元素还会触发
60
+ `loadedmetadata`、`play`、`pause`、`ended`、`timeupdate`(每 250 毫秒)与
61
+ `error`,因此常见的 `addEventListener` 写法可用。
62
+ - **没有内置控件。** `<video controls>` 刻意*不*实现:元素始终渲染原始帧,
63
+ 程序用普通元素和 CSS 绘制自己的控件(进度条、按钮)。模式见
64
+ `examples/gui/media.html`(`examples/gui/media.ts`)。
65
+ - **生命周期。** 播放器按窗口持有(`XtGuiMediaEntry`,以节点为键)。
66
+ `xt_gui_media_sync` 清除节点已不存在的条目、在 `src` 变化时重开、并在首次
67
+ 使用时打开;`xt_gui_media_shutdown` 在窗口关闭/退出/`loadHTML` 时运行。
68
+
69
+ 媒体仍需完成:`<video controls>`、音视频时钟漂移校正、硬件解码、流式协议
70
+ (仅启用 `file`)、字幕、`playbackRate` 与逐帧 `requestVideoFrameCallback`。
71
+