xbintsc 0.3.46 → 0.3.49

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