xbintsc 0.3.69 → 0.3.79
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -4
- package/README.zh-CN.md +52 -2
- package/dist/tests/e2e/gui-advanced.test.d.ts +6 -0
- package/dist/tests/e2e/gui-advanced.test.js +261 -0
- package/dist/tests/e2e/gui-advanced.test.js.map +1 -0
- package/dist/tests/e2e/gui-canvas.test.d.ts +9 -0
- package/dist/tests/e2e/gui-canvas.test.js +233 -0
- package/dist/tests/e2e/gui-canvas.test.js.map +1 -0
- package/dist/tests/e2e/gui-example.test.d.ts +1 -1
- package/dist/tests/e2e/gui-example.test.js +2 -2
- package/dist/tests/e2e/gui-helpers.d.ts +3 -0
- package/dist/tests/e2e/gui-helpers.js +5 -0
- package/dist/tests/e2e/gui-helpers.js.map +1 -1
- package/dist/tests/e2e/gui-layout.test.js +73 -0
- package/dist/tests/e2e/gui-layout.test.js.map +1 -1
- package/dist/tests/e2e/gui-media.test.d.ts +8 -0
- package/dist/tests/e2e/gui-media.test.js +119 -0
- package/dist/tests/e2e/gui-media.test.js.map +1 -0
- package/dist/tests/e2e/gui-paint.test.js +40 -0
- package/dist/tests/e2e/gui-paint.test.js.map +1 -1
- package/doc/ai/extensions.md +16 -3
- package/doc/ai/zh-CN/extensions.md +10 -3
- package/doc/gui/animation.md +46 -0
- package/doc/gui/api.md +92 -0
- package/doc/gui/build.md +91 -0
- package/doc/gui/canvas.md +98 -0
- package/doc/gui/dom.md +123 -0
- package/doc/gui/input.md +22 -0
- package/doc/gui/known-issues.md +66 -0
- package/doc/gui/layout.md +72 -0
- package/doc/gui/media.md +87 -0
- package/doc/gui/rendering.md +98 -0
- package/doc/gui/runtime.md +88 -0
- package/doc/gui-scripts.md +7 -3
- package/doc/gui.md +80 -612
- package/doc/implemented.md +1 -0
- package/doc/zh-CN/gui/animation.md +46 -0
- package/doc/zh-CN/gui/api.md +88 -0
- package/doc/zh-CN/gui/build.md +84 -0
- package/doc/zh-CN/gui/canvas.md +80 -0
- package/doc/zh-CN/gui/dom.md +114 -0
- package/doc/zh-CN/gui/input.md +21 -0
- package/doc/zh-CN/gui/known-issues.md +50 -0
- package/doc/zh-CN/gui/layout.md +60 -0
- package/doc/zh-CN/gui/media.md +71 -0
- package/doc/zh-CN/gui/rendering.md +81 -0
- package/doc/zh-CN/gui/runtime.md +76 -0
- package/doc/zh-CN/gui-scripts.md +6 -2
- package/doc/zh-CN/gui.md +64 -532
- package/doc/zh-CN/implemented.md +1 -0
- package/llms.txt +15 -2
- package/package.json +1 -1
- package/runtime/ext_gui/canvas.cpp +580 -0
- package/runtime/ext_gui/canvas.h +170 -0
- package/runtime/ext_gui/canvas_internal.h +59 -0
- package/runtime/ext_gui/canvas_raster.cpp +254 -0
- package/runtime/ext_gui/document.cpp +24 -1
- package/runtime/ext_gui/document.h +11 -0
- package/runtime/ext_gui/dom_api.cpp +3 -0
- package/runtime/ext_gui/dom_api_canvas.cpp +548 -0
- package/runtime/ext_gui/dom_api_internal.h +73 -0
- package/runtime/ext_gui/dom_api_media.cpp +274 -0
- package/runtime/ext_gui/dom_api_proto.cpp +80 -0
- package/runtime/ext_gui/gui.cpp +6 -0
- package/runtime/ext_gui/gui_canvas.cpp +95 -0
- package/runtime/ext_gui/gui_engine.h +49 -0
- package/runtime/ext_gui/gui_events.cpp +21 -5
- package/runtime/ext_gui/gui_media.cpp +193 -0
- package/runtime/ext_gui/image.cpp +31 -12
- package/runtime/ext_gui/image.h +20 -7
- package/runtime/ext_gui/layout.cpp +52 -5
- package/runtime/ext_gui/layout.h +33 -1
- package/runtime/ext_gui/layout_flex.cpp +263 -99
- package/runtime/ext_gui/layout_flow.cpp +85 -9
- package/runtime/ext_gui/layout_flow_internal.h +84 -5
- package/runtime/ext_gui/layout_inline.cpp +54 -37
- package/runtime/ext_gui/media.cpp +514 -0
- package/runtime/ext_gui/media.h +104 -0
- package/runtime/ext_gui/nanosvg.h +3278 -0
- package/runtime/ext_gui/nanosvgrast.h +1472 -0
- package/runtime/ext_gui/paint.cpp +123 -38
- package/runtime/ext_gui/paint.h +36 -6
- package/runtime/ext_gui/renderer.cpp +113 -10
- package/runtime/ext_gui/renderer.h +49 -9
- package/runtime/ext_gui/renderer_image.cpp +203 -77
- package/runtime/ext_gui/renderer_internal.h +6 -8
- package/runtime/ext_gui/renderer_text.cpp +11 -26
- package/runtime/ext_gui/style.h +11 -0
- package/runtime/ext_gui/style_print.cpp +11 -0
- package/runtime/ext_gui/style_ua.cpp +7 -0
- package/runtime/ext_gui/style_values.cpp +40 -1
- package/runtime/ext_gui/svg.cpp +182 -0
- package/runtime/ext_gui/svg.h +46 -0
- package/runtime/ext_gui/text.cpp +156 -25
- package/runtime/ext_gui/text.h +11 -2
- package/runtime/ext_gui/window.cpp +4 -1
- package/scripts/build-gui.ts +148 -6
- package/scripts/check-file-length.ts +5 -1
- package/scripts/install-gui-deps.sh +28 -0
package/doc/gui.md
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
# xbintsc GUI extension (self-hosted HTML/CSS renderer)
|
|
2
2
|
|
|
3
|
-
Status: **
|
|
3
|
+
Status: **M14** — features (M1–M13) are complete: HTML parsing, CSS selector
|
|
4
4
|
matching, the cascade, computed styles and layout (block, inline, Flexbox and
|
|
5
5
|
CSS positioning) are
|
|
6
6
|
in place, and the engine *paints*: it builds a display list of rectangles, images
|
|
7
7
|
and shaped text runs and renders them through SDL_GPU. Input events are hit
|
|
8
8
|
tested and delivered to native TS handlers, `:hover`/`:focus` are matched
|
|
9
9
|
dynamically, `<img>` is sized from its intrinsic dimensions and drawn from a
|
|
10
|
-
texture
|
|
10
|
+
texture — PNG/JPEG/… bitmaps and SVG files alike, the latter rasterised at the
|
|
11
|
+
drawn size so vector art stays crisp — and CSS transitions animate paint
|
|
12
|
+
properties. **M8** adds an interactive
|
|
11
13
|
DOM: element handles with stable identity, mutation (`appendChild`, `textContent`,
|
|
12
14
|
`classList`, `style`, …) and element-level events with capture/bubbling.
|
|
13
15
|
**M9** compiles `<script>` bodies ahead of time (inline and `<script src>`) — see
|
|
@@ -15,10 +17,31 @@ DOM: element handles with stable identity, mutation (`appendChild`, `textContent
|
|
|
15
17
|
**M11** adds `position: relative`/`absolute`/`fixed` with their offsets, which is
|
|
16
18
|
what `examples/gui/pelican-bike` — a playable 2D game — is built on. **M12** makes
|
|
17
19
|
DOM node lifetime explicit (what a mutation releases, what it keeps) and gives the
|
|
18
|
-
engine bounded memory: see
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
engine bounded memory: see [Node lifetime](./gui/dom.md#node-lifetime) and
|
|
21
|
+
[Memory & GC](./gui/runtime.md#memory--gc). **M13** plays real
|
|
22
|
+
`<video>`/`<audio>` — a vendored minimal FFmpeg decodes them on the main thread,
|
|
23
|
+
video frames go through the image texture path and audio to an SDL stream, with
|
|
24
|
+
the DOM media API exposing `play`/`pause`/`currentTime`/… and media events.
|
|
25
|
+
**M14** adds `<canvas>` with a self-written 2D software rasteriser (anti-aliased
|
|
26
|
+
fills, paths, text and `drawImage`) uploaded through the image texture path —
|
|
27
|
+
see [Canvas](./gui/canvas.md).
|
|
28
|
+
This overview records the locked decisions, the architecture, the milestone plan
|
|
29
|
+
and the current progress; the engine itself is documented by function in the
|
|
30
|
+
pages above.
|
|
31
|
+
|
|
32
|
+
## Contents
|
|
33
|
+
|
|
34
|
+
- [API](./gui/api.md) — window handles, events and diagnostics
|
|
35
|
+
- [DOM, HTML and CSS](./gui/dom.md) — parsing, cascade, interactive DOM and AOT scripts
|
|
36
|
+
- [Layout](./gui/layout.md) — block, inline, Flexbox and positioning
|
|
37
|
+
- [Rendering](./gui/rendering.md) — display list, GPU pipelines and text
|
|
38
|
+
- [Media](./gui/media.md) — images, SVG, video and audio
|
|
39
|
+
- [Canvas](./gui/canvas.md) — `<canvas>` and the 2D context
|
|
40
|
+
- [Input](./gui/input.md) — hit testing, events and `:hover`/`:focus`
|
|
41
|
+
- [Animation](./gui/animation.md) — `requestAnimationFrame` and CSS transitions
|
|
42
|
+
- [Runtime integration](./gui/runtime.md) — event loop, native bridge and memory
|
|
43
|
+
- [Packaging and build](./gui/build.md) — building `gui.a` and running examples
|
|
44
|
+
- [Known issues and open questions](./gui/known-issues.md)
|
|
22
45
|
|
|
23
46
|
## Goals
|
|
24
47
|
|
|
@@ -74,7 +97,7 @@ HTML/CSS UI with its own GPU-accelerated engine.
|
|
|
74
97
|
▼
|
|
75
98
|
windowing + input (SDL3) ── multiple windows
|
|
76
99
|
text (HarfBuzz + FreeType) ── glyph atlas / SDF
|
|
77
|
-
images (stb_image)
|
|
100
|
+
images (stb_image; SVG via NanoSVG)
|
|
78
101
|
```
|
|
79
102
|
|
|
80
103
|
The engine never talks to the compiler. The compiler only sees an `Extension`
|
|
@@ -88,10 +111,12 @@ with `nativeObjects()`, `linkerFlags()` and `modules()`.
|
|
|
88
111
|
| GPU | **SDL_GPU** (SDL3) | one render path over Metal / Vulkan / D3D12; avoids three backends |
|
|
89
112
|
| Text shaping | **HarfBuzz** | correct complex-script shaping |
|
|
90
113
|
| Glyph raster | **FreeType** | glyph outlines → GPU atlas / SDF |
|
|
91
|
-
| Images | **stb_image** (vendored header) | single
|
|
114
|
+
| Images | **stb_image** (vendored header) for bitmaps, **NanoSVG** (vendored headers) for SVG | single headers to start; SVG files rasterise on demand |
|
|
115
|
+
| Video + audio | **FFmpeg** (statically vendored, minimal build) | decodes the common container/codec set; no WebView or platform media framework |
|
|
92
116
|
|
|
93
|
-
Pinned versions: SDL3 `release-3.2.10`, FreeType `2.13.3`, HarfBuzz `10.1.0
|
|
94
|
-
(overridable with `SDL3_TAG` / `FREETYPE_VERSION` /
|
|
117
|
+
Pinned versions: SDL3 `release-3.2.10`, FreeType `2.13.3`, HarfBuzz `10.1.0`,
|
|
118
|
+
FFmpeg `7.1` (overridable with `SDL3_TAG` / `FREETYPE_VERSION` /
|
|
119
|
+
`HARFBUZZ_VERSION` / `FFMPEG_VERSION`).
|
|
95
120
|
|
|
96
121
|
Alternative if SDL3 is rejected: **GLFW + OpenGL 3.3** (simpler, but OpenGL is
|
|
97
122
|
deprecated on macOS and gives no modern GPU abstraction). **wgpu-native** if the
|
|
@@ -101,508 +126,6 @@ engine were Rust.
|
|
|
101
126
|
> xbintsc stays "download and run"; only OS frameworks are added by
|
|
102
127
|
> `linkerFlags()`.
|
|
103
128
|
|
|
104
|
-
## TS-facing API
|
|
105
|
-
|
|
106
|
-
```ts
|
|
107
|
-
import { createWindow, run, quit } from "gui";
|
|
108
|
-
|
|
109
|
-
const win = createWindow({ title: "Demo", width: 900, height: 600 });
|
|
110
|
-
win.setBackground("#14161c");
|
|
111
|
-
win.loadHTML(INDEX_HTML); // parses HTML/CSS and computes styles
|
|
112
|
-
win.on("ready", () => console.log("first frame presented"));
|
|
113
|
-
win.on("close", () => console.log("window closed"));
|
|
114
|
-
|
|
115
|
-
run(); // drives the main loop until all windows close
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
Methods implemented on a window handle: `setTitle` / `setSize` / `loadHTML` /
|
|
119
|
-
`getHTML` / `setBackground` / `close` / `isOpen` / `on` / `off`, plus `driver()`,
|
|
120
|
-
which reports the SDL_GPU backend in use (`"vulkan"`, `"direct3d12"`, `"metal"`,
|
|
121
|
-
… — SDL picks the best one the device supports). The Web spellings are available
|
|
122
|
-
too: `addEventListener` / `removeEventListener` are aliases of `on` / `off` (their
|
|
123
|
-
`options` argument is accepted and ignored) and the `closed` getter mirrors
|
|
124
|
-
`isOpen()`, so a document written against the browser API runs unchanged.
|
|
125
|
-
Events emitted:
|
|
126
|
-
`ready` (after the first presented frame), `load`, `close`, `resize` and the
|
|
127
|
-
input events `mousemove`, `mousedown`, `mouseup`, `click`, `wheel`, `keydown`,
|
|
128
|
-
`keyup`.
|
|
129
|
-
|
|
130
|
-
On Windows a GUI program is linked as a **Windows-subsystem** executable, so
|
|
131
|
-
launching it does not flash a console window. That also means stdout/stderr go
|
|
132
|
-
nowhere when it is started from Explorer; set `app.console: true` in
|
|
133
|
-
`xbintsc.config.json` to keep the console subsystem (useful while debugging).
|
|
134
|
-
|
|
135
|
-
Input handlers receive a single payload object (lifecycle handlers receive none):
|
|
136
|
-
|
|
137
|
-
```ts
|
|
138
|
-
win.on("click", (e) => console.log(e.x, e.y, e.button, e.target));
|
|
139
|
-
win.on("wheel", (e) => console.log(e.deltaX, e.deltaY));
|
|
140
|
-
win.on("keydown", (e) => console.log(e.key, e.code, e.ctrl, e.shift, e.alt, e.meta));
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
| Field | Events | Meaning |
|
|
144
|
-
| --- | --- | --- |
|
|
145
|
-
| `x`, `y` / `clientX`, `clientY` | pointer, wheel | viewport-relative logical pixels |
|
|
146
|
-
| `button` | pointer | `0` left, `1` middle, `2` right (`-1` for move) |
|
|
147
|
-
| `clicks` | pointer | click count reported by the OS |
|
|
148
|
-
| `deltaX`, `deltaY` | wheel | scroll amount (`deltaY` positive = down) |
|
|
149
|
-
| `key`, `code` | keyboard | key name and physical scancode name |
|
|
150
|
-
| `repeat`, `ctrl`, `shift`, `alt`, `meta` | keyboard | modifiers |
|
|
151
|
-
| `target` | pointer, wheel | deepest element under the point as a CSS-like descriptor (`div#main.card`), or absent |
|
|
152
|
-
|
|
153
|
-
### Implemented HTML/CSS subset (M3a)
|
|
154
|
-
|
|
155
|
-
**HTML parser** (`runtime/ext_gui/dom.{h,cpp}`): tags/attributes/text,
|
|
156
|
-
comments and doctype skipped, entity decoding (named + numeric/hex), void
|
|
157
|
-
elements, raw-text elements (`<style>`/`<script>`), and the common implicit
|
|
158
|
-
close rules (`li`, `dt`/`dd`, `option`, `p`, headings, table cells/rows).
|
|
159
|
-
|
|
160
|
-
**CSS parser** (`runtime/ext_gui/css.{h,cpp}`): comments and at-rules skipped
|
|
161
|
-
(for now), rules with multiple selectors, and declarations. Selectors: type,
|
|
162
|
-
`.class`, `#id`, attribute (`=`, `~=`, `|=`, `^=`, `$=`, `*=`), the four
|
|
163
|
-
combinators (descendant, child, adjacent and general sibling) and the
|
|
164
|
-
pseudo-classes `:first-child`, `:last-child`, `:only-child`, `:empty`,
|
|
165
|
-
`:root`, `:not(...)`, `:nth-child(an+b)`, `:disabled`, `:checked`, plus the
|
|
166
|
-
stateful `:hover` and `:focus` (see *Implemented input*). Values:
|
|
167
|
-
lengths (`px`, `%`, `em`, `rem`, `vw`, `vh`, `pt`, `pc`, `in`, `cm`, `mm`, `q`),
|
|
168
|
-
colors (hex, `rgb()`/`rgba()`, a named subset), numbers, keywords and shorthands
|
|
169
|
-
(`margin`/`padding`/`border`/`flex`).
|
|
170
|
-
|
|
171
|
-
**Cascade & computed style** (`runtime/ext_gui/style.{h,cpp}`): a small built-in
|
|
172
|
-
UA stylesheet, author rules sorted by `(!important, specificity, source order)`,
|
|
173
|
-
then inline `style=""` (highest specificity, but non-`!important` inline loses to
|
|
174
|
-
`!important`), plus CSS inheritance of the text properties. Relative lengths are
|
|
175
|
-
kept unresolved until layout, except `font-size` (resolved against the *parent*
|
|
176
|
-
font size) and `line-height`.
|
|
177
|
-
|
|
178
|
-
### Diagnostics
|
|
179
|
-
|
|
180
|
-
So the HTML/CSS/paint pipeline is testable without a GPU, a window handle
|
|
181
|
-
exposes read-only hooks:
|
|
182
|
-
|
|
183
|
-
```ts
|
|
184
|
-
win.computedStyle(selector, property) // e.g. ("#main", "width") -> "60%"
|
|
185
|
-
win.queryCount(selector) // number of matching elements
|
|
186
|
-
win.getBoundingClientRect(selector) // { x, y, width, height } (border box)
|
|
187
|
-
win.documentTree() // serialized DOM (debugging)
|
|
188
|
-
win.layoutTree() // serialized layout boxes (debugging)
|
|
189
|
-
win.paintCount() // number of shapes in the display list
|
|
190
|
-
win.paintList() // serialized display list (debugging)
|
|
191
|
-
win.measureText(text, fontSize?, family?) // shaped advance width in pixels
|
|
192
|
-
win.fontMetrics(fontSize?, family?) // { ascent, descent, lineHeight, ready }
|
|
193
|
-
win.hitTest(x, y) // deepest element descriptor, or ""
|
|
194
|
-
win.sendEvent(type, options?) // synthesise input (testing)
|
|
195
|
-
win.advance(ms) // step the CSS transition clock (testing)
|
|
196
|
-
win.stats() // engine/runtime counters (memory debugging)
|
|
197
|
-
```
|
|
198
|
-
|
|
199
|
-
`win.stats()` returns `{ heapBytes, heapAllocations, treeNodes, detachedRoots,`
|
|
200
|
-
`detachedNodes, handles, handleSlots, listeners }`: the live runtime heap, the
|
|
201
|
-
size of the DOM tree, the detached-node pool, and the element-handle tables. In a
|
|
202
|
-
steady state every one of them is constant — a climbing `detachedNodes` or
|
|
203
|
-
`handles` means the program removes nodes faster than it releases them (see
|
|
204
|
-
"Node lifetime" below).
|
|
205
|
-
|
|
206
|
-
They are used by `tests/e2e/gui-*.test.ts` to assert parsing, selector matching,
|
|
207
|
-
specificity, inheritance, `!important` and layout geometry. They will stay useful
|
|
208
|
-
afterwards for debugging.
|
|
209
|
-
|
|
210
|
-
### Interactive DOM (M8)
|
|
211
|
-
|
|
212
|
-
`win.document` returns the document handle; element handles have stable identity
|
|
213
|
-
and read/write properties, attributes, traversal and geometry:
|
|
214
|
-
|
|
215
|
-
```ts
|
|
216
|
-
const doc = win.document;
|
|
217
|
-
const box = doc.querySelector("#box");
|
|
218
|
-
const inner = doc.querySelector("#inner");
|
|
219
|
-
inner.textContent = "hi"; // write
|
|
220
|
-
inner.classList.add("hot");
|
|
221
|
-
inner.style.setProperty("color", "#0f0");
|
|
222
|
-
inner.setAttribute("data-role", "lead");
|
|
223
|
-
console.log(inner.id, inner.tagName, doc.querySelector("#box") === box);
|
|
224
|
-
|
|
225
|
-
const created = doc.createElement("div");
|
|
226
|
-
created.textContent = "added";
|
|
227
|
-
box.appendChild(created);
|
|
228
|
-
box.removeChild(created);
|
|
229
|
-
|
|
230
|
-
inner.addEventListener("click", (e) => console.log(e.target.id, e.currentTarget.id));
|
|
231
|
-
inner.click(); // synthesise a click at the element
|
|
232
|
-
console.log(box.offsetWidth, box.offsetHeight); // rounded border box
|
|
233
|
-
console.log(box.contains(inner)); // descendant test
|
|
234
|
-
```
|
|
235
|
-
|
|
236
|
-
Mutations mark the document dirty; the engine restyles + relayouts lazily (before
|
|
237
|
-
the next read or frame). `inner = …`/`innerHTML = …` do **not** run scripts.
|
|
238
|
-
Element events support capture and bubble phases, `stopPropagation`, `once`, and
|
|
239
|
-
bubble up to `document`/`window`. The legacy `win.on(type, fn)` payload keeps its
|
|
240
|
-
**string** `e.target` (`div#id.class`); the element `Event.target` is a handle
|
|
241
|
-
whose descriptor matches the same string.
|
|
242
|
-
|
|
243
|
-
#### Node lifetime
|
|
244
|
-
|
|
245
|
-
A mutation decides whether the nodes it removes can still be reached, and the
|
|
246
|
-
engine releases exactly those it cannot:
|
|
247
|
-
|
|
248
|
-
| Mutation | Removed nodes |
|
|
249
|
-
| --- | --- |
|
|
250
|
-
| `innerHTML = …`, `inner = …`, `textContent = …`, `replaceChild` | **released** — the script has no way to refer to them again |
|
|
251
|
-
| `removeChild`, `remove` | **kept** in the document's detached pool — the DOM keeps a removed node alive and usable, so it can be re-attached |
|
|
252
|
-
|
|
253
|
-
Released nodes free their C++ subtree as well as their handle, so the counters
|
|
254
|
-
from `win.stats()` (`detachedNodes`, `handles`, `handleSlots`) return to their
|
|
255
|
-
previous values. That is what makes the usual game pattern — rewriting a sprite
|
|
256
|
-
with `innerHTML` every frame — run in constant memory; before, every replaced
|
|
257
|
-
subtree stayed pooled forever and a loop writing ten nodes per frame leaked
|
|
258
|
-
several megabytes per second.
|
|
259
|
-
|
|
260
|
-
The one case the engine cannot see is a node the script creates and then drops
|
|
261
|
-
without ever attaching it: `document.createElement("div")` parks the node in the
|
|
262
|
-
pool *and* keeps its handle reachable, because the mark-sweep collector has no
|
|
263
|
-
weak references, so the engine cannot tell that the script threw the handle
|
|
264
|
-
away. A loop that creates elements without attaching them therefore grows the
|
|
265
|
-
pool. Reuse the elements (or build the content with `innerHTML`, which releases
|
|
266
|
-
what it replaces) instead of creating fresh ones per frame.
|
|
267
|
-
|
|
268
|
-
### AOT scripts (M9)
|
|
269
|
-
|
|
270
|
-
Import an `.html` file that contains inline `<script lang="ts">` bodies; the
|
|
271
|
-
loader compiles each body into a native function and `win.loadHTML(page)` runs
|
|
272
|
-
them once the document is parsed (no JavaScript engine, no runtime `eval`):
|
|
273
|
-
|
|
274
|
-
```ts
|
|
275
|
-
import { createWindow, run } from "gui";
|
|
276
|
-
import page from "./page.html";
|
|
277
|
-
|
|
278
|
-
const win = createWindow({ title: "counter", width: 320, height: 240 });
|
|
279
|
-
win.loadHTML(page);
|
|
280
|
-
run();
|
|
281
|
-
```
|
|
282
|
-
|
|
283
|
-
```html
|
|
284
|
-
<button id="b">0</button>
|
|
285
|
-
<script lang="ts">
|
|
286
|
-
const b = document.getElementById("b");
|
|
287
|
-
let n = 0;
|
|
288
|
-
b.addEventListener("click", () => { b.textContent = String(++n); });
|
|
289
|
-
</script>
|
|
290
|
-
```
|
|
291
|
-
|
|
292
|
-
The two parameters (`window`, `document`) are ordinary function arguments, so
|
|
293
|
-
script locals need no global-object machinery. External scripts work too:
|
|
294
|
-
`<script src="./counter.ts">` is read at compile time, its imports are hoisted
|
|
295
|
-
(rewritten to resolve from the HTML file) and its body is wrapped the same way —
|
|
296
|
-
so a script module can `import` helpers and still see `document`. Scripts are
|
|
297
|
-
**compile-time assets**: HTML created at runtime (`innerHTML`, fetched over the
|
|
298
|
-
network) never executes, and `src` URLs (`https://…`, `data:…`) are ignored.
|
|
299
|
-
Everything runs in document order after parsing (effectively deferred). See
|
|
300
|
-
`doc/gui-scripts.md` for the full design.
|
|
301
|
-
|
|
302
|
-
### Animation frames (M10)
|
|
303
|
-
|
|
304
|
-
`requestAnimationFrame` runs a callback once on the next frame; the callback
|
|
305
|
-
receives the frame timestamp (ms) and may mutate the DOM, which is restyled and
|
|
306
|
-
repainted in the same frame. Re-queue from inside the callback to animate:
|
|
307
|
-
|
|
308
|
-
```ts
|
|
309
|
-
const win = createWindow({ title: "anim", width: 320, height: 240 });
|
|
310
|
-
let n = 0;
|
|
311
|
-
const tick = (t: number) => {
|
|
312
|
-
win.document.getElementById("label").textContent = String(n++);
|
|
313
|
-
if (n < 120) win.requestAnimationFrame(tick); // id returned; cancelAnimationFrame(id) drops it
|
|
314
|
-
};
|
|
315
|
-
win.on("ready", () => win.requestAnimationFrame(tick));
|
|
316
|
-
win.loadHTML("<div id='label'>0</div>");
|
|
317
|
-
run();
|
|
318
|
-
```
|
|
319
|
-
|
|
320
|
-
### Implemented layout (M3)
|
|
321
|
-
|
|
322
|
-
`runtime/ext_gui/layout.{h,cpp}` turns the styled DOM into a `LayoutBox` tree
|
|
323
|
-
with absolute (viewport-relative) geometry:
|
|
324
|
-
|
|
325
|
-
- **Block flow** — block-level children stack vertically (no margin collapsing
|
|
326
|
-
yet); `display: none` generates no box; `width: auto` fills the containing
|
|
327
|
-
block, `height: auto` wraps the content. The box model (margin/padding/border)
|
|
328
|
-
is resolved, including percentages against the containing block width.
|
|
329
|
-
- **Inline flow** — consecutive inline-level children form an anonymous inline
|
|
330
|
-
formatting context with greedy, word-based line breaking, `text-align` and
|
|
331
|
-
`line-height`. Inline elements get the union of their descendants' geometry;
|
|
332
|
-
`display: inline-block` is laid out atomically with a shrink-to-fit width.
|
|
333
|
-
Each text fragment remembers the run it covers, so paint can shape it. Text is
|
|
334
|
-
measured with the HarfBuzz/FreeType stack (see *Implemented text*).
|
|
335
|
-
- **Flexbox** — single-line `row`/`column` (and the `-reverse` variants) with
|
|
336
|
-
`gap`, `flex-basis`/`flex-grow`/`flex-shrink`, `justify-content` and
|
|
337
|
-
`align-items` (including `stretch` when the cross size is definite). Item
|
|
338
|
-
margins are part of the item's outer size on both axes, so a negative
|
|
339
|
-
`margin-top` pulls an item up instead of being ignored.
|
|
340
|
-
- **Positioning** — `position: relative` offsets a box (and its subtree) by
|
|
341
|
-
`top`/`right`/`bottom`/`left` without changing the space it reserved;
|
|
342
|
-
`position: absolute` (and `fixed`, which resolves against the viewport) takes
|
|
343
|
-
the box out of flow and places it against the padding box of the nearest
|
|
344
|
-
positioned ancestor — the viewport when there is none. An `auto` inset keeps
|
|
345
|
-
the static position on that axis, an `auto` width is shrink-to-fit unless both
|
|
346
|
-
`left` and `right` are set (then it fills the space between them), and
|
|
347
|
-
percentages resolve against the containing block. There is no `z-index`
|
|
348
|
-
stacking yet: painting stays in document order.
|
|
349
|
-
Note the consequence of that shrink-to-fit rule: an absolutely positioned box
|
|
350
|
-
with `width: auto` and no explicit size gets **no** available width for its own
|
|
351
|
-
inline content (its preferred width comes from its text or its non-absolute
|
|
352
|
-
children only), so inline runs inside it are laid out at zero available width
|
|
353
|
-
and each one wraps onto its own line. Give such a box an explicit `width`, or
|
|
354
|
-
place its children with `left`/`width` (as
|
|
355
|
-
`examples/gui/pelican-bike` does for its sprite runs), instead of relying on
|
|
356
|
-
line flow inside it.
|
|
357
|
-
|
|
358
|
-
Not yet implemented: margin collapsing, multi-line flex wrapping, `z-index`
|
|
359
|
-
stacking, `overflow` clipping and floats.
|
|
360
|
-
|
|
361
|
-
**Document** (`runtime/ext_gui/document.{h,cpp}`): owns the DOM tree, gathers
|
|
362
|
-
`<style>` text into one stylesheet, computes styles and layout for a viewport and
|
|
363
|
-
offers `querySelector`/`querySelectorAll`/`styleOf`/`boxOf`.
|
|
364
|
-
|
|
365
|
-
### Implemented paint (M4a/M4b)
|
|
366
|
-
|
|
367
|
-
`runtime/ext_gui/paint.{h,cpp}` walks the layout tree in painter's order and
|
|
368
|
-
emits a backend-agnostic `DisplayList` with two parallel lists: **rectangles**
|
|
369
|
-
(backgrounds and four solid border edges, with `border-radius`) and **text runs**
|
|
370
|
-
(each carrying its text, colour and resolved `FontSpec`). Keeping them separate
|
|
371
|
-
lets the renderer draw all rectangles, then all text on top, with one draw call
|
|
372
|
-
per list. `DisplayList::dump()` feeds `paintList()`/`paintCount()`.
|
|
373
|
-
|
|
374
|
-
`opacity` is honoured only at its extremes: a box whose computed `opacity` is
|
|
375
|
-
`0` (and its whole subtree) paints nothing, and anything above `0` paints fully
|
|
376
|
-
opaque. A shape carries its colour's alpha only, so there is no compositing of a
|
|
377
|
-
partly transparent box — the `opacity` transitions the CSS parser accepts are
|
|
378
|
-
therefore not visible either. Hiding and revealing a layer with `opacity: 0` on
|
|
379
|
-
the hidden state is what `examples/gui/pelican-bike`'s game-over overlay does.
|
|
380
|
-
|
|
381
|
-
`runtime/ext_gui/renderer.{h,cpp}` turns that list into two batched vertex
|
|
382
|
-
buffers per window (one for shapes, one for glyph quads) drawn through two
|
|
383
|
-
SDL_GPU graphics pipelines:
|
|
384
|
-
|
|
385
|
-
- The shared pipelines are created lazily from the device's supported shader
|
|
386
|
-
format. Each of SDL_GPU's three backends consumes a different binary and none
|
|
387
|
-
of them compiles GLSL/HLSL at runtime, so the engine ships all three and
|
|
388
|
-
`selectShader` (`renderer_shaders.h`) picks the one the device accepts:
|
|
389
|
-
**MSL** for Metal (compiled by SDL from the embedded source),
|
|
390
|
-
**SPIR-V** for Vulkan (`runtime/ext_gui/spirv/*.{vert,frag}`, compiled by
|
|
391
|
-
`glslc`), and **DXIL** for Direct3D 12 (`shaders.hlsl`, compiled by `dxc`).
|
|
392
|
-
`scripts/build-gui-shaders.mjs` regenerates the embedded blobs and the result
|
|
393
|
-
is committed, so nothing extra is needed to build the engine.
|
|
394
|
-
The descriptor bindings differ per format — SPIR-V follows what
|
|
395
|
-
`SDL_gpu_vulkan.c` builds (uniforms `set=1, binding=0`, samplers
|
|
396
|
-
`set=2, binding=0`, textures `set=2, binding=1`) — and glslc names every entry
|
|
397
|
-
point `main` where MSL/DXIL keep the descriptive names.
|
|
398
|
-
Note that SDL 3.2.10's Direct3D 12 backend cannot create a graphics pipeline
|
|
399
|
-
whose shaders declare a uniform buffer (it fails with `E_INVALIDARG`); the
|
|
400
|
-
engine passes the viewport that way, so the DXIL path is built but not usable
|
|
401
|
-
until that is resolved upstream.
|
|
402
|
-
- A **rounded-rectangle distance field** in the shape fragment shader gives
|
|
403
|
-
antialiased fills; the vertex carries `position`, `local`, `half extents`,
|
|
404
|
-
`radius` and colour, and a viewport-size push constant does the projection.
|
|
405
|
-
Alpha blending is enabled.
|
|
406
|
-
- The text pipeline samples a **single shared grayscale glyph atlas**
|
|
407
|
-
(`R8_UNORM`, 2048², shelf-packed, LINEAR filtering) and draws each glyph as a
|
|
408
|
-
textured quad (`position`, `uv`, colour), modulating alpha by the coverage.
|
|
409
|
-
- Geometry is uploaded only when the document or viewport changes
|
|
410
|
-
(`geometry.dirty`), so steady-state frames are bind-and-draw.
|
|
411
|
-
|
|
412
|
-
The window background (`setBackground`) is the render-pass clear colour.
|
|
413
|
-
|
|
414
|
-
Still to do in M4: gradients.
|
|
415
|
-
|
|
416
|
-
### Implemented input (M5)
|
|
417
|
-
|
|
418
|
-
`LayoutTree::hitTest` returns the deepest box containing a point (probing later
|
|
419
|
-
siblings first so the topmost element wins), and `xt_dom_describe` turns the
|
|
420
|
-
element into the `div#id.class` descriptor carried by event payloads.
|
|
421
|
-
|
|
422
|
-
- SDL pointer/wheel/key events are routed to the owning window, hit tested, and
|
|
423
|
-
delivered to the handlers registered with `on`. `mousedown` also moves focus.
|
|
424
|
-
- `:hover` matches the hovered element **and its ancestors** (so hovering a child
|
|
425
|
-
lights up its parents); `:focus` matches the focused element. When either
|
|
426
|
-
changes, `XtDocument::setHover`/`setFocus` recompute styles and layout and mark
|
|
427
|
-
the window's geometry dirty, so the change is painted on the next frame.
|
|
428
|
-
- `win.hitTest(x, y)` and `win.sendEvent(type, options)` expose hit testing and
|
|
429
|
-
synthetic input so the whole path is testable headlessly (the e2e suite drives
|
|
430
|
-
clicks, wheels and keys without a real mouse).
|
|
431
|
-
|
|
432
|
-
Still to do for full input: text selection, drag, IME and clipboard.
|
|
433
|
-
|
|
434
|
-
### Implemented images (M6a)
|
|
435
|
-
|
|
436
|
-
`runtime/ext_gui/image.{h,cpp}` wraps the vendored **stb_image** header
|
|
437
|
-
decoding PNG/JPEG/BMP/GIF/TGA to RGBA8, with a decode cache and a header-only
|
|
438
|
-
size cache (`stbi_info`). Paths accept a `file://` prefix and percent-encoding.
|
|
439
|
-
|
|
440
|
-
- `<img>` is a replaced element (`display: inline-block`): layout gives it the
|
|
441
|
-
CSS size when set, otherwise the intrinsic pixel size, and preserves the
|
|
442
|
-
aspect ratio when only one axis is constrained. The intrinsic size is read
|
|
443
|
-
from the file header (no full decode) while building the box tree.
|
|
444
|
-
- Paint emits one `PaintImage` per `<img>`; the renderer decodes/uploads each
|
|
445
|
-
unique `src` to an RGBA texture (cached by path) and draws textured quads,
|
|
446
|
-
batching consecutive quads that share a texture.
|
|
447
|
-
|
|
448
|
-
Still to do for images: CSS `background-image: url(...)`, `data:` URIs,
|
|
449
|
-
`object-fit` and 9-slice borders.
|
|
450
|
-
|
|
451
|
-
### Implemented transitions (M6b)
|
|
452
|
-
|
|
453
|
-
`XtDocument` runs CSS transitions between the *target* computed styles (the
|
|
454
|
-
cascade for the current `:hover`/`:focus` state) and the *displayed* styles used
|
|
455
|
-
for layout and paint. When a state change alters a transitioned property, a
|
|
456
|
-
running transition is recorded and re-applied every frame until it finishes;
|
|
457
|
-
the engine advances the clock with real frame deltas (`XtDocument::advance`),
|
|
458
|
-
and `win.advance(ms)` lets tests step it deterministically.
|
|
459
|
-
|
|
460
|
-
- Supported properties: `background-color`, `color`, `border-color`,
|
|
461
|
-
`border-radius`; `transition: all` covers them. Structural/layout-affecting
|
|
462
|
-
properties are not animated yet (that would relayout every frame).
|
|
463
|
-
- Both the `transition` shorthand and the `transition-property` / `-duration` /
|
|
464
|
-
`-delay` / `-timing-function` longhands are parsed; time values accept `s` and
|
|
465
|
-
`ms`; timing functions are `linear`, `ease`, `ease-in`, `ease-out` and
|
|
466
|
-
`ease-in-out` (`ease` is a smoothstep approximation).
|
|
467
|
-
- Retargeting mid-flight starts a new transition from the current interpolated
|
|
468
|
-
value, so reversing a hover animates smoothly from wherever it was.
|
|
469
|
-
- `computedStyle()` reports the displayed (interpolated) value, so transitions
|
|
470
|
-
are directly observable in tests.
|
|
471
|
-
|
|
472
|
-
Still to do for animation: `@keyframes` animations and `cubic-bezier(...)`.
|
|
473
|
-
|
|
474
|
-
### Implemented text stack (M4b)
|
|
475
|
-
|
|
476
|
-
`runtime/ext_gui/text.{h,cpp}` wraps **HarfBuzz** (shaping) and **FreeType**
|
|
477
|
-
(metrics + eventual rasterisation). Both are built statically and linked into
|
|
478
|
-
`gui.a` by `scripts/build-gui.ts`.
|
|
479
|
-
|
|
480
|
-
- Fonts are resolved from well-known system paths (Helvetica/Arial on macOS,
|
|
481
|
-
DejaVu/Liberation on Linux, Segoe UI/Arial on Windows), overridable with
|
|
482
|
-
`XT_GUI_FONT` (and `XT_GUI_FONT_MONO`), and cached per `(family class, size)`.
|
|
483
|
-
Only regular upright faces are used for now; weight/italic selection is a
|
|
484
|
-
later refinement.
|
|
485
|
-
- The default candidates prefer a **CJK-capable** face (Microsoft YaHei on
|
|
486
|
-
Windows, PingFang on macOS, Noto Sans CJK/WenQuanYi on Linux) before the
|
|
487
|
-
Latin-only ones, because a run is shaped with a *single* face: without this,
|
|
488
|
-
Chinese/Japanese/Korean text renders as blank or `.notdef` boxes. Set
|
|
489
|
-
`XT_GUI_FONT` to a Latin-only face to get its Latin design instead (at the
|
|
490
|
-
cost of CJK coverage).
|
|
491
|
-
- `xt_text_measure_width` shapes the run with HarfBuzz (so kerning and
|
|
492
|
-
ligatures are honoured), `xt_text_metrics` returns FreeType's ascent /
|
|
493
|
-
descent / normal line height. When no font file can be found the module falls
|
|
494
|
-
back to a deterministic per-byte approximation, so layout still works.
|
|
495
|
-
- `xt_text_shape_run` returns positioned glyphs and `xt_text_rasterize` renders
|
|
496
|
-
an 8-bit bitmap; the renderer packs those into the atlas. On HiDPI displays
|
|
497
|
-
glyphs are rasterised at `font_size * SDL_GetWindowPixelDensity` while quads
|
|
498
|
-
are positioned in logical pixels, so text stays crisp.
|
|
499
|
-
- Layout uses these real metrics for text widths, line breaking and
|
|
500
|
-
`line-height: normal`; `measureText`/`fontMetrics` expose them to tests.
|
|
501
|
-
|
|
502
|
-
Multiple windows fall out of the object model: `createWindow` returns a native
|
|
503
|
-
object handle; each handle owns its own `SDL_Window`/GPU surface and its own DOM
|
|
504
|
-
tree. `run()` drives one shared main loop that ticks every window and exits when
|
|
505
|
-
the last one closes.
|
|
506
|
-
|
|
507
|
-
## Event loop integration
|
|
508
|
-
|
|
509
|
-
The runtime change (already landed):
|
|
510
|
-
|
|
511
|
-
- `int xt_loop_poll(int timeout_ms)` — one reactor iteration; `0` polls without
|
|
512
|
-
blocking, `< 0` blocks.
|
|
513
|
-
- `void xt_loop_set_main(xt_main_loop_fn fn)` — a host may take over the main
|
|
514
|
-
loop. `xt_run_event_loop()` delegates to it; the generated `main` is unchanged.
|
|
515
|
-
- `xt_loop_set_main(NULL)` restores the default `select(2)` loop.
|
|
516
|
-
|
|
517
|
-
The GUI engine can either register its own loop through `xt_loop_set_main`, or
|
|
518
|
-
expose an explicit `run()`; **M2 uses the explicit `run()`** so the window is a
|
|
519
|
-
plain native call the TypeScript program controls. Each tick it:
|
|
520
|
-
|
|
521
|
-
1. pumps SDL window/input events for every window,
|
|
522
|
-
2. builds/uploads the display list when it changed and renders every open
|
|
523
|
-
window (clear pass + shape geometry + text geometry),
|
|
524
|
-
3. calls `xt_loop_poll(0)` and `xt_drain_microtasks()` so sockets/timers and
|
|
525
|
-
`await` continuations keep making progress,
|
|
526
|
-
4. repeats until all windows close or `quit()` is called.
|
|
527
|
-
|
|
528
|
-
The `xt_loop_set_main` hook remains available for hosts that want to own the
|
|
529
|
-
loop themselves.
|
|
530
|
-
|
|
531
|
-
This keeps network I/O, timers and `await` working inside a GUI program.
|
|
532
|
-
|
|
533
|
-
## Native ↔ TS bridge
|
|
534
|
-
|
|
535
|
-
- **TS → engine**: direct `xt_gui_*` calls / window methods.
|
|
536
|
-
- **Engine → TS**: `xt_call_with_this(fn, thisValue, argc, argv)` with function
|
|
537
|
-
values captured from TS (e.g. event handlers registered with `win.on(...)`).
|
|
538
|
-
- **Future page→native RPC**: not needed while there is no page JS; native TS is
|
|
539
|
-
the controller. If a declarative layer is added later, it will use the same
|
|
540
|
-
`xt_call_*` entry points.
|
|
541
|
-
|
|
542
|
-
## Packaging & build
|
|
543
|
-
|
|
544
|
-
- Sources live in `runtime/ext_gui/` (C/C++) plus vendored libs under `vendor/`
|
|
545
|
-
(gitignored; fetched on demand).
|
|
546
|
-
- `npm run gui` (`scripts/build-gui.ts`) fetches a pinned SDL3 (`SDL3_TAG`,
|
|
547
|
-
default `release-3.2.10`), builds a static SDL3, fetches and builds static
|
|
548
|
-
FreeType (`FREETYPE_VERSION`) and HarfBuzz (`HARFBUZZ_VERSION`), compiles the
|
|
549
|
-
engine and merges everything into `runtime/lib/<os>-<arch>/gui.a` (or
|
|
550
|
-
`gui.lib` with the MSVC ABI on Windows), same convention as `core.a`,
|
|
551
|
-
statically bundling all three. The merge uses `libtool` on macOS and
|
|
552
|
-
`ar -M` (GNU/LLVM) elsewhere; CMake archives are discovered under the build
|
|
553
|
-
root or `Release/` for either generator style.
|
|
554
|
-
- `src/extensions/gui/index.ts` exposes the archive through `nativeObjects()`
|
|
555
|
-
(via `findRuntimeLibrary`, so `.a`/`.lib` both work) and the OS frameworks
|
|
556
|
-
through `linkerFlags()`.
|
|
557
|
-
- CI builds `gui.a` before assembling the release archive so it ships inside
|
|
558
|
-
`runtime/lib/<slug>/` (the release tarball copies the whole `runtime/` tree).
|
|
559
|
-
The archive is built and the example is run on Linux (under Xvfb, with the
|
|
560
|
-
lavapipe software Vulkan driver) and macOS; Windows is provisional.
|
|
561
|
-
- The existing cache fingerprint already hashes `nativeObjects()` contents, so
|
|
562
|
-
rebuilding `gui.a` invalidates cached binaries automatically.
|
|
563
|
-
|
|
564
|
-
### Running the example
|
|
565
|
-
|
|
566
|
-
```sh
|
|
567
|
-
npm run runtime # rebuild core.a after a runtime change
|
|
568
|
-
npm run gui # build runtime/lib/<os>-<arch>/gui.a (fetches SDL3 once)
|
|
569
|
-
xbintsc run examples/gui/hello.ts --ext gui
|
|
570
|
-
```
|
|
571
|
-
|
|
572
|
-
Two examples live here: `hello.ts` (HTML/CSS + a reactive counter) and
|
|
573
|
-
[`pelican-bike/`](../examples/gui/pelican-bike), a playable 2D game that uses
|
|
574
|
-
positioned layers, CSS sprites, `requestAnimationFrame` and keyboard input — see
|
|
575
|
-
its README for controls and the geometry report it prints.
|
|
576
|
-
|
|
577
|
-
Set `XT_GUI_AUTOCLOSE_MS=<n>` to close all windows after `n` milliseconds,
|
|
578
|
-
which the e2e test (`tests/e2e/gui-*.test.ts`) uses to run headlessly.
|
|
579
|
-
|
|
580
|
-
Those suites are skipped on Windows by default (the archive is built there but
|
|
581
|
-
the run is not validated in CI). With a `gui.lib` and a working GPU, opt in with
|
|
582
|
-
`xbintsc_GUI_TESTS=1`:
|
|
583
|
-
|
|
584
|
-
```bat
|
|
585
|
-
set xbintsc_GUI_TESTS=1
|
|
586
|
-
npx vitest run tests/e2e/gui-layout.test.ts tests/e2e/gui-example.test.ts
|
|
587
|
-
```
|
|
588
|
-
|
|
589
|
-
On a headless Linux box, install the SDL3 build headers and run under Xvfb with a
|
|
590
|
-
software Vulkan driver:
|
|
591
|
-
|
|
592
|
-
```sh
|
|
593
|
-
sudo apt-get install -y clang cmake libx11-dev libxext-dev libxrandr-dev \
|
|
594
|
-
libxcursor-dev libxi-dev libxinerama-dev libxfixes-dev libxkbcommon-dev \
|
|
595
|
-
libwayland-dev wayland-protocols libdecor-0-dev libasound2-dev libpulse-dev \
|
|
596
|
-
libdbus-1-dev libudev-dev libdrm-dev libgbm-dev libgl1-mesa-dev \
|
|
597
|
-
libegl1-mesa-dev libvulkan-dev mesa-vulkan-drivers xvfb
|
|
598
|
-
npm run runtime && npm run gui
|
|
599
|
-
xvfb-run -a --server-args="-screen 0 1280x720x24" \
|
|
600
|
-
npx tsx src/cli/main.ts run examples/gui/hello.ts --ext gui
|
|
601
|
-
```
|
|
602
|
-
|
|
603
|
-
Those are the packages the CI `compile-examples` job installs. The X11 backend is
|
|
604
|
-
used by default; Wayland is enabled too but not yet exercised.
|
|
605
|
-
|
|
606
129
|
## Milestones
|
|
607
130
|
|
|
608
131
|
1. **M1 — foundation** ✅
|
|
@@ -633,8 +156,10 @@ used by default; Wayland is enabled too but not yet exercised.
|
|
|
633
156
|
restyle (`css.*`, `style.*`, `document.*`, `gui.cpp`), plus the
|
|
634
157
|
`hitTest`/`sendEvent` test hooks.
|
|
635
158
|
6. **M6 — images, then CSS transitions/animations**
|
|
636
|
-
- **M6a — images** ✅ stb_image decode
|
|
637
|
-
|
|
159
|
+
- **M6a — images** ✅ stb_image decode (bitmaps) and NanoSVG rasterisation
|
|
160
|
+
(SVG files, at the drawn pixel size), `<img>` replaced-element layout,
|
|
161
|
+
per-file GPU textures and textured quads (`image.*`, `svg.*`, `paint.*`,
|
|
162
|
+
`renderer.*`).
|
|
638
163
|
- **M6b — transitions/animations** ✅ `transition` shorthand + longhands,
|
|
639
164
|
animated `background-color`/`color`/`border-color`/`border-radius`, retargeting
|
|
640
165
|
and `win.advance(ms)`. `@keyframes` remain.
|
|
@@ -650,7 +175,7 @@ used by default; Wayland is enabled too but not yet exercised.
|
|
|
650
175
|
keyed by `scripts/build-gui.ts`.
|
|
651
176
|
- Windows (build + run) stays provisional until the MSVC-compatible
|
|
652
177
|
`gui.lib` and D3D12/DXIL shader path are validated; a failure is logged
|
|
653
|
-
without annotating the run (see
|
|
178
|
+
without annotating the run (see [Open questions](./gui/known-issues.md#open-questions)).
|
|
654
179
|
8. **M8 — interactive DOM** ✅
|
|
655
180
|
- Element/document handles with stable identity, read/write properties via
|
|
656
181
|
runtime accessors, traversal/attributes/queries, mutation with lazy
|
|
@@ -697,7 +222,24 @@ used by default; Wayland is enabled too but not yet exercised.
|
|
|
697
222
|
so a leaking or spiking program cannot spiral (`runtime/xt_alloc.c`).
|
|
698
223
|
`XT_GC_TRACE=1` reports every collection.
|
|
699
224
|
- `win.stats()` exposes the engine counters; e2e coverage in
|
|
700
|
-
`tests/e2e/gui-retention.test.ts` (see
|
|
225
|
+
`tests/e2e/gui-retention.test.ts` (see [Memory & GC](./gui/runtime.md#memory--gc)).
|
|
226
|
+
13. **M13 — video & audio** ✅
|
|
227
|
+
- `<video>` / `<audio>` decode through a statically vendored minimal FFmpeg
|
|
228
|
+
(`media.*`, `gui_media.cpp`), frames upload to GPU textures through the
|
|
229
|
+
existing image pipeline, audio is pushed to an SDL stream on the main
|
|
230
|
+
thread, and the DOM media API plus `loadedmetadata`/`play`/`pause`/
|
|
231
|
+
`ended`/`timeupdate`/`error` events are exposed on element handles
|
|
232
|
+
(`dom_api_media.cpp`, `dom_api_proto.cpp`). Controlled from TS + CSS;
|
|
233
|
+
there are no built-in controls. Example in `examples/gui/media.*`, e2e in
|
|
234
|
+
`tests/e2e/gui-media.test.ts`.
|
|
235
|
+
14. **M14 — `<canvas>` 2D** ✅
|
|
236
|
+
- `<canvas>` is a replaced element (`xtgui::Canvas2D`, `layout.*`), the
|
|
237
|
+
`getContext("2d")` handle and drawing API live in `dom_api_canvas.cpp`,
|
|
238
|
+
the self-written anti-aliased software rasteriser in `canvas_raster.cpp`,
|
|
239
|
+
and the per-window lifetime in `gui_canvas.cpp`. The bitmap uploads to a
|
|
240
|
+
GPU texture through the image path (`renderer_image.cpp`) and the display
|
|
241
|
+
list carries a `canvas_id` (`paint.*`). See [Canvas](./gui/canvas.md);
|
|
242
|
+
e2e in `tests/e2e/gui-canvas.test.ts`.
|
|
701
243
|
|
|
702
244
|
## Progress log
|
|
703
245
|
|
|
@@ -722,7 +264,9 @@ used by default; Wayland is enabled too but not yet exercised.
|
|
|
722
264
|
`hitTest`/`sendEvent` test hooks, e2e coverage.
|
|
723
265
|
- **M6a** ✅ image decoding (`image.*`, vendored stb_image), `<img>` intrinsic
|
|
724
266
|
sizing in layout, `PaintImage` in the display list, per-file RGBA textures and
|
|
725
|
-
an image pipeline in `renderer.*`, e2e coverage.
|
|
267
|
+
an image pipeline in `renderer.*`, e2e coverage. SVG files are decoded too
|
|
268
|
+
(`svg.*`, vendored NanoSVG): a vector source is rasterised at the drawn device
|
|
269
|
+
pixel size and cached by that size, so scaling and HiDPI stay crisp.
|
|
726
270
|
- **M6b** ✅ CSS transitions (parsing in `style.*`, an animation clock and
|
|
727
271
|
transition state in `document.*`, `advance`/`advance(ms)` hooks, e2e coverage).
|
|
728
272
|
- **M7** ✅ builds `gui.a` in CI on Linux and macOS (required) and runs the
|
|
@@ -762,99 +306,23 @@ used by default; Wayland is enabled too but not yet exercised.
|
|
|
762
306
|
inert, and the collector's trigger follows the live set instead of only
|
|
763
307
|
ratcheting upwards. `win.stats()` exposes the engine counters and
|
|
764
308
|
`tests/e2e/gui-retention.test.ts` pins them; a game loop rewriting its sprite
|
|
765
|
-
every frame now runs in constant memory (see
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
leak signature to look for first; `tests/e2e/gui-retention.test.ts` asserts that
|
|
785
|
-
the usual mutation patterns leave all of them unchanged.
|
|
786
|
-
|
|
787
|
-
Two engine details matter when reading those numbers:
|
|
788
|
-
|
|
789
|
-
- **What releases nodes.** `innerHTML`/`inner`/`textContent`/`replaceChild`
|
|
790
|
-
release the subtrees they drop; `removeChild`/`remove` keep them (see "Node
|
|
791
|
-
lifetime"). A game that rebuilds a sprite with `innerHTML` therefore runs in
|
|
792
|
-
constant memory.
|
|
793
|
-
- **What can still accumulate.** A node created with `createElement` and never
|
|
794
|
-
attached stays in the pool, because the collector has no weak references and
|
|
795
|
-
the engine cannot tell that the script dropped its handle. The same applies to
|
|
796
|
-
a node removed and then forgotten by the script. Both are bounded by the number
|
|
797
|
-
of *live* handles, but a program that creates elements per frame without
|
|
798
|
-
attaching them grows the pool with the frame count.
|
|
799
|
-
|
|
800
|
-
The collector behind `heapBytes` is a non-moving mark-sweep (`runtime/xt_alloc.c`).
|
|
801
|
-
Its trigger follows the live set: after every collection the threshold becomes
|
|
802
|
-
twice the surviving heap (floor 1 MiB), so a small live set means small, frequent
|
|
803
|
-
collections and a bounded resident set. `XT_GC_THRESHOLD=<bytes>` pins the
|
|
804
|
-
threshold instead, and `XT_GC_TRACE=1` prints one line per collection (heap and
|
|
805
|
-
live-allocation counts before and after, the next trigger, and the live set by
|
|
806
|
-
object kind) to stderr — that is how the DOM leak was traced to the node pool.
|
|
807
|
-
|
|
808
|
-
## Known issues
|
|
809
|
-
|
|
810
|
-
- **Intermittent crash at auto-close.** `XT_GUI_AUTOCLOSE_MS` can end a run
|
|
811
|
-
with a native access violation in roughly two runs out of five; a clean exit
|
|
812
|
-
looks identical otherwise. Windows reports it as the NTSTATUS exit code
|
|
813
|
-
`0xC0000005`; on Linux/macOS the process dies with a signal — an access
|
|
814
|
-
violation is `SIGSEGV`, so `spawnSync` reports `status: null` plus a
|
|
815
|
-
`signal`. It only reproduces with a *running animation-frame loop* (a static
|
|
816
|
-
document closes fine) and it is **not** the game's own code: no exception is
|
|
817
|
-
reported, the crash usually lands inside the first second regardless of the
|
|
818
|
-
deadline, and raising `XT_GC_THRESHOLD` hides it, so it looks like a
|
|
819
|
-
GC/teardown race. Interactive runs (no auto-close) are stable for minutes.
|
|
820
|
-
A leading cause was that the engine cached `xt_value`s in C++ containers
|
|
821
|
-
the mark-sweep collector cannot see — the AOT `<script>` registry, element
|
|
822
|
-
handles, element listeners and the queued/currently-running
|
|
823
|
-
`requestAnimationFrame` callbacks. They are now marked by GC root providers
|
|
824
|
-
(`xt_gui_script_gc_scan` and `xt_gui_gc_scan_roots`), which removes that
|
|
825
|
-
class of use-after-free. Since the M12 node-lifetime work it has not
|
|
826
|
-
reproduced at all: 44 auto-close runs of `examples/gui/pelican-bike` (deadlines
|
|
827
|
-
800 ms–4 s, with the default, a 1 MiB and a 64 MiB collector threshold, where
|
|
828
|
-
the old report said roughly two in five) all exited cleanly, which points at
|
|
829
|
-
the same root cause — a detached-node pool and handle table that grew with the
|
|
830
|
-
frame count while the collector's threshold ratcheted up. It stays listed
|
|
831
|
-
because the archive is built per platform and the failure is timing-sensitive;
|
|
832
|
-
`tests/e2e/gui-example.test.ts` still treats either crash spelling as an
|
|
833
|
-
acceptable outcome, and the behavioural assertions live in
|
|
834
|
-
`tests/e2e/gui-example-browser.test.ts` (no GPU, deterministic).
|
|
835
|
-
- **`display: none -> flex` restyle.** A hidden element being shown by a
|
|
836
|
-
*class change* on a flex container has also been observed to crash the same
|
|
837
|
-
way; `examples/gui/pelican-bike` reveals its game-over overlay with `opacity`
|
|
838
|
-
instead.
|
|
839
|
-
|
|
840
|
-
## Open questions
|
|
309
|
+
every frame now runs in constant memory (see [Memory & GC](./gui/runtime.md#memory--gc)).
|
|
310
|
+
- **M13** ✅ `<video>` / `<audio>` support. A minimal static FFmpeg is vendored
|
|
311
|
+
and merged into `gui.a` (`scripts/build-gui.ts`), `media.*` decodes video to
|
|
312
|
+
RGBA frames and audio to 48 kHz stereo PCM on the main thread, `gui_media.cpp`
|
|
313
|
+
owns the players and pumps the media clock and events each frame,
|
|
314
|
+
`renderer_image.cpp` uploads each new video frame through the image texture
|
|
315
|
+
path, and `dom_api_media.cpp` exposes `play`/`pause`/`load` plus the media
|
|
316
|
+
properties on element handles. `examples/gui/media.*` demonstrates the
|
|
317
|
+
TS-plus-CSS-controls pattern and `tests/e2e/gui-media.test.ts` pins it.
|
|
318
|
+
- **M14** ✅ `<canvas>` support. A new replaced element (`layout.*`) carries an
|
|
319
|
+
intrinsic size from its `width`/`height` attributes (default `300x150`),
|
|
320
|
+
`xtgui::Canvas2D` (`canvas.{h,cpp}`) owns the RGBA8 bitmap and drawing state,
|
|
321
|
+
`canvas_raster.cpp` is a self-written anti-aliased software rasteriser
|
|
322
|
+
(4 sub-scanlines + analytic horizontal coverage, non-zero winding), the
|
|
323
|
+
`getContext("2d")` handle and its API are bound in `dom_api_canvas.cpp`, and
|
|
324
|
+
`gui_canvas.cpp` owns the per-window lifetime. The bitmap uploads through the
|
|
325
|
+
image texture path (`renderer_image.cpp`) and the display list carries a
|
|
326
|
+
`canvas_id` (`paint.*`). See [Canvas](./gui/canvas.md); e2e in
|
|
327
|
+
`tests/e2e/gui-canvas.test.ts`.
|
|
841
328
|
|
|
842
|
-
- Whether Linux ships X11, Wayland, or both in the first cut. (Decision:
|
|
843
|
-
X11 first, Wayland later.)
|
|
844
|
-
- **SVG:** there is no SVG support and none is planned. The HTML parser knows
|
|
845
|
-
elements and attributes, the layout engine knows boxes, and paint knows
|
|
846
|
-
rectangles/text/images, so `<svg>` trees would need a whole second
|
|
847
|
-
geometry+paint path. An SVG *file* works only through the image loader
|
|
848
|
-
(`<img src="logo.svg">`), which needs an SVG rasteriser (`stb_image` does not
|
|
849
|
-
decode SVG); vector art therefore has to arrive as a raster (PNG) or be drawn
|
|
850
|
-
from boxes, as `examples/gui/pelican-bike` does.
|
|
851
|
-
- Windows: an MSVC-compatible `gui.lib` (COFF objects + `ar -M`/`llvm-ar`) is
|
|
852
|
-
produced by `scripts/build-gui.ts`, but it has not been validated in CI yet,
|
|
853
|
-
so the Windows step is provisional (the job stays green) and `package` skips
|
|
854
|
-
it. It also needs an SDL3 build with the D3D12/DXIL backend (DXIL requires
|
|
855
|
-
`dxc`).
|
|
856
|
-
- **D3D12 shaders:** the SPIR-V (Vulkan) and DXIL (Direct3D 12) blobs are built
|
|
857
|
-
and embedded, so the non-Metal path no longer skips geometry. DXIL remains
|
|
858
|
-
unusable on **SDL 3.2.10**: its D3D12 backend rejects any graphics pipeline
|
|
859
|
-
whose shaders declare a uniform buffer, and the engine passes the viewport that
|
|
860
|
-
way. Re-check after an SDL upgrade.
|