xbintsc 0.3.46 → 0.3.61

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 (131) hide show
  1. package/AGENTS.md +95 -0
  2. package/README.md +25 -0
  3. package/README.zh-CN.md +23 -0
  4. package/dist/src/cli/hints.d.ts +54 -0
  5. package/dist/src/cli/hints.js +165 -0
  6. package/dist/src/cli/hints.js.map +1 -0
  7. package/dist/src/cli/main.js +73 -9
  8. package/dist/src/cli/main.js.map +1 -1
  9. package/dist/src/codegen/generator/tables.d.ts +26 -0
  10. package/dist/src/codegen/generator/tables.js +64 -12
  11. package/dist/src/codegen/generator/tables.js.map +1 -1
  12. package/dist/src/diagnostics/source-text.d.ts +22 -0
  13. package/dist/src/diagnostics/source-text.js +76 -0
  14. package/dist/src/diagnostics/source-text.js.map +1 -0
  15. package/dist/src/driver/bundler/graph.js +2 -1
  16. package/dist/src/driver/bundler/graph.js.map +1 -1
  17. package/dist/src/driver/compiler.js +28 -3
  18. package/dist/src/driver/compiler.js.map +1 -1
  19. package/dist/src/driver/config.d.ts +7 -0
  20. package/dist/src/driver/config.js.map +1 -1
  21. package/dist/src/driver/win-entry.d.ts +21 -0
  22. package/dist/src/driver/win-entry.js +52 -0
  23. package/dist/src/driver/win-entry.js.map +1 -0
  24. package/dist/src/lexer/scanner/strings.js +16 -3
  25. package/dist/src/lexer/scanner/strings.js.map +1 -1
  26. package/dist/tests/cli/hints.test.d.ts +9 -0
  27. package/dist/tests/cli/hints.test.js +143 -0
  28. package/dist/tests/cli/hints.test.js.map +1 -0
  29. package/dist/tests/cli/main.test.js +6 -4
  30. package/dist/tests/cli/main.test.js.map +1 -1
  31. package/dist/tests/codegen/llvm.test.js +17 -2
  32. package/dist/tests/codegen/llvm.test.js.map +1 -1
  33. package/dist/tests/e2e/gui-example-browser.test.d.ts +12 -0
  34. package/dist/tests/e2e/gui-example-browser.test.js +164 -0
  35. package/dist/tests/e2e/gui-example-browser.test.js.map +1 -0
  36. package/dist/tests/e2e/gui-example.test.d.ts +14 -0
  37. package/dist/tests/e2e/gui-example.test.js +76 -0
  38. package/dist/tests/e2e/gui-example.test.js.map +1 -0
  39. package/dist/tests/e2e/gui-helpers.d.ts +39 -4
  40. package/dist/tests/e2e/gui-helpers.js +38 -9
  41. package/dist/tests/e2e/gui-helpers.js.map +1 -1
  42. package/dist/tests/e2e/gui-layout.test.js +155 -0
  43. package/dist/tests/e2e/gui-layout.test.js.map +1 -1
  44. package/dist/tests/e2e/gui-lifecycle.test.js +10 -1
  45. package/dist/tests/e2e/gui-lifecycle.test.js.map +1 -1
  46. package/dist/tests/e2e/gui-paint.test.js +44 -4
  47. package/dist/tests/e2e/gui-paint.test.js.map +1 -1
  48. package/dist/tests/e2e/gui-script.test.js +8 -4
  49. package/dist/tests/e2e/gui-script.test.js.map +1 -1
  50. package/dist/tests/helpers.js +3 -2
  51. package/dist/tests/helpers.js.map +1 -1
  52. package/dist/tests/lexer/strings.test.js +14 -2
  53. package/dist/tests/lexer/strings.test.js.map +1 -1
  54. package/doc/DESIGN.md +117 -0
  55. package/doc/ai/README.md +63 -0
  56. package/doc/ai/build-recipe.md +137 -0
  57. package/doc/ai/cli.md +142 -0
  58. package/doc/ai/contributing.md +196 -0
  59. package/doc/ai/extensions.md +148 -0
  60. package/doc/ai/language-support.md +152 -0
  61. package/doc/ai/troubleshooting.md +163 -0
  62. package/doc/ai/zh-CN/README.md +56 -0
  63. package/doc/ai/zh-CN/build-recipe.md +132 -0
  64. package/doc/ai/zh-CN/cli.md +127 -0
  65. package/doc/ai/zh-CN/contributing.md +173 -0
  66. package/doc/ai/zh-CN/extensions.md +139 -0
  67. package/doc/ai/zh-CN/language-support.md +147 -0
  68. package/doc/ai/zh-CN/troubleshooting.md +150 -0
  69. package/doc/gui-scripts.md +350 -0
  70. package/doc/gui.md +752 -0
  71. package/doc/icon.md +265 -0
  72. package/doc/implemented.md +373 -0
  73. package/doc/node-implemented.md +588 -0
  74. package/doc/node-unimplemented.md +167 -0
  75. package/doc/post/announce.md +43 -0
  76. package/doc/requirements.md +145 -0
  77. package/doc/unimplemented.md +286 -0
  78. package/doc/xbintsc.config.schema.json +71 -0
  79. package/doc/zh-CN/DESIGN.md +104 -0
  80. package/doc/zh-CN/gui-scripts.md +329 -0
  81. package/doc/zh-CN/gui.md +656 -0
  82. package/doc/zh-CN/icon.md +241 -0
  83. package/doc/zh-CN/implemented.md +365 -0
  84. package/doc/zh-CN/node-implemented.md +533 -0
  85. package/doc/zh-CN/node-unimplemented.md +141 -0
  86. package/doc/zh-CN/plan-require-node-modules.md +284 -0
  87. package/doc/zh-CN/post/announce.md +47 -0
  88. package/doc/zh-CN/requirements.md +134 -0
  89. package/doc/zh-CN/unimplemented.md +247 -0
  90. package/llms.txt +45 -0
  91. package/package.json +4 -1
  92. package/runtime/ext_gui/dom_api.cpp +19 -0
  93. package/runtime/ext_gui/dom_api_node.cpp +4 -1
  94. package/runtime/ext_gui/dom_api_node_read.cpp +12 -0
  95. package/runtime/ext_gui/gui.cpp +45 -3
  96. package/runtime/ext_gui/gui_engine.h +15 -0
  97. package/runtime/ext_gui/gui_events.cpp +10 -3
  98. package/runtime/ext_gui/layout.cpp +6 -0
  99. package/runtime/ext_gui/layout.h +4 -0
  100. package/runtime/ext_gui/layout_flex.cpp +13 -5
  101. package/runtime/ext_gui/layout_flow.cpp +134 -4
  102. package/runtime/ext_gui/layout_flow_internal.h +41 -2
  103. package/runtime/ext_gui/layout_inline.cpp +11 -3
  104. package/runtime/ext_gui/paint.cpp +6 -0
  105. package/runtime/ext_gui/renderer.cpp +13 -11
  106. package/runtime/ext_gui/renderer_image.cpp +12 -8
  107. package/runtime/ext_gui/renderer_shaders.h +131 -4
  108. package/runtime/ext_gui/renderer_shaders_data.h +1809 -0
  109. package/runtime/ext_gui/renderer_text.cpp +12 -8
  110. package/runtime/ext_gui/script.cpp +12 -2
  111. package/runtime/ext_gui/shaders.hlsl +98 -0
  112. package/runtime/ext_gui/spirv/fill.frag +19 -0
  113. package/runtime/ext_gui/spirv/fill.vert +42 -0
  114. package/runtime/ext_gui/spirv/image.frag +16 -0
  115. package/runtime/ext_gui/spirv/quad.vert +30 -0
  116. package/runtime/ext_gui/spirv/text.frag +16 -0
  117. package/runtime/ext_gui/text.cpp +14 -0
  118. package/runtime/ext_gui/window.cpp +12 -0
  119. package/runtime/rt.h +10 -0
  120. package/scripts/build-gui-shaders.mjs +204 -0
  121. package/scripts/build-gui.ts +35 -0
  122. package/scripts/check-file-length.ts +5 -1
  123. package/src/cli/hints.ts +194 -0
  124. package/src/cli/main.ts +82 -9
  125. package/src/codegen/generator/tables.ts +60 -14
  126. package/src/diagnostics/source-text.ts +78 -0
  127. package/src/driver/bundler/graph.ts +2 -1
  128. package/src/driver/compiler.ts +29 -3
  129. package/src/driver/config.ts +7 -0
  130. package/src/driver/win-entry.ts +59 -0
  131. package/src/lexer/scanner/strings.ts +16 -3
package/doc/gui.md ADDED
@@ -0,0 +1,752 @@
1
+ # xbintsc GUI extension (self-hosted HTML/CSS renderer)
2
+
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
6
+ in place, and the engine *paints*: it builds a display list of rectangles, images
7
+ and shaped text runs and renders them through SDL_GPU. Input events are hit
8
+ tested and delivered to native TS handlers, `:hover`/`:focus` are matched
9
+ dynamically, `<img>` is sized from its intrinsic dimensions and drawn from a
10
+ texture, and CSS transitions animate paint properties. **M8** adds an interactive
11
+ DOM: element handles with stable identity, mutation (`appendChild`, `textContent`,
12
+ `classList`, `style`, …) and element-level events with capture/bubbling.
13
+ **M9** compiles `<script>` bodies ahead of time (inline and `<script src>`) — see
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.
17
+ This document records the locked decisions, the architecture, the milestone plan
18
+ and the current progress of a cross-platform GUI extension that renders an
19
+ HTML/CSS UI with its own GPU-accelerated engine.
20
+
21
+ ## Goals
22
+
23
+ 1. **Cross-platform GUI** from TypeScript compiled by xbintsc to a native
24
+ binary: macOS, Linux, Windows.
25
+ 2. **HTML5/CSS rendering** with a **self-written** engine (parser, cascade,
26
+ layout, paint), not a system WebView or an embedded browser.
27
+ 3. **GPU accelerated** — hardware rasterization/compositing is a hard
28
+ requirement, not an optimization.
29
+ 4. **Multiple windows.**
30
+ 5. The engine must not compromise xbintsc's nature as a binary compiler: the
31
+ core compiler, lexer, parser, binder and code generator must stay
32
+ platform-agnostic and never grow GUI branches.
33
+
34
+ ## Non-goals (for now)
35
+
36
+ - Executing **runtime** page `<script>` (scripts fetched over the network or
37
+ created dynamically). **Compile-time** scripts are AOT-compiled by xbintsc and
38
+ do run — see `doc/gui-scripts.md`. Logic may also live in native TS called
39
+ back through `xt_call_with_this`.
40
+ - A JS engine (QuickJS/V8/...). Explicitly out of scope; there is no runtime
41
+ interpreter/JIT, so `<script>` bodies are compiled ahead of time.
42
+ - Full web compatibility / a browser. We implement a practical HTML/CSS subset.
43
+
44
+ ## Locked decisions
45
+
46
+ | # | Decision |
47
+ | --- | --- |
48
+ | 1 | **No page JS engine.** Behaviour is native TS, called back through `xt_call_with_this`. `<script>` bodies are **AOT-compiled** by xbintsc itself (no interpreter) — see `doc/gui-scripts.md`. |
49
+ | 2 | Third-party **low-level** libraries are allowed (GPU backend, text shaping, image decode). HTML/CSS parsing + layout + paint scheduling are self-written. |
50
+ | 3 | **GPU acceleration is mandatory.** |
51
+ | 4 | The engine is **not self-hosted** (it is C/C++, not TS), but must not affect the compiler's platform-agnostic design. Delivered as a per-platform prebuilt archive linked via `nativeObjects`. |
52
+ | 5 | The **event loop is generic**: the runtime exposes a generic main-loop hook and a poll primitive; nothing GUI-specific enters `runtime/`. |
53
+ | 6 | **Multiple windows** are supported by the core object model. |
54
+ | 7 | Third-party low-level libs are **statically linked into `gui.a`** so releases stay self-contained; only OS frameworks are added at link time. |
55
+
56
+ ## Architecture
57
+
58
+ ```
59
+ TypeScript (compiled by xbintsc to native code)
60
+ │ import { createWindow, run } from "gui"
61
+ ▼
62
+ gui extension bindings (src/extensions/gui)
63
+ │ xt_gui_* symbols (uniform (argc, argv) ABI)
64
+ ▼
65
+ gui.a ── the self-hosted engine (C/C++)
66
+ ┌───────────────┬──────────────────┬───────────────────┐
67
+ ▼ ▼ ▼ ▼
68
+ HTML parser CSS cascade + Layout GPU compositor
69
+ (subset) selector match (block/inline/flex) (SDL_GPU: Metal/
70
+ Vulkan/D3D12)
71
+ │
72
+ ▼
73
+ windowing + input (SDL3) ── multiple windows
74
+ text (HarfBuzz + FreeType) ── glyph atlas / SDF
75
+ images (stb_image)
76
+ ```
77
+
78
+ The engine never talks to the compiler. The compiler only sees an `Extension`
79
+ with `nativeObjects()`, `linkerFlags()` and `modules()`.
80
+
81
+ ## Stack (confirmed)
82
+
83
+ | Concern | Choice | Why |
84
+ | --- | --- | --- |
85
+ | Window + input + multi-window | **SDL3** | cross-platform windows, HiDPI, IME, clipboard, DnD, close/hide events |
86
+ | GPU | **SDL_GPU** (SDL3) | one render path over Metal / Vulkan / D3D12; avoids three backends |
87
+ | Text shaping | **HarfBuzz** | correct complex-script shaping |
88
+ | Glyph raster | **FreeType** | glyph outlines → GPU atlas / SDF |
89
+ | Images | **stb_image** (vendored header) | single header to start |
90
+
91
+ Pinned versions: SDL3 `release-3.2.10`, FreeType `2.13.3`, HarfBuzz `10.1.0`
92
+ (overridable with `SDL3_TAG` / `FREETYPE_VERSION` / `HARFBUZZ_VERSION`).
93
+
94
+ Alternative if SDL3 is rejected: **GLFW + OpenGL 3.3** (simpler, but OpenGL is
95
+ deprecated on macOS and gives no modern GPU abstraction). **wgpu-native** if the
96
+ engine were Rust.
97
+
98
+ > These vendored libraries are linked **statically into `gui.a`** so a released
99
+ > xbintsc stays "download and run"; only OS frameworks are added by
100
+ > `linkerFlags()`.
101
+
102
+ ## TS-facing API
103
+
104
+ ```ts
105
+ import { createWindow, run, quit } from "gui";
106
+
107
+ const win = createWindow({ title: "Demo", width: 900, height: 600 });
108
+ win.setBackground("#14161c");
109
+ win.loadHTML(INDEX_HTML); // parses HTML/CSS and computes styles
110
+ win.on("ready", () => console.log("first frame presented"));
111
+ win.on("close", () => console.log("window closed"));
112
+
113
+ run(); // drives the main loop until all windows close
114
+ ```
115
+
116
+ Methods implemented on a window handle: `setTitle` / `setSize` / `loadHTML` /
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). Events emitted:
120
+ `ready` (after the first presented frame), `load`, `close`, and the input events
121
+ `mousemove`, `mousedown`, `mouseup`, `click`, `wheel`, `keydown`, `keyup`.
122
+
123
+ On Windows a GUI program is linked as a **Windows-subsystem** executable, so
124
+ launching it does not flash a console window. That also means stdout/stderr go
125
+ nowhere when it is started from Explorer; set `app.console: true` in
126
+ `xbintsc.config.json` to keep the console subsystem (useful while debugging).
127
+
128
+ Input handlers receive a single payload object (lifecycle handlers receive none):
129
+
130
+ ```ts
131
+ win.on("click", (e) => console.log(e.x, e.y, e.button, e.target));
132
+ win.on("wheel", (e) => console.log(e.deltaX, e.deltaY));
133
+ win.on("keydown", (e) => console.log(e.key, e.code, e.ctrl, e.shift, e.alt, e.meta));
134
+ ```
135
+
136
+ | Field | Events | Meaning |
137
+ | --- | --- | --- |
138
+ | `x`, `y` / `clientX`, `clientY` | pointer, wheel | viewport-relative logical pixels |
139
+ | `button` | pointer | `0` left, `1` middle, `2` right (`-1` for move) |
140
+ | `clicks` | pointer | click count reported by the OS |
141
+ | `deltaX`, `deltaY` | wheel | scroll amount (`deltaY` positive = down) |
142
+ | `key`, `code` | keyboard | key name and physical scancode name |
143
+ | `repeat`, `ctrl`, `shift`, `alt`, `meta` | keyboard | modifiers |
144
+ | `target` | pointer, wheel | deepest element under the point as a CSS-like descriptor (`div#main.card`), or absent |
145
+
146
+ ### Implemented HTML/CSS subset (M3a)
147
+
148
+ **HTML parser** (`runtime/ext_gui/dom.{h,cpp}`): tags/attributes/text,
149
+ comments and doctype skipped, entity decoding (named + numeric/hex), void
150
+ elements, raw-text elements (`<style>`/`<script>`), and the common implicit
151
+ close rules (`li`, `dt`/`dd`, `option`, `p`, headings, table cells/rows).
152
+
153
+ **CSS parser** (`runtime/ext_gui/css.{h,cpp}`): comments and at-rules skipped
154
+ (for now), rules with multiple selectors, and declarations. Selectors: type,
155
+ `.class`, `#id`, attribute (`=`, `~=`, `|=`, `^=`, `$=`, `*=`), the four
156
+ combinators (descendant, child, adjacent and general sibling) and the
157
+ pseudo-classes `:first-child`, `:last-child`, `:only-child`, `:empty`,
158
+ `:root`, `:not(...)`, `:nth-child(an+b)`, `:disabled`, `:checked`, plus the
159
+ stateful `:hover` and `:focus` (see *Implemented input*). Values:
160
+ lengths (`px`, `%`, `em`, `rem`, `vw`, `vh`, `pt`, `pc`, `in`, `cm`, `mm`, `q`),
161
+ colors (hex, `rgb()`/`rgba()`, a named subset), numbers, keywords and shorthands
162
+ (`margin`/`padding`/`border`/`flex`).
163
+
164
+ **Cascade & computed style** (`runtime/ext_gui/style.{h,cpp}`): a small built-in
165
+ UA stylesheet, author rules sorted by `(!important, specificity, source order)`,
166
+ then inline `style=""` (highest specificity, but non-`!important` inline loses to
167
+ `!important`), plus CSS inheritance of the text properties. Relative lengths are
168
+ kept unresolved until layout, except `font-size` (resolved against the *parent*
169
+ font size) and `line-height`.
170
+
171
+ ### Diagnostics
172
+
173
+ So the HTML/CSS/paint pipeline is testable without a GPU, a window handle
174
+ exposes read-only hooks:
175
+
176
+ ```ts
177
+ win.computedStyle(selector, property) // e.g. ("#main", "width") -> "60%"
178
+ win.queryCount(selector) // number of matching elements
179
+ win.getBoundingClientRect(selector) // { x, y, width, height } (border box)
180
+ win.documentTree() // serialized DOM (debugging)
181
+ win.layoutTree() // serialized layout boxes (debugging)
182
+ win.paintCount() // number of shapes in the display list
183
+ win.paintList() // serialized display list (debugging)
184
+ win.measureText(text, fontSize?, family?) // shaped advance width in pixels
185
+ win.fontMetrics(fontSize?, family?) // { ascent, descent, lineHeight, ready }
186
+ win.hitTest(x, y) // deepest element descriptor, or ""
187
+ win.sendEvent(type, options?) // synthesise input (testing)
188
+ win.advance(ms) // step the CSS transition clock (testing)
189
+ ```
190
+
191
+ They are used by `tests/e2e/gui-*.test.ts` to assert parsing, selector matching,
192
+ specificity, inheritance, `!important` and layout geometry. They will stay useful
193
+ afterwards for debugging.
194
+
195
+ ### Interactive DOM (M8)
196
+
197
+ `win.document` returns the document handle; element handles have stable identity
198
+ and read/write properties, attributes, traversal and geometry:
199
+
200
+ ```ts
201
+ const doc = win.document;
202
+ const box = doc.querySelector("#box");
203
+ const inner = doc.querySelector("#inner");
204
+ inner.textContent = "hi"; // write
205
+ inner.classList.add("hot");
206
+ inner.style.setProperty("color", "#0f0");
207
+ inner.setAttribute("data-role", "lead");
208
+ console.log(inner.id, inner.tagName, doc.querySelector("#box") === box);
209
+
210
+ const created = doc.createElement("div");
211
+ created.textContent = "added";
212
+ box.appendChild(created);
213
+ box.removeChild(created);
214
+
215
+ inner.addEventListener("click", (e) => console.log(e.target.id, e.currentTarget.id));
216
+ inner.click(); // synthesise a click at the element
217
+ console.log(box.offsetWidth, box.offsetHeight); // rounded border box
218
+ console.log(box.contains(inner)); // descendant test
219
+ ```
220
+
221
+ Mutations mark the document dirty; the engine restyles + relayouts lazily (before
222
+ the next read or frame). `inner = …`/`innerHTML = …` do **not** run scripts.
223
+ Element events support capture and bubble phases, `stopPropagation`, `once`, and
224
+ bubble up to `document`/`window`. The legacy `win.on(type, fn)` payload keeps its
225
+ **string** `e.target` (`div#id.class`); the element `Event.target` is a handle
226
+ whose descriptor matches the same string.
227
+
228
+ ### AOT scripts (M9)
229
+
230
+ Import an `.html` file that contains inline `<script lang="ts">` bodies; the
231
+ loader compiles each body into a native function and `win.loadHTML(page)` runs
232
+ them once the document is parsed (no JavaScript engine, no runtime `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
+ The two parameters (`window`, `document`) are ordinary function arguments, so
253
+ script locals need no global-object machinery. External scripts work too:
254
+ `<script src="./counter.ts">` is read at compile time, its imports are hoisted
255
+ (rewritten to resolve from the HTML file) and its body is wrapped the same way —
256
+ so a script module can `import` helpers and still see `document`. Scripts are
257
+ **compile-time assets**: HTML created at runtime (`innerHTML`, fetched over the
258
+ network) never executes, and `src` URLs (`https://…`, `data:…`) are ignored.
259
+ Everything runs in document order after parsing (effectively deferred). See
260
+ `doc/gui-scripts.md` for the full design.
261
+
262
+ ### Animation frames (M10)
263
+
264
+ `requestAnimationFrame` runs a callback once on the next frame; the callback
265
+ receives the frame timestamp (ms) and may mutate the DOM, which is restyled and
266
+ repainted in the same frame. Re-queue from inside the callback to animate:
267
+
268
+ ```ts
269
+ const win = createWindow({ title: "anim", width: 320, height: 240 });
270
+ let n = 0;
271
+ const tick = (t: number) => {
272
+ win.document.getElementById("label").textContent = String(n++);
273
+ if (n < 120) win.requestAnimationFrame(tick); // id returned; cancelAnimationFrame(id) drops it
274
+ };
275
+ win.on("ready", () => win.requestAnimationFrame(tick));
276
+ win.loadHTML("<div id='label'>0</div>");
277
+ run();
278
+ ```
279
+
280
+ ### Implemented layout (M3)
281
+
282
+ `runtime/ext_gui/layout.{h,cpp}` turns the styled DOM into a `LayoutBox` tree
283
+ with absolute (viewport-relative) geometry:
284
+
285
+ - **Block flow** — block-level children stack vertically (no margin collapsing
286
+ yet); `display: none` generates no box; `width: auto` fills the containing
287
+ block, `height: auto` wraps the content. The box model (margin/padding/border)
288
+ is resolved, including percentages against the containing block width.
289
+ - **Inline flow** — consecutive inline-level children form an anonymous inline
290
+ formatting context with greedy, word-based line breaking, `text-align` and
291
+ `line-height`. Inline elements get the union of their descendants' geometry;
292
+ `display: inline-block` is laid out atomically with a shrink-to-fit width.
293
+ Each text fragment remembers the run it covers, so paint can shape it. Text is
294
+ measured with the HarfBuzz/FreeType stack (see *Implemented text*).
295
+ - **Flexbox** — single-line `row`/`column` (and the `-reverse` variants) with
296
+ `gap`, `flex-basis`/`flex-grow`/`flex-shrink`, `justify-content` and
297
+ `align-items` (including `stretch` when the cross size is definite). Item
298
+ margins are part of the item's outer size on both axes, so a negative
299
+ `margin-top` pulls an item up instead of being ignored.
300
+ - **Positioning** — `position: relative` offsets a box (and its subtree) by
301
+ `top`/`right`/`bottom`/`left` without changing the space it reserved;
302
+ `position: absolute` (and `fixed`, which resolves against the viewport) takes
303
+ the box out of flow and places it against the padding box of the nearest
304
+ positioned ancestor — the viewport when there is none. An `auto` inset keeps
305
+ the static position on that axis, an `auto` width is shrink-to-fit unless both
306
+ `left` and `right` are set (then it fills the space between them), and
307
+ percentages resolve against the containing block. There is no `z-index`
308
+ stacking yet: painting stays in document order.
309
+ Note the consequence of that shrink-to-fit rule: an absolutely positioned box
310
+ with `width: auto` and no explicit size gets **no** available width for its own
311
+ inline content (its preferred width comes from its text or its non-absolute
312
+ children only), so inline runs inside it are laid out at zero available width
313
+ and each one wraps onto its own line. Give such a box an explicit `width`, or
314
+ place its children with `left`/`width` (as
315
+ `examples/gui/pelican-bike` does for its sprite runs), instead of relying on
316
+ line flow inside it.
317
+
318
+ Not yet implemented: margin collapsing, multi-line flex wrapping, `z-index`
319
+ stacking, `overflow` clipping and floats.
320
+
321
+ **Document** (`runtime/ext_gui/document.{h,cpp}`): owns the DOM tree, gathers
322
+ `<style>` text into one stylesheet, computes styles and layout for a viewport and
323
+ offers `querySelector`/`querySelectorAll`/`styleOf`/`boxOf`.
324
+
325
+ ### Implemented paint (M4a/M4b)
326
+
327
+ `runtime/ext_gui/paint.{h,cpp}` walks the layout tree in painter's order and
328
+ emits a backend-agnostic `DisplayList` with two parallel lists: **rectangles**
329
+ (backgrounds and four solid border edges, with `border-radius`) and **text runs**
330
+ (each carrying its text, colour and resolved `FontSpec`). Keeping them separate
331
+ lets the renderer draw all rectangles, then all text on top, with one draw call
332
+ per list. `DisplayList::dump()` feeds `paintList()`/`paintCount()`.
333
+
334
+ `opacity` is honoured only at its extremes: a box whose computed `opacity` is
335
+ `0` (and its whole subtree) paints nothing, and anything above `0` paints fully
336
+ opaque. A shape carries its colour's alpha only, so there is no compositing of a
337
+ partly transparent box — the `opacity` transitions the CSS parser accepts are
338
+ therefore not visible either. Hiding and revealing a layer with `opacity: 0` on
339
+ the hidden state is what `examples/gui/pelican-bike`'s game-over overlay does.
340
+
341
+ `runtime/ext_gui/renderer.{h,cpp}` turns that list into two batched vertex
342
+ buffers per window (one for shapes, one for glyph quads) drawn through two
343
+ SDL_GPU graphics pipelines:
344
+
345
+ - The shared pipelines are created lazily from the device's supported shader
346
+ format. Each of SDL_GPU's three backends consumes a different binary and none
347
+ of them compiles GLSL/HLSL at runtime, so the engine ships all three and
348
+ `selectShader` (`renderer_shaders.h`) picks the one the device accepts:
349
+ **MSL** for Metal (compiled by SDL from the embedded source),
350
+ **SPIR-V** for Vulkan (`runtime/ext_gui/spirv/*.{vert,frag}`, compiled by
351
+ `glslc`), and **DXIL** for Direct3D 12 (`shaders.hlsl`, compiled by `dxc`).
352
+ `scripts/build-gui-shaders.mjs` regenerates the embedded blobs and the result
353
+ is committed, so nothing extra is needed to build the engine.
354
+ The descriptor bindings differ per format — SPIR-V follows what
355
+ `SDL_gpu_vulkan.c` builds (uniforms `set=1, binding=0`, samplers
356
+ `set=2, binding=0`, textures `set=2, binding=1`) — and glslc names every entry
357
+ point `main` where MSL/DXIL keep the descriptive names.
358
+ Note that SDL 3.2.10's Direct3D 12 backend cannot create a graphics pipeline
359
+ whose shaders declare a uniform buffer (it fails with `E_INVALIDARG`); the
360
+ engine passes the viewport that way, so the DXIL path is built but not usable
361
+ until that is resolved upstream.
362
+ - A **rounded-rectangle distance field** in the shape fragment shader gives
363
+ antialiased fills; the vertex carries `position`, `local`, `half extents`,
364
+ `radius` and colour, and a viewport-size push constant does the projection.
365
+ Alpha blending is enabled.
366
+ - The text pipeline samples a **single shared grayscale glyph atlas**
367
+ (`R8_UNORM`, 2048², shelf-packed, LINEAR filtering) and draws each glyph as a
368
+ textured quad (`position`, `uv`, colour), modulating alpha by the coverage.
369
+ - Geometry is uploaded only when the document or viewport changes
370
+ (`geometry.dirty`), so steady-state frames are bind-and-draw.
371
+
372
+ The window background (`setBackground`) is the render-pass clear colour.
373
+
374
+ Still to do in M4: gradients.
375
+
376
+ ### Implemented input (M5)
377
+
378
+ `LayoutTree::hitTest` returns the deepest box containing a point (probing later
379
+ siblings first so the topmost element wins), and `xt_dom_describe` turns the
380
+ element into the `div#id.class` descriptor carried by event payloads.
381
+
382
+ - SDL pointer/wheel/key events are routed to the owning window, hit tested, and
383
+ delivered to the handlers registered with `on`. `mousedown` also moves focus.
384
+ - `:hover` matches the hovered element **and its ancestors** (so hovering a child
385
+ lights up its parents); `:focus` matches the focused element. When either
386
+ changes, `XtDocument::setHover`/`setFocus` recompute styles and layout and mark
387
+ the window's geometry dirty, so the change is painted on the next frame.
388
+ - `win.hitTest(x, y)` and `win.sendEvent(type, options)` expose hit testing and
389
+ synthetic input so the whole path is testable headlessly (the e2e suite drives
390
+ clicks, wheels and keys without a real mouse).
391
+
392
+ Still to do for full input: text selection, drag, IME and clipboard.
393
+
394
+ ### Implemented images (M6a)
395
+
396
+ `runtime/ext_gui/image.{h,cpp}` wraps the vendored **stb_image** header
397
+ decoding PNG/JPEG/BMP/GIF/TGA to RGBA8, with a decode cache and a header-only
398
+ size cache (`stbi_info`). Paths accept a `file://` prefix and percent-encoding.
399
+
400
+ - `<img>` is a replaced element (`display: inline-block`): layout gives it the
401
+ CSS size when set, otherwise the intrinsic pixel size, and preserves the
402
+ aspect ratio when only one axis is constrained. The intrinsic size is read
403
+ from the file header (no full decode) while building the box tree.
404
+ - Paint emits one `PaintImage` per `<img>`; the renderer decodes/uploads each
405
+ unique `src` to an RGBA texture (cached by path) and draws textured quads,
406
+ batching consecutive quads that share a texture.
407
+
408
+ Still to do for images: CSS `background-image: url(...)`, `data:` URIs,
409
+ `object-fit` and 9-slice borders.
410
+
411
+ ### Implemented transitions (M6b)
412
+
413
+ `XtDocument` runs CSS transitions between the *target* computed styles (the
414
+ cascade for the current `:hover`/`:focus` state) and the *displayed* styles used
415
+ for layout and paint. When a state change alters a transitioned property, a
416
+ running transition is recorded and re-applied every frame until it finishes;
417
+ the engine advances the clock with real frame deltas (`XtDocument::advance`),
418
+ and `win.advance(ms)` lets tests step it deterministically.
419
+
420
+ - Supported properties: `background-color`, `color`, `border-color`,
421
+ `border-radius`; `transition: all` covers them. Structural/layout-affecting
422
+ properties are not animated yet (that would relayout every frame).
423
+ - Both the `transition` shorthand and the `transition-property` / `-duration` /
424
+ `-delay` / `-timing-function` longhands are parsed; time values accept `s` and
425
+ `ms`; timing functions are `linear`, `ease`, `ease-in`, `ease-out` and
426
+ `ease-in-out` (`ease` is a smoothstep approximation).
427
+ - Retargeting mid-flight starts a new transition from the current interpolated
428
+ value, so reversing a hover animates smoothly from wherever it was.
429
+ - `computedStyle()` reports the displayed (interpolated) value, so transitions
430
+ are directly observable in tests.
431
+
432
+ Still to do for animation: `@keyframes` animations and `cubic-bezier(...)`.
433
+
434
+ ### Implemented text stack (M4b)
435
+
436
+ `runtime/ext_gui/text.{h,cpp}` wraps **HarfBuzz** (shaping) and **FreeType**
437
+ (metrics + eventual rasterisation). Both are built statically and linked into
438
+ `gui.a` by `scripts/build-gui.ts`.
439
+
440
+ - Fonts are resolved from well-known system paths (Helvetica/Arial on macOS,
441
+ DejaVu/Liberation on Linux, Segoe UI/Arial on Windows), overridable with
442
+ `XT_GUI_FONT` (and `XT_GUI_FONT_MONO`), and cached per `(family class, size)`.
443
+ Only regular upright faces are used for now; weight/italic selection is a
444
+ later refinement.
445
+ - The default candidates prefer a **CJK-capable** face (Microsoft YaHei on
446
+ Windows, PingFang on macOS, Noto Sans CJK/WenQuanYi on Linux) before the
447
+ Latin-only ones, because a run is shaped with a *single* face: without this,
448
+ Chinese/Japanese/Korean text renders as blank or `.notdef` boxes. Set
449
+ `XT_GUI_FONT` to a Latin-only face to get its Latin design instead (at the
450
+ cost of CJK coverage).
451
+ - `xt_text_measure_width` shapes the run with HarfBuzz (so kerning and
452
+ ligatures are honoured), `xt_text_metrics` returns FreeType's ascent /
453
+ descent / normal line height. When no font file can be found the module falls
454
+ back to a deterministic per-byte approximation, so layout still works.
455
+ - `xt_text_shape_run` returns positioned glyphs and `xt_text_rasterize` renders
456
+ an 8-bit bitmap; the renderer packs those into the atlas. On HiDPI displays
457
+ glyphs are rasterised at `font_size * SDL_GetWindowPixelDensity` while quads
458
+ are positioned in logical pixels, so text stays crisp.
459
+ - Layout uses these real metrics for text widths, line breaking and
460
+ `line-height: normal`; `measureText`/`fontMetrics` expose them to tests.
461
+
462
+ Multiple windows fall out of the object model: `createWindow` returns a native
463
+ object handle; each handle owns its own `SDL_Window`/GPU surface and its own DOM
464
+ tree. `run()` drives one shared main loop that ticks every window and exits when
465
+ the last one closes.
466
+
467
+ ## Event loop integration
468
+
469
+ The runtime change (already landed):
470
+
471
+ - `int xt_loop_poll(int timeout_ms)` — one reactor iteration; `0` polls without
472
+ blocking, `< 0` blocks.
473
+ - `void xt_loop_set_main(xt_main_loop_fn fn)` — a host may take over the main
474
+ loop. `xt_run_event_loop()` delegates to it; the generated `main` is unchanged.
475
+ - `xt_loop_set_main(NULL)` restores the default `select(2)` loop.
476
+
477
+ The GUI engine can either register its own loop through `xt_loop_set_main`, or
478
+ expose an explicit `run()`; **M2 uses the explicit `run()`** so the window is a
479
+ plain native call the TypeScript program controls. Each tick it:
480
+
481
+ 1. pumps SDL window/input events for every window,
482
+ 2. builds/uploads the display list when it changed and renders every open
483
+ window (clear pass + shape geometry + text geometry),
484
+ 3. calls `xt_loop_poll(0)` and `xt_drain_microtasks()` so sockets/timers and
485
+ `await` continuations keep making progress,
486
+ 4. repeats until all windows close or `quit()` is called.
487
+
488
+ The `xt_loop_set_main` hook remains available for hosts that want to own the
489
+ loop themselves.
490
+
491
+ This keeps network I/O, timers and `await` working inside a GUI program.
492
+
493
+ ## Native ↔ TS bridge
494
+
495
+ - **TS → engine**: direct `xt_gui_*` calls / window methods.
496
+ - **Engine → TS**: `xt_call_with_this(fn, thisValue, argc, argv)` with function
497
+ values captured from TS (e.g. event handlers registered with `win.on(...)`).
498
+ - **Future page→native RPC**: not needed while there is no page JS; native TS is
499
+ the controller. If a declarative layer is added later, it will use the same
500
+ `xt_call_*` entry points.
501
+
502
+ ## Packaging & build
503
+
504
+ - Sources live in `runtime/ext_gui/` (C/C++) plus vendored libs under `vendor/`
505
+ (gitignored; fetched on demand).
506
+ - `npm run gui` (`scripts/build-gui.ts`) fetches a pinned SDL3 (`SDL3_TAG`,
507
+ default `release-3.2.10`), builds a static SDL3, fetches and builds static
508
+ FreeType (`FREETYPE_VERSION`) and HarfBuzz (`HARFBUZZ_VERSION`), compiles the
509
+ engine and merges everything into `runtime/lib/<os>-<arch>/gui.a` (or
510
+ `gui.lib` with the MSVC ABI on Windows), same convention as `core.a`,
511
+ statically bundling all three. The merge uses `libtool` on macOS and
512
+ `ar -M` (GNU/LLVM) elsewhere; CMake archives are discovered under the build
513
+ root or `Release/` for either generator style.
514
+ - `src/extensions/gui/index.ts` exposes the archive through `nativeObjects()`
515
+ (via `findRuntimeLibrary`, so `.a`/`.lib` both work) and the OS frameworks
516
+ through `linkerFlags()`.
517
+ - CI builds `gui.a` before assembling the release archive so it ships inside
518
+ `runtime/lib/<slug>/` (the release tarball copies the whole `runtime/` tree).
519
+ The archive is built and the example is run on Linux (under Xvfb, with the
520
+ lavapipe software Vulkan driver) and macOS; Windows is provisional.
521
+ - The existing cache fingerprint already hashes `nativeObjects()` contents, so
522
+ rebuilding `gui.a` invalidates cached binaries automatically.
523
+
524
+ ### Running the example
525
+
526
+ ```sh
527
+ npm run runtime # rebuild core.a after a runtime change
528
+ npm run gui # build runtime/lib/<os>-<arch>/gui.a (fetches SDL3 once)
529
+ xbintsc run examples/gui/hello.ts --ext gui
530
+ ```
531
+
532
+ Two examples live here: `hello.ts` (HTML/CSS + a reactive counter) and
533
+ [`pelican-bike/`](../examples/gui/pelican-bike), a playable 2D game that uses
534
+ positioned layers, CSS sprites, `requestAnimationFrame` and keyboard input — see
535
+ its README for controls and the geometry report it prints.
536
+
537
+ Set `XT_GUI_AUTOCLOSE_MS=<n>` to close all windows after `n` milliseconds,
538
+ which the e2e test (`tests/e2e/gui-*.test.ts`) uses to run headlessly.
539
+
540
+ Those suites are skipped on Windows by default (the archive is built there but
541
+ the run is not validated in CI). With a `gui.lib` and a working GPU, opt in with
542
+ `xbintsc_GUI_TESTS=1`:
543
+
544
+ ```bat
545
+ set xbintsc_GUI_TESTS=1
546
+ npx vitest run tests/e2e/gui-layout.test.ts tests/e2e/gui-example.test.ts
547
+ ```
548
+
549
+ On a headless Linux box, install the SDL3 build headers and run under Xvfb with a
550
+ software Vulkan driver:
551
+
552
+ ```sh
553
+ sudo apt-get install -y clang cmake libx11-dev libxext-dev libxrandr-dev \
554
+ libxcursor-dev libxi-dev libxinerama-dev libxfixes-dev libxkbcommon-dev \
555
+ libwayland-dev wayland-protocols libdecor-0-dev libasound2-dev libpulse-dev \
556
+ libdbus-1-dev libudev-dev libdrm-dev libgbm-dev libgl1-mesa-dev \
557
+ libegl1-mesa-dev libvulkan-dev mesa-vulkan-drivers xvfb
558
+ npm run runtime && npm run gui
559
+ xvfb-run -a --server-args="-screen 0 1280x720x24" \
560
+ npx tsx src/cli/main.ts run examples/gui/hello.ts --ext gui
561
+ ```
562
+
563
+ Those are the packages the CI `compile-examples` job installs. The X11 backend is
564
+ used by default; Wayland is enabled too but not yet exercised.
565
+
566
+ ## Milestones
567
+
568
+ 1. **M1 — foundation** ✅
569
+ - Generic `xt_loop_poll` / `xt_loop_set_main` in the runtime.
570
+ - `gui` extension skeleton + generic CLI extension registration.
571
+ 2. **M2 — window + GPU clear** ✅
572
+ - SDL3 window, SDL_GPU swapchain, multiple windows, main-loop integration.
573
+ - `createWindow` / `run` / `quit` + `on`/`off` window methods work
574
+ end-to-end (`runtime/ext_gui/`, `scripts/build-gui.ts`).
575
+ 3. **M3 — HTML/CSS subset** ✅
576
+ - **M3a — parse + cascade** ✅ HTML parser, DOM tree, CSS parser, selector
577
+ matching, UA/author/inline cascade, inheritance, computed style
578
+ (`dom.*`, `css.*`, `style.*`, `document.*`).
579
+ - **M3b — layout** ✅ block/inline flow + Flexbox (`layout.*`),
580
+ `getBoundingClientRect`/`layoutTree`.
581
+ 4. **M4 — paint + text + display list**
582
+ - **M4a — display list + GPU shapes** ✅ background/border display list,
583
+ rounded-rect SDL_GPU pipeline (`paint.*`, `renderer.*`).
584
+ - **M4b-1 — text stack + metrics** ✅ HarfBuzz + FreeType linked into
585
+ `gui.a`, font resolution/caching, shaping-based text metrics used by
586
+ layout (`text.*`, `measureText`/`fontMetrics`).
587
+ - **M4b-2 — glyph rendering** ✅ FreeType rasterisation, a shared shelf-packed
588
+ glyph atlas, textured text quads in the display list and HiDPI-aware raster
589
+ scaling. Gradients remain.
590
+ 5. **M5 — input + events** ✅
591
+ - hit testing (`LayoutTree::hitTest`), pointer/wheel/keyboard events delivered
592
+ to TS handlers with a payload, `:hover`/`:focus` stateful matching and
593
+ restyle (`css.*`, `style.*`, `document.*`, `gui.cpp`), plus the
594
+ `hitTest`/`sendEvent` test hooks.
595
+ 6. **M6 — images, then CSS transitions/animations**
596
+ - **M6a — images** ✅ stb_image decode, `<img>` replaced-element layout,
597
+ per-file GPU textures and textured quads (`image.*`, `paint.*`, `renderer.*`).
598
+ - **M6b — transitions/animations** ✅ `transition` shorthand + longhands,
599
+ animated `background-color`/`color`/`border-color`/`border-radius`, retargeting
600
+ and `win.advance(ms)`. `@keyframes` remain.
601
+ 7. **M7 — CI & releases** ✅ (Linux/macOS build) / 🚧 (run + Windows)
602
+ - `compile-examples` builds `gui.a` on Linux and macOS (required), then runs
603
+ the example and `tests/e2e/gui-*.test.ts` under Xvfb + lavapipe on Linux
604
+ (required). The macOS run is provisional until a WindowServer is confirmed;
605
+ it is guarded so a failure is logged without annotating the run.
606
+ - The `package` job builds `gui.a` before assembling the release, so it ships
607
+ inside the existing runtime archive (`package-release` copies all of
608
+ `runtime/`).
609
+ - `vendor/` (SDL3/FreeType/HarfBuzz, the slow part) is cached per OS/arch,
610
+ keyed by `scripts/build-gui.ts`.
611
+ - Windows (build + run) stays provisional until the MSVC-compatible
612
+ `gui.lib` and D3D12/DXIL shader path are validated; a failure is logged
613
+ without annotating the run (see *Open questions*).
614
+ 8. **M8 — interactive DOM** ✅
615
+ - Element/document handles with stable identity, read/write properties via
616
+ runtime accessors, traversal/attributes/queries, mutation with lazy
617
+ restyle/relayout (`dom_api.*`, `document.*`, `dom.*`, `gui.cpp`,
618
+ `window.cpp`), element events with capture + bubble phases and
619
+ `stopPropagation`, and window-level `e.target` compatibility. No compiler
620
+ changes. See `doc/gui-scripts.md`.
621
+ 9. **M9 — AOT `<script>`** ✅
622
+ - **M9a** ✅ — generic `Extension.assetLoaders` hook + bundler integration.
623
+ - **M9b** ✅ — gui `.html` asset loader (`src/extensions/gui/html.ts`): inline
624
+ bodies are wrapped in `__xt_script_<hash>(window, document)` functions,
625
+ registered through the `__registerScript` builtin and replaced by
626
+ `<script data-xt-id="<hash>">` markers; `win.loadHTML` runs the matching
627
+ functions after parsing and fires `DOMContentLoaded` then `load`.
628
+ - **M9c** ✅ — `<script src>` files are read relative to the HTML, their
629
+ top-level imports are hoisted (specifiers rewritten from the HTML dir) and
630
+ their body is wrapped/registered; missing files and import-binding
631
+ collisions become diagnostics. Everything runs in document order.
632
+ 10. **M10 — polish** ✅
633
+ - `win.requestAnimationFrame(fn)` / `win.cancelAnimationFrame(id)`; callbacks
634
+ run at the top of each frame with the frame timestamp and may mutate the
635
+ DOM (`gui.cpp`, `window.cpp`, `gui_engine.h`).
636
+ - `Element.offsetWidth` / `offsetHeight` (rounded border box, flushes pending
637
+ mutations) and `Element.contains(other)` (`dom_api.cpp`).
638
+ 11. **M11 — CSS positioning** ✅
639
+ - `position: relative` / `absolute` / `fixed` with `top`/`right`/`bottom`/
640
+ `left`, resolved against the nearest positioned ancestor (the viewport when
641
+ there is none), `auto` insets keeping the static position, shrink-to-fit
642
+ for an out-of-flow `width: auto` and fill-available when both horizontal
643
+ insets are set (`layout_flow.cpp`, `layout.h`).
644
+ - Item margins are honoured in the flex algorithm, so a negative margin
645
+ participates in the layout instead of being dropped (`layout_flex.cpp`).
646
+ - Drives `examples/gui/pelican-bike` and e2e coverage in
647
+ `tests/e2e/gui-layout.test.ts`.
648
+
649
+ ## Progress log
650
+
651
+ - **M1** ✅ generic `xt_loop_poll`/`xt_loop_set_main`; `gui` extension skeleton.
652
+ - **M2** ✅ SDL3 window + SDL_GPU clear, multiple windows, `createWindow`/`run`.
653
+ - **M3a** ✅ HTML parser (`dom.*`), CSS parser/matcher (`css.*`), cascade and
654
+ computed style (`style.*`), document model (`document.*`),
655
+ `computedStyle`/`queryCount`/`documentTree`, e2e coverage.
656
+ - **M3b** ✅ layout (`layout.*`): block flow, inline formatting context with line
657
+ breaking, single-line Flexbox, `getBoundingClientRect`/`layoutTree`, e2e
658
+ coverage.
659
+ - **M4a** ✅ display list (`paint.*`) and the SDL_GPU 2D renderer with an MSL
660
+ rounded-rect pipeline (`renderer.*`), `paintList`/`paintCount`, e2e coverage.
661
+ - **M4b-1** ✅ FreeType + HarfBuzz fetched/built/merged into `gui.a`, the text
662
+ module (`text.*`) with font resolution, HarfBuzz shaping and FreeType metrics,
663
+ real text metrics in layout, `measureText`/`fontMetrics`, e2e coverage.
664
+ - **M4b-2** ✅ glyph atlas + textured text pipeline in `renderer.*`, shaped text
665
+ runs in `paint.*`, `xt_text_shape_run`/`xt_text_rasterize` in `text.*`, HiDPI
666
+ raster scaling, e2e coverage.
667
+ - **M5** ✅ hit testing + input events (`LayoutTree::hitTest`, `xt_dom_describe`,
668
+ `xt_gui_dispatch_*`), `:hover`/`:focus` in the matcher with dynamic restyle,
669
+ `hitTest`/`sendEvent` test hooks, e2e coverage.
670
+ - **M6a** ✅ image decoding (`image.*`, vendored stb_image), `<img>` intrinsic
671
+ sizing in layout, `PaintImage` in the display list, per-file RGBA textures and
672
+ an image pipeline in `renderer.*`, e2e coverage.
673
+ - **M6b** ✅ CSS transitions (parsing in `style.*`, an animation clock and
674
+ transition state in `document.*`, `advance`/`advance(ms)` hooks, e2e coverage).
675
+ - **M7** ✅ builds `gui.a` in CI on Linux and macOS (required) and runs the
676
+ example plus the GUI e2e suite under Xvfb on Linux; `package` ships `gui.a`,
677
+ `vendor/` is cached, and the `ar -M` merge works on GNU/Linux and macOS.
678
+ macOS runs and all of Windows remain provisional in CI.
679
+ - **M8** ✅ element/document handles (`dom_api.*`), DOM mutation with lazy
680
+ restyle/relayout (`document.*`, `xt_gui_flush_dom`), element event dispatch
681
+ with capture/bubble and `stopPropagation` (`dom_api.*`, `gui.cpp`), and e2e
682
+ coverage in `tests/e2e/gui-*.test.ts`.
683
+ - **M9a** ✅ extensions can register asset loaders keyed by file extension;
684
+ `bundleModules`/`loadGraph` consult them after reading a file. Unit tests in
685
+ `tests/driver/modules.test.ts`.
686
+ - **M9b** ✅ `import page from "./page.html"` compiles inline `<script lang="ts">`
687
+ bodies into AOT functions registered at startup and run by `win.loadHTML`
688
+ (before first layout; `DOMContentLoaded` then `load`). Unit tests in
689
+ `tests/extensions/gui.test.ts`, e2e in `tests/e2e/gui-*.test.ts`.
690
+ - **M9c** ✅ external `<script src>` files are read, their imports hoisted
691
+ (specifiers rewritten from the HTML dir) and their body wrapped/registered;
692
+ missing files and import-binding collisions are diagnostics. Loader errors are
693
+ caught by `loadGraph` and reported as build errors.
694
+ - **M10** ✅ `requestAnimationFrame`/`cancelAnimationFrame` on window handles
695
+ (callbacks run with the frame timestamp before layout each frame) plus
696
+ `offsetWidth`/`offsetHeight`/`contains` on element handles; e2e coverage in
697
+ `tests/e2e/gui-*.test.ts`.
698
+ - **M11** ✅ `position: relative`/`absolute`/`fixed` with `top`/`right`/`bottom`/
699
+ `left`. Layout keeps a containing block per box (the nearest positioned
700
+ ancestor's padding box, published only once that ancestor has its final size,
701
+ so `bottom`/`right` are exact), absolute boxes are laid out after in-flow
702
+ content so they never affect it, shrink-to-fit/fill-available width rules are
703
+ applied, and flex items now account for their margins. e2e coverage in
704
+ `tests/e2e/gui-layout.test.ts`; `examples/gui/pelican-bike` is built on it.
705
+
706
+ ## Known issues
707
+
708
+ - **Intermittent crash at auto-close.** `XT_GUI_AUTOCLOSE_MS` can end a run
709
+ with a native access violation in roughly two runs out of five; a clean exit
710
+ looks identical otherwise. Windows reports it as the NTSTATUS exit code
711
+ `0xC0000005`; on Linux/macOS the process dies with a signal — an access
712
+ violation is `SIGSEGV`, so `spawnSync` reports `status: null` plus a
713
+ `signal`. It only reproduces with a *running animation-frame loop* (a static
714
+ document closes fine) and it is **not** the game's own code: no exception is
715
+ reported, the crash usually lands inside the first second regardless of the
716
+ deadline, and raising `XT_GC_THRESHOLD` hides it, so it looks like a
717
+ GC/teardown race. Interactive runs (no auto-close) are stable for minutes.
718
+ A leading cause was that the engine cached `xt_value`s in C++ containers
719
+ the mark-sweep collector cannot see — the AOT `<script>` registry, element
720
+ handles, element listeners and the queued/currently-running
721
+ `requestAnimationFrame` callbacks. They are now marked by GC root providers
722
+ (`xt_gui_script_gc_scan` and `xt_gui_gc_scan_roots`), which removes that
723
+ class of use-after-free. `tests/e2e/gui-example.test.ts` still treats either
724
+ crash spelling as an acceptable outcome — the archive is built per platform
725
+ and the run is timing-sensitive — and the behavioural assertions live in
726
+ `tests/e2e/gui-example-browser.test.ts` (no GPU, deterministic).
727
+ - **`display: none -> flex` restyle.** A hidden element being shown by a
728
+ *class change* on a flex container has also been observed to crash the same
729
+ way; `examples/gui/pelican-bike` reveals its game-over overlay with `opacity`
730
+ instead.
731
+
732
+ ## Open questions
733
+
734
+ - Whether Linux ships X11, Wayland, or both in the first cut. (Decision:
735
+ X11 first, Wayland later.)
736
+ - **SVG:** there is no SVG support and none is planned. The HTML parser knows
737
+ elements and attributes, the layout engine knows boxes, and paint knows
738
+ rectangles/text/images, so `<svg>` trees would need a whole second
739
+ geometry+paint path. An SVG *file* works only through the image loader
740
+ (`<img src="logo.svg">`), which needs an SVG rasteriser (`stb_image` does not
741
+ decode SVG); vector art therefore has to arrive as a raster (PNG) or be drawn
742
+ from boxes, as `examples/gui/pelican-bike` does.
743
+ - Windows: an MSVC-compatible `gui.lib` (COFF objects + `ar -M`/`llvm-ar`) is
744
+ produced by `scripts/build-gui.ts`, but it has not been validated in CI yet,
745
+ so the Windows step is provisional (the job stays green) and `package` skips
746
+ it. It also needs an SDL3 build with the D3D12/DXIL backend (DXIL requires
747
+ `dxc`).
748
+ - **D3D12 shaders:** the SPIR-V (Vulkan) and DXIL (Direct3D 12) blobs are built
749
+ and embedded, so the non-Metal path no longer skips geometry. DXIL remains
750
+ unusable on **SDL 3.2.10**: its D3D12 backend rejects any graphics pipeline
751
+ whose shaders declare a uniform buffer, and the engine passes the viewport that
752
+ way. Re-check after an SDL upgrade.