xbintsc 0.3.49 → 0.3.65

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 (51) hide show
  1. package/dist/src/driver/compiler.js +25 -1
  2. package/dist/src/driver/compiler.js.map +1 -1
  3. package/dist/src/driver/config.d.ts +7 -0
  4. package/dist/src/driver/config.js.map +1 -1
  5. package/dist/src/driver/win-entry.d.ts +21 -0
  6. package/dist/src/driver/win-entry.js +52 -0
  7. package/dist/src/driver/win-entry.js.map +1 -0
  8. package/dist/tests/e2e/gui-events.test.js +32 -0
  9. package/dist/tests/e2e/gui-events.test.js.map +1 -1
  10. package/dist/tests/e2e/gui-example-browser.test.d.ts +12 -0
  11. package/dist/tests/e2e/gui-example-browser.test.js +168 -0
  12. package/dist/tests/e2e/gui-example-browser.test.js.map +1 -0
  13. package/dist/tests/e2e/gui-example.test.d.ts +14 -0
  14. package/dist/tests/e2e/gui-example.test.js +76 -0
  15. package/dist/tests/e2e/gui-example.test.js.map +1 -0
  16. package/dist/tests/e2e/gui-helpers.d.ts +39 -4
  17. package/dist/tests/e2e/gui-helpers.js +38 -9
  18. package/dist/tests/e2e/gui-helpers.js.map +1 -1
  19. package/dist/tests/e2e/gui-layout.test.js +155 -0
  20. package/dist/tests/e2e/gui-layout.test.js.map +1 -1
  21. package/dist/tests/e2e/gui-lifecycle.test.js +10 -1
  22. package/dist/tests/e2e/gui-lifecycle.test.js.map +1 -1
  23. package/dist/tests/e2e/gui-paint.test.js +44 -4
  24. package/dist/tests/e2e/gui-paint.test.js.map +1 -1
  25. package/dist/tests/e2e/gui-script.test.js +8 -4
  26. package/dist/tests/e2e/gui-script.test.js.map +1 -1
  27. package/doc/gui.md +120 -9
  28. package/doc/xbintsc.config.schema.json +4 -0
  29. package/doc/zh-CN/gui.md +78 -7
  30. package/package.json +1 -1
  31. package/runtime/ext_gui/dom_api.cpp +19 -0
  32. package/runtime/ext_gui/dom_api_node.cpp +4 -1
  33. package/runtime/ext_gui/dom_api_node_read.cpp +12 -0
  34. package/runtime/ext_gui/gui.cpp +42 -2
  35. package/runtime/ext_gui/gui_engine.h +15 -0
  36. package/runtime/ext_gui/gui_events.cpp +13 -3
  37. package/runtime/ext_gui/layout.cpp +6 -0
  38. package/runtime/ext_gui/layout.h +4 -0
  39. package/runtime/ext_gui/layout_flex.cpp +13 -5
  40. package/runtime/ext_gui/layout_flow.cpp +134 -4
  41. package/runtime/ext_gui/layout_flow_internal.h +41 -2
  42. package/runtime/ext_gui/layout_inline.cpp +11 -3
  43. package/runtime/ext_gui/paint.cpp +6 -0
  44. package/runtime/ext_gui/script.cpp +12 -2
  45. package/runtime/ext_gui/text.cpp +14 -0
  46. package/runtime/ext_gui/window.cpp +44 -0
  47. package/runtime/rt.h +10 -0
  48. package/runtime/xt_io/console.inc +15 -3
  49. package/src/driver/compiler.ts +26 -1
  50. package/src/driver/config.ts +7 -0
  51. package/src/driver/win-entry.ts +59 -0
package/doc/gui.md CHANGED
@@ -1,7 +1,8 @@
1
1
  # xbintsc GUI extension (self-hosted HTML/CSS renderer)
2
2
 
3
- Status: **M10** — features (M1–M10) are complete: HTML parsing, CSS selector
4
- matching, the cascade, computed styles and layout (block, inline and Flexbox) are
3
+ Status: **M11** — features (M1–M11) are complete: HTML parsing, CSS selector
4
+ matching, the cascade, computed styles and layout (block, inline, Flexbox and
5
+ CSS positioning) are
5
6
  in place, and the engine *paints*: it builds a display list of rectangles, images
6
7
  and shaped text runs and renders them through SDL_GPU. Input events are hit
7
8
  tested and delivered to native TS handlers, `:hover`/`:focus` are matched
@@ -11,6 +12,8 @@ DOM: element handles with stable identity, mutation (`appendChild`, `textContent
11
12
  `classList`, `style`, …) and element-level events with capture/bubbling.
12
13
  **M9** compiles `<script>` bodies ahead of time (inline and `<script src>`) — see
13
14
  `doc/gui-scripts.md`. **M10** adds `requestAnimationFrame` plus a few DOM helpers.
15
+ **M11** adds `position: relative`/`absolute`/`fixed` with their offsets, which is
16
+ what `examples/gui/pelican-bike` — a playable 2D game — is built on.
14
17
  This document records the locked decisions, the architecture, the milestone plan
15
18
  and the current progress of a cross-platform GUI extension that renders an
16
19
  HTML/CSS UI with its own GPU-accelerated engine.
@@ -111,9 +114,21 @@ run(); // drives the main loop until all windows
111
114
  ```
112
115
 
113
116
  Methods implemented on a window handle: `setTitle` / `setSize` / `loadHTML` /
114
- `getHTML` / `setBackground` / `close` / `isOpen` / `on` / `off`. Events emitted:
115
- `ready` (after the first presented frame), `load`, `close`, and the input events
116
- `mousemove`, `mousedown`, `mouseup`, `click`, `wheel`, `keydown`, `keyup`.
117
+ `getHTML` / `setBackground` / `close` / `isOpen` / `on` / `off`, plus `driver()`,
118
+ which reports the SDL_GPU backend in use (`"vulkan"`, `"direct3d12"`, `"metal"`,
119
+ … — SDL picks the best one the device supports). The Web spellings are available
120
+ too: `addEventListener` / `removeEventListener` are aliases of `on` / `off` (their
121
+ `options` argument is accepted and ignored) and the `closed` getter mirrors
122
+ `isOpen()`, so a document written against the browser API runs unchanged.
123
+ Events emitted:
124
+ `ready` (after the first presented frame), `load`, `close`, `resize` and the
125
+ input events `mousemove`, `mousedown`, `mouseup`, `click`, `wheel`, `keydown`,
126
+ `keyup`.
127
+
128
+ On Windows a GUI program is linked as a **Windows-subsystem** executable, so
129
+ launching it does not flash a console window. That also means stdout/stderr go
130
+ nowhere when it is started from Explorer; set `app.console: true` in
131
+ `xbintsc.config.json` to keep the console subsystem (useful while debugging).
117
132
 
118
133
  Input handlers receive a single payload object (lifecycle handlers receive none):
119
134
 
@@ -284,10 +299,29 @@ with absolute (viewport-relative) geometry:
284
299
  measured with the HarfBuzz/FreeType stack (see *Implemented text*).
285
300
  - **Flexbox** — single-line `row`/`column` (and the `-reverse` variants) with
286
301
  `gap`, `flex-basis`/`flex-grow`/`flex-shrink`, `justify-content` and
287
- `align-items` (including `stretch` when the cross size is definite).
288
-
289
- Not yet implemented: margin collapsing, multi-line flex wrapping, `position`
290
- offsets (`relative`/`absolute`/`fixed`), `overflow` clipping and floats.
302
+ `align-items` (including `stretch` when the cross size is definite). Item
303
+ margins are part of the item's outer size on both axes, so a negative
304
+ `margin-top` pulls an item up instead of being ignored.
305
+ - **Positioning** — `position: relative` offsets a box (and its subtree) by
306
+ `top`/`right`/`bottom`/`left` without changing the space it reserved;
307
+ `position: absolute` (and `fixed`, which resolves against the viewport) takes
308
+ the box out of flow and places it against the padding box of the nearest
309
+ positioned ancestor — the viewport when there is none. An `auto` inset keeps
310
+ the static position on that axis, an `auto` width is shrink-to-fit unless both
311
+ `left` and `right` are set (then it fills the space between them), and
312
+ percentages resolve against the containing block. There is no `z-index`
313
+ stacking yet: painting stays in document order.
314
+ Note the consequence of that shrink-to-fit rule: an absolutely positioned box
315
+ with `width: auto` and no explicit size gets **no** available width for its own
316
+ inline content (its preferred width comes from its text or its non-absolute
317
+ children only), so inline runs inside it are laid out at zero available width
318
+ and each one wraps onto its own line. Give such a box an explicit `width`, or
319
+ place its children with `left`/`width` (as
320
+ `examples/gui/pelican-bike` does for its sprite runs), instead of relying on
321
+ line flow inside it.
322
+
323
+ Not yet implemented: margin collapsing, multi-line flex wrapping, `z-index`
324
+ stacking, `overflow` clipping and floats.
291
325
 
292
326
  **Document** (`runtime/ext_gui/document.{h,cpp}`): owns the DOM tree, gathers
293
327
  `<style>` text into one stylesheet, computes styles and layout for a viewport and
@@ -302,6 +336,13 @@ emits a backend-agnostic `DisplayList` with two parallel lists: **rectangles**
302
336
  lets the renderer draw all rectangles, then all text on top, with one draw call
303
337
  per list. `DisplayList::dump()` feeds `paintList()`/`paintCount()`.
304
338
 
339
+ `opacity` is honoured only at its extremes: a box whose computed `opacity` is
340
+ `0` (and its whole subtree) paints nothing, and anything above `0` paints fully
341
+ opaque. A shape carries its colour's alpha only, so there is no compositing of a
342
+ partly transparent box — the `opacity` transitions the CSS parser accepts are
343
+ therefore not visible either. Hiding and revealing a layer with `opacity: 0` on
344
+ the hidden state is what `examples/gui/pelican-bike`'s game-over overlay does.
345
+
305
346
  `runtime/ext_gui/renderer.{h,cpp}` turns that list into two batched vertex
306
347
  buffers per window (one for shapes, one for glyph quads) drawn through two
307
348
  SDL_GPU graphics pipelines:
@@ -406,6 +447,12 @@ Still to do for animation: `@keyframes` animations and `cubic-bezier(...)`.
406
447
  `XT_GUI_FONT` (and `XT_GUI_FONT_MONO`), and cached per `(family class, size)`.
407
448
  Only regular upright faces are used for now; weight/italic selection is a
408
449
  later refinement.
450
+ - The default candidates prefer a **CJK-capable** face (Microsoft YaHei on
451
+ Windows, PingFang on macOS, Noto Sans CJK/WenQuanYi on Linux) before the
452
+ Latin-only ones, because a run is shaped with a *single* face: without this,
453
+ Chinese/Japanese/Korean text renders as blank or `.notdef` boxes. Set
454
+ `XT_GUI_FONT` to a Latin-only face to get its Latin design instead (at the
455
+ cost of CJK coverage).
409
456
  - `xt_text_measure_width` shapes the run with HarfBuzz (so kerning and
410
457
  ligatures are honoured), `xt_text_metrics` returns FreeType's ascent /
411
458
  descent / normal line height. When no font file can be found the module falls
@@ -487,9 +534,23 @@ npm run gui # build runtime/lib/<os>-<arch>/gui.a (fetches SDL3 once)
487
534
  xbintsc run examples/gui/hello.ts --ext gui
488
535
  ```
489
536
 
537
+ Two examples live here: `hello.ts` (HTML/CSS + a reactive counter) and
538
+ [`pelican-bike/`](../examples/gui/pelican-bike), a playable 2D game that uses
539
+ positioned layers, CSS sprites, `requestAnimationFrame` and keyboard input — see
540
+ its README for controls and the geometry report it prints.
541
+
490
542
  Set `XT_GUI_AUTOCLOSE_MS=<n>` to close all windows after `n` milliseconds,
491
543
  which the e2e test (`tests/e2e/gui-*.test.ts`) uses to run headlessly.
492
544
 
545
+ Those suites are skipped on Windows by default (the archive is built there but
546
+ the run is not validated in CI). With a `gui.lib` and a working GPU, opt in with
547
+ `xbintsc_GUI_TESTS=1`:
548
+
549
+ ```bat
550
+ set xbintsc_GUI_TESTS=1
551
+ npx vitest run tests/e2e/gui-layout.test.ts tests/e2e/gui-example.test.ts
552
+ ```
553
+
493
554
  On a headless Linux box, install the SDL3 build headers and run under Xvfb with a
494
555
  software Vulkan driver:
495
556
 
@@ -579,6 +640,16 @@ used by default; Wayland is enabled too but not yet exercised.
579
640
  DOM (`gui.cpp`, `window.cpp`, `gui_engine.h`).
580
641
  - `Element.offsetWidth` / `offsetHeight` (rounded border box, flushes pending
581
642
  mutations) and `Element.contains(other)` (`dom_api.cpp`).
643
+ 11. **M11 — CSS positioning** ✅
644
+ - `position: relative` / `absolute` / `fixed` with `top`/`right`/`bottom`/
645
+ `left`, resolved against the nearest positioned ancestor (the viewport when
646
+ there is none), `auto` insets keeping the static position, shrink-to-fit
647
+ for an out-of-flow `width: auto` and fill-available when both horizontal
648
+ insets are set (`layout_flow.cpp`, `layout.h`).
649
+ - Item margins are honoured in the flex algorithm, so a negative margin
650
+ participates in the layout instead of being dropped (`layout_flex.cpp`).
651
+ - Drives `examples/gui/pelican-bike` and e2e coverage in
652
+ `tests/e2e/gui-layout.test.ts`.
582
653
 
583
654
  ## Progress log
584
655
 
@@ -629,11 +700,51 @@ used by default; Wayland is enabled too but not yet exercised.
629
700
  (callbacks run with the frame timestamp before layout each frame) plus
630
701
  `offsetWidth`/`offsetHeight`/`contains` on element handles; e2e coverage in
631
702
  `tests/e2e/gui-*.test.ts`.
703
+ - **M11** ✅ `position: relative`/`absolute`/`fixed` with `top`/`right`/`bottom`/
704
+ `left`. Layout keeps a containing block per box (the nearest positioned
705
+ ancestor's padding box, published only once that ancestor has its final size,
706
+ so `bottom`/`right` are exact), absolute boxes are laid out after in-flow
707
+ content so they never affect it, shrink-to-fit/fill-available width rules are
708
+ applied, and flex items now account for their margins. e2e coverage in
709
+ `tests/e2e/gui-layout.test.ts`; `examples/gui/pelican-bike` is built on it.
710
+
711
+ ## Known issues
712
+
713
+ - **Intermittent crash at auto-close.** `XT_GUI_AUTOCLOSE_MS` can end a run
714
+ with a native access violation in roughly two runs out of five; a clean exit
715
+ looks identical otherwise. Windows reports it as the NTSTATUS exit code
716
+ `0xC0000005`; on Linux/macOS the process dies with a signal — an access
717
+ violation is `SIGSEGV`, so `spawnSync` reports `status: null` plus a
718
+ `signal`. It only reproduces with a *running animation-frame loop* (a static
719
+ document closes fine) and it is **not** the game's own code: no exception is
720
+ reported, the crash usually lands inside the first second regardless of the
721
+ deadline, and raising `XT_GC_THRESHOLD` hides it, so it looks like a
722
+ GC/teardown race. Interactive runs (no auto-close) are stable for minutes.
723
+ A leading cause was that the engine cached `xt_value`s in C++ containers
724
+ the mark-sweep collector cannot see — the AOT `<script>` registry, element
725
+ handles, element listeners and the queued/currently-running
726
+ `requestAnimationFrame` callbacks. They are now marked by GC root providers
727
+ (`xt_gui_script_gc_scan` and `xt_gui_gc_scan_roots`), which removes that
728
+ class of use-after-free. `tests/e2e/gui-example.test.ts` still treats either
729
+ crash spelling as an acceptable outcome — the archive is built per platform
730
+ and the run is timing-sensitive — and the behavioural assertions live in
731
+ `tests/e2e/gui-example-browser.test.ts` (no GPU, deterministic).
732
+ - **`display: none -> flex` restyle.** A hidden element being shown by a
733
+ *class change* on a flex container has also been observed to crash the same
734
+ way; `examples/gui/pelican-bike` reveals its game-over overlay with `opacity`
735
+ instead.
632
736
 
633
737
  ## Open questions
634
738
 
635
739
  - Whether Linux ships X11, Wayland, or both in the first cut. (Decision:
636
740
  X11 first, Wayland later.)
741
+ - **SVG:** there is no SVG support and none is planned. The HTML parser knows
742
+ elements and attributes, the layout engine knows boxes, and paint knows
743
+ rectangles/text/images, so `<svg>` trees would need a whole second
744
+ geometry+paint path. An SVG *file* works only through the image loader
745
+ (`<img src="logo.svg">`), which needs an SVG rasteriser (`stb_image` does not
746
+ decode SVG); vector art therefore has to arrive as a raster (PNG) or be drawn
747
+ from boxes, as `examples/gui/pelican-bike` does.
637
748
  - Windows: an MSVC-compatible `gui.lib` (COFF objects + `ar -M`/`llvm-ar`) is
638
749
  produced by `scripts/build-gui.ts`, but it has not been validated in CI yet,
639
750
  so the Windows step is provisional (the job stays green) and `package` skips
@@ -60,6 +60,10 @@
60
60
  "bundleId": {
61
61
  "type": "string",
62
62
  "description": "macOS CFBundleIdentifier, e.g. com.example.demo."
63
+ },
64
+ "console": {
65
+ "type": "boolean",
66
+ "description": "Windows only: keep the console subsystem (a console window) instead of the default Windows subsystem, so stdout/stderr are visible while debugging a GUI program."
63
67
  }
64
68
  }
65
69
  }
package/doc/zh-CN/gui.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  > 语言 / Language:[English](../gui.md) | **简体中文**
4
4
 
5
- 状态:**M10** —— 功能(M1–M10)已全部完成:HTML 解析、CSS 选择器匹配、层叠(cascade)、计算样式与布局(块级、行内与 Flexbox)均已就位,引擎还会**绘制**:它构建一个由矩形、图像与排版后的文本 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 辅助方法。本文档记录了一个跨平台 GUI 扩展的锁定决策、架构、里程碑计划与当前进度;该扩展使用自有的 GPU 加速引擎渲染 HTML/CSS UI。
5
+ 状态:**M11** —— 功能(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 游戏)就构建在它之上。本文档记录了一个跨平台 GUI 扩展的锁定决策、架构、里程碑计划与当前进度;该扩展使用自有的 GPU 加速引擎渲染 HTML/CSS UI。
6
6
 
7
7
  ## 目标
8
8
 
@@ -88,8 +88,19 @@ run(); // 驱动主循环,直到所有窗口
88
88
  ```
89
89
 
90
90
  窗口句柄上已实现的方法:`setTitle` / `setSize` / `loadHTML` /
91
- `getHTML` / `setBackground` / `close` / `isOpen` / `on` / `off`。触发的事件:
92
- `ready`(首个呈现帧之后)、`load`、`close`,以及输入事件
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`,以及输入事件
93
104
  `mousemove`、`mousedown`、`mouseup`、`click`、`wheel`、`keydown`、`keyup`。
94
105
 
95
106
  输入处理器接收单个负载对象(生命周期处理器不接收任何参数):
@@ -252,10 +263,21 @@ run();
252
263
  技术栈测量(见 *已实现的文本*)。
253
264
  - **Flexbox** —— 单行 `row`/`column`(以及 `-reverse` 变体),支持 `gap`、
254
265
  `flex-basis`/`flex-grow`/`flex-shrink`、`justify-content` 与 `align-items`
255
- (当交叉轴尺寸确定时包括 `stretch`)。
256
-
257
- 尚未实现:外边距折叠、多行 flex 换行、`position` 偏移
258
- (`relative`/`absolute`/`fixed`)、`overflow` 裁剪与浮动。
266
+ (当交叉轴尺寸确定时包括 `stretch`)。flex 项的外边距参与其外框尺寸,因此
267
+ 负的 `margin-top` 会把该项上移,而不是被忽略。
268
+ - **定位** —— `position: relative` 会按 `top`/`right`/`bottom`/`left` 偏移该盒子
269
+ 及其子树,但不改变它在流中占用的空间;`position: absolute`(以及针对视口解析
270
+ 的 `fixed`)使其脱离文档流,并相对最近的已定位祖先的 padding box 定位(没有
271
+ 这样的祖先时则相对视口)。`auto` 的 inset 在该轴上保留静态位置;宽度 `auto`
272
+ 时使用 shrink-to-fit,除非 `left` 与 `right` 同时给出(此时填满两者之间的空间);
273
+ 百分比相对包含块解析。目前还没有 `z-index` 层叠,绘制仍按文档顺序进行。
274
+ 注意该 shrink-to-fit 规则的后果:一个 `width: auto` 且没有显式尺寸的绝对定位
275
+ 盒子,其内部行内内容得到的**可用宽度为 0**(它的首选宽度只来自文本与
276
+ 非绝对定位的子元素),因此其中的行内 run 会各自换到独立的一行。请为此类盒子
277
+ 给出显式 `width`,或用 `left`/`width` 摆放其子元素(`examples/gui/pelican-bike`
278
+ 的精灵 run 就是这样做的),而不要依赖其内部的行流。
279
+
280
+ 尚未实现:外边距折叠、多行 flex 换行、`z-index` 层叠、`overflow` 裁剪与浮动。
259
281
 
260
282
  **Document**(`runtime/ext_gui/document.{h,cpp}`):持有 DOM 树,把 `<style>`
261
283
  文本汇总为一份样式表,为某个视口计算样式与布局,并提供
@@ -269,6 +291,11 @@ run();
269
291
  把它们分开可以让渲染器先绘制所有矩形,再在其上绘制所有文本,每个列表一次
270
292
  绘制调用。`DisplayList::dump()` 为 `paintList()`/`paintCount()` 提供数据。
271
293
 
294
+ `opacity` 只在两端生效:计算值为 `0` 的盒子(及其整棵子树)不绘制任何内容,
295
+ 大于 `0` 的一律按完全不透明绘制。图形只携带其颜色自身的 alpha,因此没有部分
296
+ 透明盒子的合成——CSS 解析器接受的 `opacity` 过渡因此也看不到效果。把隐藏态写成
297
+ `opacity: 0` 来隐藏/显示一层,正是 `examples/gui/pelican-bike` 游戏结束遮罩的做法。
298
+
272
299
  `runtime/ext_gui/renderer.{h,cpp}` 把该列表转换为每个窗口两个批处理顶点缓冲
273
300
  (一个用于图形,一个用于字形四边形),通过两条 SDL_GPU 图形管线绘制:
274
301
 
@@ -441,6 +468,19 @@ xbintsc run examples/gui/hello.ts --ext gui
441
468
  设置 `XT_GUI_AUTOCLOSE_MS=<n>` 可在 `n` 毫秒后关闭所有窗口,e2e 测试
442
469
  (`tests/e2e/gui-*.test.ts`)用它来无头运行。
443
470
 
471
+ 这里有两个示例:`hello.ts`(HTML/CSS + 响应式计数器)与
472
+ [`pelican-bike/`](../examples/gui/pelican-bike)(一个可玩的 2D 游戏,使用分层
473
+ 定位、CSS 像素精灵、`requestAnimationFrame` 与键盘输入)——操作方式与它输出的
474
+ 几何报告见该目录的 README。
475
+
476
+ 这些测试套件在 Windows 上默认跳过(该平台会构建归档,但 CI 未验证运行)。
477
+ 如果本机有 `gui.lib` 与可用的 GPU,可用 `xbintsc_GUI_TESTS=1` 选择启用:
478
+
479
+ ```bat
480
+ set xbintsc_GUI_TESTS=1
481
+ npx vitest run tests/e2e/gui-layout.test.ts tests/e2e/gui-example.test.ts
482
+ ```
483
+
444
484
  在无头 Linux 机器上,安装 SDL3 构建头文件,并在 Xvfb 与软件 Vulkan 驱动下
445
485
  运行:
446
486
 
@@ -525,6 +565,15 @@ xvfb-run -a --server-args="-screen 0 1280x720x24" \
525
565
  (`gui.cpp`、`window.cpp`、`gui_engine.h`)。
526
566
  - `Element.offsetWidth` / `offsetHeight`(取整后的边框盒,会冲刷待处理
527
567
  的变更)与 `Element.contains(other)`(`dom_api.cpp`)。
568
+ 11. **M11 — CSS 定位** ✅
569
+ - `position: relative` / `absolute` / `fixed` 与 `top`/`right`/`bottom`/
570
+ `left`,相对最近的已定位祖先解析(没有时相对视口);`auto` inset 保留
571
+ 静态位置;脱离文档流的 `width: auto` 使用 shrink-to-fit,两个水平 inset
572
+ 都给出时则填满可用宽度(`layout_flow.cpp`、`layout.h`)。
573
+ - flex 算法现在会计入项的外边距,负外边距参与布局而不再被丢弃
574
+ (`layout_flex.cpp`)。
575
+ - `examples/gui/pelican-bike` 基于它实现,e2e 覆盖见
576
+ `tests/e2e/gui-layout.test.ts`。
528
577
 
529
578
  ## 进度日志
530
579
 
@@ -573,6 +622,28 @@ xvfb-run -a --server-args="-screen 0 1280x720x24" \
573
622
  (回调在每帧布局之前以帧时间戳运行),以及元素句柄上的
574
623
  `offsetWidth`/`offsetHeight`/`contains`;e2e 覆盖在
575
624
  `tests/e2e/gui-*.test.ts` 中。
625
+ - **M11** ✅ `position: relative`/`absolute`/`fixed` 与 `top`/`right`/`bottom`/
626
+ `left`。布局为每个盒子保留包含块(最近的已定位祖先的 padding box,且只在
627
+ 该祖先拥有最终尺寸后才发布,因此 `bottom`/`right` 精确);绝对定位的盒子
628
+ 在正常流内容之后布局,因此不影响它们;实现了 shrink-to-fit / 填满可用宽度
629
+ 两种规则;flex 项现在会计入外边距。e2e 覆盖在
630
+ `tests/e2e/gui-layout.test.ts`;`examples/gui/pelican-bike` 基于它实现。
631
+
632
+ ## 已知问题
633
+
634
+ - **自动关闭时的偶发崩溃。** 设置 `XT_GUI_AUTOCLOSE_MS` 结束运行时,约五次中有两次会以原生
635
+ 访问违例(`0xC0000005`)退出,其余运行与正常退出无异。该问题只在**动画帧循环运行中**复现
636
+ (静态文档可以正常关闭),且**不是示例游戏自身**的代码:没有任何异常信息,崩溃多发生在
637
+ 启动后一秒内、与截止时间无关,而提高 `XT_GC_THRESHOLD` 可以掩盖它,因此看起来是
638
+ GC/析构竞态。交互式运行(不设自动关闭)可稳定运行数分钟。`tests/e2e/gui-example.test.ts`
639
+ 因此把该退出码视为可接受结果,行为断言放在不依赖 GPU 的
640
+ `tests/e2e/gui-example-browser.test.ts` 中。
641
+ - **`display: none -> flex` 的重排。** 通过类名切换让隐藏元素显示在 flex 容器上时,也曾观察
642
+ 到同样的崩溃;`examples/gui/pelican-bike` 改用 `opacity` 显示游戏结束遮罩。
643
+ - **SVG:** 引擎不支持也不计划支持 SVG 元素。HTML 解析器只认识元素与属性,布局只认识盒子,
644
+ 绘制只认识矩形/文本/图像;`<svg>` 子树需要另一套几何与绘制路径。SVG *文件* 也只能走图像
645
+ 加载器(`<img src="logo.svg">`),而这需要 SVG 光栅化器(`stb_image` 不解码 SVG)。因此
646
+ 矢量美术要么以位图(PNG)形式提供,要么像 `examples/gui/pelican-bike` 那样用盒子绘制。
576
647
 
577
648
  ## 待决问题
578
649
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xbintsc",
3
- "version": "0.3.49",
3
+ "version": "0.3.65",
4
4
  "description": "xbintsc - a TypeScript binary compiler (TypeScript -> LLVM IR -> native binary)",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -136,6 +136,25 @@ xt_value xt_gui_node_handle(XtGuiWindow *win, const xtgui::Node *node) {
136
136
  return handle;
137
137
  }
138
138
 
139
+ void xt_gui_node_handle_forget(XtGuiWindow *win, const xtgui::Node *node) {
140
+ if (win == nullptr || node == nullptr) return;
141
+ /* The node is about to be destroyed (a removal, or `innerHTML = …` dropping
142
+ * the old children). Drop its slot as well as its handle: the slot is what
143
+ * `resolve_node` reads, and leaving a freed pointer there is a
144
+ * use-after-free the next time any handle is resolved. Existing handles keep
145
+ * resolving to "stale" and no-op, which is the documented tombstone
146
+ * behaviour. */
147
+ auto index = win->node_index.find(node);
148
+ if (index != win->node_index.end()) {
149
+ int slot = index->second;
150
+ if (slot >= 0 && slot < (int)win->node_order.size()) {
151
+ win->node_order[(size_t)slot] = nullptr;
152
+ }
153
+ win->node_index.erase(index);
154
+ }
155
+ win->node_handles.erase(node);
156
+ }
157
+
139
158
  xt_value xt_gui_document_handle(XtGuiWindow *win) {
140
159
  if (win == nullptr) return XT_UNDEFINED;
141
160
  if (!XT_IS_OBJECT(win->document_object)) {
@@ -177,7 +177,10 @@ METHOD(node_replace_child) {
177
177
  xtgui::Node *newChild = resolve_node(xt_arg(argc, argv, 0), nullptr);
178
178
  xtgui::Node *oldChild = resolve_node(xt_arg(argc, argv, 1), nullptr);
179
179
  if (newChild == nullptr || oldChild == nullptr) return XT_UNDEFINED;
180
- return xt_gui_node_handle(win, win->document->replaceChild(node, newChild, oldChild));
180
+ /* `replaceChild` frees the node it replaces, so its slot may not survive. */
181
+ xtgui::Node *replaced = win->document->replaceChild(node, newChild, oldChild);
182
+ if (replaced != oldChild) xt_gui_node_handle_forget(win, oldChild);
183
+ return xt_gui_node_handle(win, replaced);
181
184
  }
182
185
 
183
186
  METHOD(node_remove) {
@@ -25,6 +25,13 @@ static void collect_text(const xtgui::Node *node, std::string &out) {
25
25
  for (const std::unique_ptr<xtgui::Node> &child : node->children) collect_text(child.get(), out);
26
26
  }
27
27
 
28
+ /** Forget the handles of `node` and every descendant: they are about to be
29
+ * freed, and a slot left behind would dangle. */
30
+ static void forget_subtree(XtGuiWindow *win, const xtgui::Node *node) {
31
+ xt_gui_node_handle_forget(win, node);
32
+ for (const std::unique_ptr<xtgui::Node> &child : node->children) forget_subtree(win, child.get());
33
+ }
34
+
28
35
  #define METHOD(name) xt_value name(xt_value self, xt_value env, int32_t argc, xt_value *argv)
29
36
 
30
37
  METHOD(node_tag_name_get) {
@@ -105,6 +112,7 @@ METHOD(node_text_content_set) {
105
112
  std::vector<xtgui::Node *> existing;
106
113
  for (const std::unique_ptr<xtgui::Node> &child : node->children) existing.push_back(child.get());
107
114
  for (xtgui::Node *child : existing) win->document->removeChild(node, child);
115
+ for (xtgui::Node *child : existing) forget_subtree(win, child);
108
116
  if (!text.empty()) node->addText(text);
109
117
  mark_dirty(win);
110
118
  return XT_UNDEFINED;
@@ -132,6 +140,10 @@ METHOD(node_inner_html_set) {
132
140
  std::vector<xtgui::Node *> existing;
133
141
  for (const std::unique_ptr<xtgui::Node> &child : node->children) existing.push_back(child.get());
134
142
  for (xtgui::Node *child : existing) win->document->removeChild(node, child);
143
+ /* The old children are dropped here (only `removeChild` keeps a detached node
144
+ * alive), so forget them *and everything below them* before they are freed:
145
+ * a leftover slot would be a dangling pointer for the next handle lookup. */
146
+ for (xtgui::Node *child : existing) forget_subtree(win, child);
135
147
  std::unique_ptr<xtgui::Node> fragment = xtgui::xt_html_parse(html);
136
148
  for (std::unique_ptr<xtgui::Node> &child : fragment->children) node->append(std::move(child));
137
149
  mark_dirty(win);
@@ -64,10 +64,36 @@ void xt_gui_apply_icon(SDL_Window *window) {
64
64
  SDL_DestroySurface(surface);
65
65
  }
66
66
 
67
+ /* -- garbage collector roots ---------------------------------------------- */
68
+
69
+ /**
70
+ * GC root provider: the engine caches `xt_value`s in C++ containers that the
71
+ * mark-sweep collector cannot see — per-window element handles, element
72
+ * listeners, the document handle and the queued/currently-running
73
+ * `requestAnimationFrame` callbacks. Rooting the window object alone does not
74
+ * reach these (they are not its properties), so without this provider a
75
+ * collection frees values the engine still uses and the next frame or DOM read
76
+ * crashes. The process-wide AOT `<script>` registry registers its own
77
+ * provider (`xt_gui_script_gc_scan`).
78
+ */
79
+ static void xt_gui_gc_scan_roots(void) {
80
+ for (int i = 0; i < g_window_count; i++) {
81
+ const XtGuiWindow &win = g_windows[i];
82
+ xt_gc_mark_value(win.object);
83
+ xt_gc_mark_value(win.document_object);
84
+ for (const auto &entry : win.node_handles) xt_gc_mark_value(entry.second);
85
+ for (const auto &entry : win.node_listeners) {
86
+ for (const XtGuiNodeListener &listener : entry.second) xt_gc_mark_value(listener.fn);
87
+ }
88
+ for (const XtGuiAnimationFrame &frame : win.animation_frames) xt_gc_mark_value(frame.fn);
89
+ for (const XtGuiAnimationFrame &frame : win.running_frames) xt_gc_mark_value(frame.fn);
90
+ }
91
+ }
92
+
67
93
  /* -- initialisation ------------------------------------------------------- */
68
94
 
69
- static int ensure_init(void) {
70
- if (g_sdl_initialized) return 1;
95
+ static int ensure_init(void) { if (g_sdl_initialized) return 1;
96
+ xt_gc_register_root_provider(xt_gui_gc_scan_roots);
71
97
  if (!SDL_Init(SDL_INIT_VIDEO)) {
72
98
  fprintf(stderr, "xt_gui: SDL_Init failed: %s\n", SDL_GetError());
73
99
  return 0;
@@ -102,6 +128,12 @@ static XtGuiWindow *window_by_object(xt_value object) {
102
128
 
103
129
  using namespace guidev;
104
130
 
131
+ const char *xt_gui_driver(void) {
132
+ if (g_device == NULL) return "";
133
+ const char *driver = SDL_GetGPUDeviceDriver(g_device);
134
+ return driver != NULL ? driver : "";
135
+ }
136
+
105
137
  XtGuiWindow *xt_gui_window_from_this(xt_value thisValue) {
106
138
  return window_by_object(thisValue);
107
139
  }
@@ -206,6 +238,14 @@ extern "C" xt_value xt_gui_create_window(int32_t argc, xt_value *argv) {
206
238
  xt_object_set(object, xt_string_from_cstr("__xt_gui_index"), xt_number((double)index));
207
239
  xt_object_set(object, xt_string_from_cstr("__xt_gui_html"), xt_string_from_cstr(""));
208
240
  xt_object_set(object, xt_string_from_cstr("__xt_gui_events"), xt_object_new());
241
+ /* The engine owns a graph of values on the window's behalf — element
242
+ * handles (`node_handles`), their event listeners and the queued animation
243
+ * frame closures. Most of them are referenced from TypeScript too, but not
244
+ * all: a handle the script queried once and dropped (or one only reachable
245
+ * through an ancestor element) would otherwise be collected while the engine
246
+ * still uses it, which is a use-after-free. Rooting the window object makes
247
+ * everything hanging off it reachable. */
248
+ xt_gc_add_root(&record->object);
209
249
 
210
250
  return object;
211
251
  }
@@ -24,6 +24,9 @@ xt_value xt_gui_run(int32_t argc, xt_value *argv);
24
24
  xt_value xt_gui_quit(int32_t argc, xt_value *argv);
25
25
  /** `__registerScript(id, fn)`: record an AOT-compiled `<script>` body. */
26
26
  xt_value xt_register_script(int32_t argc, xt_value *argv);
27
+ /** Which SDL_GPU backend the device is using (`"vulkan"`, `"direct3d12"`,
28
+ * `"metal"`, …), or `""` before the device exists. */
29
+ const char *xt_gui_driver(void);
27
30
 
28
31
  #ifdef __cplusplus
29
32
  } /* extern "C" */
@@ -84,6 +87,11 @@ struct XtGuiWindow {
84
87
  int struct_dirty = 0;
85
88
  /** Callbacks queued for the next frame, plus the id counter. */
86
89
  std::vector<XtGuiAnimationFrame> animation_frames;
90
+ /** The batch currently running: its callbacks have been swapped out of
91
+ * `animation_frames`, and a collection triggered by one of them must still
92
+ * see the rest. Kept on the record (not a local) so the GC root provider can
93
+ * mark it. Empty except while `xt_gui_run_animation_frames` runs. */
94
+ std::vector<XtGuiAnimationFrame> running_frames;
87
95
  int next_animation_frame_id = 1;
88
96
  };
89
97
 
@@ -93,6 +101,10 @@ XtGuiWindow *xt_gui_window_from_this(xt_value thisValue);
93
101
  xt_value xt_gui_document_handle(XtGuiWindow *win);
94
102
  /** Node handle for `node` (created and cached on first use). */
95
103
  xt_value xt_gui_node_handle(XtGuiWindow *win, const xtgui::Node *node);
104
+ /** Forget a node's slot and handle because it is about to be destroyed (a
105
+ * removal that frees it, or `innerHTML = …` dropping the old children). Without
106
+ * this the node table would keep a dangling pointer. */
107
+ void xt_gui_node_handle_forget(XtGuiWindow *win, const xtgui::Node *node);
96
108
  /** Drop every cached handle/listener and bump the document generation. */
97
109
  void xt_gui_handles_reset(XtGuiWindow *win);
98
110
  /** Deliver `type` to element listeners along the propagation path. */
@@ -105,6 +117,9 @@ void xt_gui_flush_dom(XtGuiWindow *win);
105
117
  /** Run every registered `<script data-xt-id>` body found in the document, in
106
118
  * document order, passing `(windowHandle, documentHandle)`. */
107
119
  void xt_gui_run_scripts(XtGuiWindow *win);
120
+ /** GC root provider: mark every AOT-compiled `<script>` function in the
121
+ * process-wide registry. Registered with the collector by `xt_register_script`. */
122
+ void xt_gui_script_gc_scan(void);
108
123
  /** Shared prototype carrying the window methods. */
109
124
  xt_value xt_gui_window_proto(void);
110
125
  /** Release GPU claim + destroy the window and mark the record closed. */
@@ -165,6 +165,9 @@ void handle_event(const SDL_Event *event) {
165
165
  win->height = event->window.data2;
166
166
  if (win->document != NULL) win->document->restyle((float)win->width, (float)win->height);
167
167
  win->geometry.dirty = 1;
168
+ /* Web 'resize', so a document can re-measure on a window change in every
169
+ * host (a browser already fires one). */
170
+ xt_gui_emit(win, "resize");
168
171
  return;
169
172
  }
170
173
 
@@ -220,15 +223,18 @@ void handle_event(const SDL_Event *event) {
220
223
 
221
224
  void xt_gui_run_animation_frames(XtGuiWindow *win, double timestamp_ms) {
222
225
  if (win == NULL || win->animation_frames.empty()) return;
223
- /* Swap the queue out first: a callback that calls requestAnimationFrame
224
- * again schedules for the *next* frame, not this one. */
225
- std::vector<XtGuiAnimationFrame> frames;
226
+ /* Swap the queue into a member, not a local: the GC root provider marks
227
+ * `running_frames` too, so a collection triggered while one callback runs
228
+ * cannot free the rest of the batch. Re-queueing from a callback lands in
229
+ * `animation_frames` and runs on the *next* frame, not this one. */
230
+ std::vector<XtGuiAnimationFrame> &frames = win->running_frames;
226
231
  frames.swap(win->animation_frames);
227
232
  for (const XtGuiAnimationFrame &frame : frames) {
228
233
  if (!win->open) break;
229
234
  xt_value arg = xt_number(timestamp_ms);
230
235
  xt_call_with_this(frame.fn, win->object, 1, &arg);
231
236
  }
237
+ frames.clear();
232
238
  }
233
239
 
234
240
  void xt_gui_render_window(XtGuiWindow *win) {
@@ -244,6 +250,10 @@ void xt_gui_render_window(XtGuiWindow *win) {
244
250
  /* Animation-frame callbacks run before layout, so any DOM mutation they make
245
251
  * is picked up by this same frame. */
246
252
  xt_gui_run_animation_frames(win, now);
253
+ /* A callback may have closed the window (directly or through the
254
+ * auto-close guard): `xt_gui_quit_window` released the document and the GPU
255
+ * geometry, so there is nothing left to lay out or paint. */
256
+ if (!win->open) return;
247
257
  if (win->document != nullptr && win->document->advance(delta)) win->geometry.dirty = 1;
248
258
 
249
259
  /* A DOM mutation from a handler invalidates the tree: restyle/relayout
@@ -96,6 +96,12 @@ void LayoutTree::compute(const Node *root, const std::unordered_map<const Node *
96
96
  root_->node = root;
97
97
  root_->style = &root_style_;
98
98
  root_->display = Display::Block;
99
+ /* The initial containing block: absolute boxes with no positioned ancestor
100
+ * resolve their insets against the viewport. */
101
+ root_->cb_x = 0;
102
+ root_->cb_y = 0;
103
+ root_->cb_width = viewportWidth;
104
+ root_->cb_height = viewportHeight;
99
105
  if (root != nullptr) {
100
106
  if (root->type == NodeType::Document) {
101
107
  for (const std::unique_ptr<Node> &child : root->children) {
@@ -59,6 +59,10 @@ struct LayoutBox {
59
59
  float content_x = 0, content_y = 0, content_width = 0, content_height = 0;
60
60
  float baseline = 0;
61
61
 
62
+ /* The padding box of the nearest positioned ancestor (the viewport when there
63
+ * is none): the containing block `position: absolute` resolves against. */
64
+ float cb_x = 0, cb_y = 0, cb_width = 0, cb_height = 0;
65
+
62
66
  LayoutBox *parent = nullptr;
63
67
  std::vector<std::unique_ptr<LayoutBox>> children;
64
68
  };