xbintsc 0.3.68 → 0.3.79

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/README.md +59 -4
  2. package/README.zh-CN.md +52 -2
  3. package/dist/tests/e2e/gui-advanced.test.d.ts +6 -0
  4. package/dist/tests/e2e/gui-advanced.test.js +261 -0
  5. package/dist/tests/e2e/gui-advanced.test.js.map +1 -0
  6. package/dist/tests/e2e/gui-canvas.test.d.ts +9 -0
  7. package/dist/tests/e2e/gui-canvas.test.js +233 -0
  8. package/dist/tests/e2e/gui-canvas.test.js.map +1 -0
  9. package/dist/tests/e2e/gui-example.test.d.ts +1 -1
  10. package/dist/tests/e2e/gui-example.test.js +2 -2
  11. package/dist/tests/e2e/gui-helpers.d.ts +3 -0
  12. package/dist/tests/e2e/gui-helpers.js +5 -0
  13. package/dist/tests/e2e/gui-helpers.js.map +1 -1
  14. package/dist/tests/e2e/gui-layout.test.js +73 -0
  15. package/dist/tests/e2e/gui-layout.test.js.map +1 -1
  16. package/dist/tests/e2e/gui-media.test.d.ts +8 -0
  17. package/dist/tests/e2e/gui-media.test.js +119 -0
  18. package/dist/tests/e2e/gui-media.test.js.map +1 -0
  19. package/dist/tests/e2e/gui-paint.test.js +40 -0
  20. package/dist/tests/e2e/gui-paint.test.js.map +1 -1
  21. package/doc/ai/extensions.md +16 -3
  22. package/doc/ai/zh-CN/extensions.md +10 -3
  23. package/doc/gui/animation.md +46 -0
  24. package/doc/gui/api.md +92 -0
  25. package/doc/gui/build.md +91 -0
  26. package/doc/gui/canvas.md +98 -0
  27. package/doc/gui/dom.md +123 -0
  28. package/doc/gui/input.md +22 -0
  29. package/doc/gui/known-issues.md +66 -0
  30. package/doc/gui/layout.md +72 -0
  31. package/doc/gui/media.md +87 -0
  32. package/doc/gui/rendering.md +98 -0
  33. package/doc/gui/runtime.md +88 -0
  34. package/doc/gui-scripts.md +7 -3
  35. package/doc/gui.md +80 -612
  36. package/doc/implemented.md +1 -0
  37. package/doc/zh-CN/gui/animation.md +46 -0
  38. package/doc/zh-CN/gui/api.md +88 -0
  39. package/doc/zh-CN/gui/build.md +84 -0
  40. package/doc/zh-CN/gui/canvas.md +80 -0
  41. package/doc/zh-CN/gui/dom.md +114 -0
  42. package/doc/zh-CN/gui/input.md +21 -0
  43. package/doc/zh-CN/gui/known-issues.md +50 -0
  44. package/doc/zh-CN/gui/layout.md +60 -0
  45. package/doc/zh-CN/gui/media.md +71 -0
  46. package/doc/zh-CN/gui/rendering.md +81 -0
  47. package/doc/zh-CN/gui/runtime.md +76 -0
  48. package/doc/zh-CN/gui-scripts.md +6 -2
  49. package/doc/zh-CN/gui.md +64 -532
  50. package/doc/zh-CN/implemented.md +1 -0
  51. package/doc/zh-CN/post/xbintsc-build-pelican.md +2 -2
  52. package/llms.txt +15 -2
  53. package/package.json +1 -1
  54. package/runtime/ext_gui/canvas.cpp +580 -0
  55. package/runtime/ext_gui/canvas.h +170 -0
  56. package/runtime/ext_gui/canvas_internal.h +59 -0
  57. package/runtime/ext_gui/canvas_raster.cpp +254 -0
  58. package/runtime/ext_gui/document.cpp +24 -1
  59. package/runtime/ext_gui/document.h +11 -0
  60. package/runtime/ext_gui/dom_api.cpp +3 -0
  61. package/runtime/ext_gui/dom_api_canvas.cpp +548 -0
  62. package/runtime/ext_gui/dom_api_internal.h +73 -0
  63. package/runtime/ext_gui/dom_api_media.cpp +274 -0
  64. package/runtime/ext_gui/dom_api_proto.cpp +80 -0
  65. package/runtime/ext_gui/gui.cpp +6 -0
  66. package/runtime/ext_gui/gui_canvas.cpp +95 -0
  67. package/runtime/ext_gui/gui_engine.h +49 -0
  68. package/runtime/ext_gui/gui_events.cpp +21 -5
  69. package/runtime/ext_gui/gui_media.cpp +193 -0
  70. package/runtime/ext_gui/image.cpp +31 -12
  71. package/runtime/ext_gui/image.h +20 -7
  72. package/runtime/ext_gui/layout.cpp +52 -5
  73. package/runtime/ext_gui/layout.h +33 -1
  74. package/runtime/ext_gui/layout_flex.cpp +263 -99
  75. package/runtime/ext_gui/layout_flow.cpp +85 -9
  76. package/runtime/ext_gui/layout_flow_internal.h +84 -5
  77. package/runtime/ext_gui/layout_inline.cpp +54 -37
  78. package/runtime/ext_gui/media.cpp +514 -0
  79. package/runtime/ext_gui/media.h +104 -0
  80. package/runtime/ext_gui/nanosvg.h +3278 -0
  81. package/runtime/ext_gui/nanosvgrast.h +1472 -0
  82. package/runtime/ext_gui/paint.cpp +123 -38
  83. package/runtime/ext_gui/paint.h +36 -6
  84. package/runtime/ext_gui/renderer.cpp +113 -10
  85. package/runtime/ext_gui/renderer.h +49 -9
  86. package/runtime/ext_gui/renderer_image.cpp +203 -77
  87. package/runtime/ext_gui/renderer_internal.h +6 -8
  88. package/runtime/ext_gui/renderer_text.cpp +11 -26
  89. package/runtime/ext_gui/style.h +11 -0
  90. package/runtime/ext_gui/style_print.cpp +11 -0
  91. package/runtime/ext_gui/style_ua.cpp +7 -0
  92. package/runtime/ext_gui/style_values.cpp +40 -1
  93. package/runtime/ext_gui/svg.cpp +182 -0
  94. package/runtime/ext_gui/svg.h +46 -0
  95. package/runtime/ext_gui/text.cpp +156 -25
  96. package/runtime/ext_gui/text.h +11 -2
  97. package/runtime/ext_gui/window.cpp +4 -1
  98. package/scripts/build-gui.ts +148 -6
  99. package/scripts/check-file-length.ts +5 -1
  100. package/scripts/install-gui-deps.sh +28 -0
@@ -67,10 +67,23 @@ import { createWindow, run } from "gui";
67
67
 
68
68
  It is a self-contained GPU-accelerated renderer (own HTML parser, CSS cascade,
69
69
  layout and compositor) — not a system WebView. It ships as a per-platform
70
- prebuilt `gui.a`/`gui.lib` because it is C++. If that archive is missing for
70
+ prebuilt `gui.a`/`gui.lib` because it is C++. `<img>`, `<video>` and `<audio>`
71
+ really decode: a minimal static FFmpeg is bundled into the archive, and the DOM
72
+ media API (`play`/`pause`/`currentTime`/`volume`/…) plus the
73
+ `loadedmetadata`/`play`/`pause`/`ended`/`timeupdate`/`error` events let a
74
+ program drive playback. There are no built-in controls — draw your own chrome
75
+ with elements and CSS (see `examples/gui/media.ts`). Video frames and audio PCM
76
+ are decoded on the main thread. `<canvas>` is supported too: `getContext("2d")`
77
+ returns a context whose fills, paths, text and `drawImage` are rasterised by a
78
+ self-written CPU rasteriser, and element handles expose `canvas` drawing plus
79
+ `width`/`height`. If that archive is missing for
71
80
  your platform, the build fails with an actionable message; design and roadmap
72
- live in [../gui.md](../gui.md), and the scripting surface in
73
- [../gui-scripts.md](../gui-scripts.md).
81
+ live in [../gui.md](../gui.md) (overview; the engine is documented by function
82
+ under [`doc/gui/`](../gui/)), and the scripting surface in
83
+ [../gui-scripts.md](../gui-scripts.md). Media playback is the one part that
84
+ Windows does not get: the Windows `gui.a` is built without FFmpeg and media
85
+ slots are inert. See [../gui/canvas.md](../gui/canvas.md) for the canvas API and
86
+ its limits.
74
87
 
75
88
  ## Third-party npm packages
76
89
 
@@ -64,9 +64,16 @@ import { createWindow, run } from "gui";
64
64
  ```
65
65
 
66
66
  它是自带渲染引擎的 GPU 加速实现(自己的 HTML 解析器、CSS 级联、布局与合成器),不是
67
- 系统 WebView。因为主体是 C++,它以各平台预编译的 `gui.a`/`gui.lib` 形式分发。如果该
68
- 平台的归档不存在,构建会以明确的提示失败;设计与路线图见 [../../zh-CN/gui.md](../../zh-CN/gui.md),
69
- 脚本接口见 [../../zh-CN/gui-scripts.md](../../zh-CN/gui-scripts.md)。
67
+ 系统 WebView。`<img>`、`<video>`、`<audio>` 都是真正解码:归档内打入一个最小化的
68
+ 静态 FFmpeg,DOM 媒体 API(`play`/`pause`/`currentTime`/`volume`/…)与
69
+ `loadedmetadata`/`play`/`pause`/`ended`/`timeupdate`/`error` 事件可让程序驱动播放。
70
+ 没有内置控件——用元素和 CSS 自绘控件(见 `examples/gui/media.ts`);视频帧与音频 PCM
71
+ 都在主线程解码。也支持 `<canvas>`:`getContext("2d")` 返回的上下文,其填充、路径、
72
+ 文本与 `drawImage` 由自研 CPU 光栅器处理,元素句柄还暴露 `width`/`height`。因为主体是 C++,它以各平台预编译的 `gui.a`/`gui.lib` 形式分发。如果该
73
+ 平台的归档不存在,构建会以明确的提示失败;设计与路线图见 [../../zh-CN/gui.md](../../zh-CN/gui.md)(总览;引擎按功能拆分在 [`doc/zh-CN/gui/`](../../zh-CN/gui/) 下),
74
+ 脚本接口见 [../../zh-CN/gui-scripts.md](../../zh-CN/gui-scripts.md)。媒体播放是
75
+ Windows 唯一不支持的:Windows 的 `gui.a` 不构建 FFmpeg,媒体槽位为空操作。Canvas API
76
+ 及其限制见 [../../zh-CN/gui/canvas.md](../../zh-CN/gui/canvas.md)。
70
77
 
71
78
  ## 第三方 npm 包
72
79
 
@@ -0,0 +1,46 @@
1
+ # Animation: frames and transitions
2
+
3
+ > Part of the [GUI extension](../gui.md); the engine overview, locked
4
+ > decisions and roadmap live there.
5
+
6
+ ## Animation frames (M10)
7
+
8
+ `requestAnimationFrame` runs a callback once on the next frame; the callback
9
+ receives the frame timestamp (ms) and may mutate the DOM, which is restyled and
10
+ repainted in the same frame. Re-queue from inside the callback to animate:
11
+
12
+ ```ts
13
+ const win = createWindow({ title: "anim", width: 320, height: 240 });
14
+ let n = 0;
15
+ const tick = (t: number) => {
16
+ win.document.getElementById("label").textContent = String(n++);
17
+ if (n < 120) win.requestAnimationFrame(tick); // id returned; cancelAnimationFrame(id) drops it
18
+ };
19
+ win.on("ready", () => win.requestAnimationFrame(tick));
20
+ win.loadHTML("<div id='label'>0</div>");
21
+ run();
22
+ ```
23
+
24
+ ## CSS transitions (M6b)
25
+
26
+ `XtDocument` runs CSS transitions between the *target* computed styles (the
27
+ cascade for the current `:hover`/`:focus` state) and the *displayed* styles used
28
+ for layout and paint. When a state change alters a transitioned property, a
29
+ running transition is recorded and re-applied every frame until it finishes;
30
+ the engine advances the clock with real frame deltas (`XtDocument::advance`),
31
+ and `win.advance(ms)` lets tests step it deterministically.
32
+
33
+ - Supported properties: `background-color`, `color`, `border-color`,
34
+ `border-radius`; `transition: all` covers them. Structural/layout-affecting
35
+ properties are not animated yet (that would relayout every frame).
36
+ - Both the `transition` shorthand and the `transition-property` / `-duration` /
37
+ `-delay` / `-timing-function` longhands are parsed; time values accept `s` and
38
+ `ms`; timing functions are `linear`, `ease`, `ease-in`, `ease-out` and
39
+ `ease-in-out` (`ease` is a smoothstep approximation).
40
+ - Retargeting mid-flight starts a new transition from the current interpolated
41
+ value, so reversing a hover animates smoothly from wherever it was.
42
+ - `computedStyle()` reports the displayed (interpolated) value, so transitions
43
+ are directly observable in tests.
44
+
45
+ Still to do for animation: `@keyframes` animations and `cubic-bezier(...)`.
46
+
package/doc/gui/api.md ADDED
@@ -0,0 +1,92 @@
1
+ # GUI API
2
+
3
+ > Part of the [GUI extension](../gui.md); the engine overview, locked
4
+ > decisions and roadmap live there.
5
+
6
+ ## TS-facing API
7
+
8
+ ```ts
9
+ import { createWindow, run, quit } from "gui";
10
+
11
+ const win = createWindow({ title: "Demo", width: 900, height: 600 });
12
+ win.setBackground("#14161c");
13
+ win.loadHTML(INDEX_HTML); // parses HTML/CSS and computes styles
14
+ win.on("ready", () => console.log("first frame presented"));
15
+ win.on("close", () => console.log("window closed"));
16
+
17
+ run(); // drives the main loop until all windows close
18
+ ```
19
+
20
+ Methods implemented on a window handle: `setTitle` / `setSize` / `loadHTML` /
21
+ `getHTML` / `setBackground` / `close` / `isOpen` / `on` / `off`, plus `driver()`,
22
+ which reports the SDL_GPU backend in use (`"vulkan"`, `"direct3d12"`, `"metal"`,
23
+ … — SDL picks the best one the device supports). The Web spellings are available
24
+ too: `addEventListener` / `removeEventListener` are aliases of `on` / `off` (their
25
+ `options` argument is accepted and ignored) and the `closed` getter mirrors
26
+ `isOpen()`, so a document written against the browser API runs unchanged.
27
+ Events emitted:
28
+ `ready` (after the first presented frame), `load`, `close`, `resize` and the
29
+ input events `mousemove`, `mousedown`, `mouseup`, `click`, `wheel`, `keydown`,
30
+ `keyup`.
31
+
32
+ On Windows a GUI program is linked as a **Windows-subsystem** executable, so
33
+ launching it does not flash a console window. That also means stdout/stderr go
34
+ nowhere when it is started from Explorer; set `app.console: true` in
35
+ `xbintsc.config.json` to keep the console subsystem (useful while debugging).
36
+
37
+ Input handlers receive a single payload object (lifecycle handlers receive none):
38
+
39
+ ```ts
40
+ win.on("click", (e) => console.log(e.x, e.y, e.button, e.target));
41
+ win.on("wheel", (e) => console.log(e.deltaX, e.deltaY));
42
+ win.on("keydown", (e) => console.log(e.key, e.code, e.ctrl, e.shift, e.alt, e.meta));
43
+ ```
44
+
45
+ | Field | Events | Meaning |
46
+ | --- | --- | --- |
47
+ | `x`, `y` / `clientX`, `clientY` | pointer, wheel | viewport-relative logical pixels |
48
+ | `button` | pointer | `0` left, `1` middle, `2` right (`-1` for move) |
49
+ | `clicks` | pointer | click count reported by the OS |
50
+ | `deltaX`, `deltaY` | wheel | scroll amount (`deltaY` positive = down) |
51
+ | `key`, `code` | keyboard | key name and physical scancode name |
52
+ | `repeat`, `ctrl`, `shift`, `alt`, `meta` | keyboard | modifiers |
53
+ | `target` | pointer, wheel | deepest element under the point as a CSS-like descriptor (`div#main.card`), or absent |
54
+
55
+ ## Diagnostics
56
+
57
+ So the HTML/CSS/paint pipeline is testable without a GPU, a window handle
58
+ exposes read-only hooks:
59
+
60
+ ```ts
61
+ win.computedStyle(selector, property) // e.g. ("#main", "width") -> "60%"
62
+ win.queryCount(selector) // number of matching elements
63
+ win.getBoundingClientRect(selector) // { x, y, width, height } (border box)
64
+ win.documentTree() // serialized DOM (debugging)
65
+ win.layoutTree() // serialized layout boxes (debugging)
66
+ win.paintCount() // number of shapes in the display list
67
+ win.paintList() // serialized display list (debugging)
68
+ win.measureText(text, fontSize?, family?) // shaped advance width in pixels
69
+ win.fontMetrics(fontSize?, family?) // { ascent, descent, lineHeight, ready }
70
+ win.hitTest(x, y) // deepest element descriptor, or ""
71
+ win.sendEvent(type, options?) // synthesise input (testing)
72
+ win.advance(ms) // step the CSS transition clock (testing)
73
+ win.stats() // engine/runtime counters (memory debugging)
74
+ ```
75
+
76
+ `win.stats()` returns `{ heapBytes, heapAllocations, treeNodes, detachedRoots,`
77
+ `detachedNodes, handles, handleSlots, listeners }`: the live runtime heap, the
78
+ size of the DOM tree, the detached-node pool, and the element-handle tables. In a
79
+ steady state every one of them is constant — a climbing `detachedNodes` or
80
+ `handles` means the program removes nodes faster than it releases them (see
81
+ [Node lifetime](./dom.md#node-lifetime)).
82
+
83
+ They are used by `tests/e2e/gui-*.test.ts` to assert parsing, selector matching,
84
+ specificity, inheritance, `!important` and layout geometry. They will stay useful
85
+ afterwards for debugging.
86
+
87
+ ## Multiple windows
88
+
89
+ Multiple windows fall out of the object model: `createWindow` returns a native
90
+ object handle; each handle owns its own `SDL_Window`/GPU surface and its own DOM
91
+ tree. `run()` drives one shared main loop that ticks every window and exits when
92
+ the last one closes.
@@ -0,0 +1,91 @@
1
+ # Packaging and build
2
+
3
+ > Part of the [GUI extension](../gui.md); the engine overview, locked
4
+ > decisions and roadmap live there.
5
+
6
+ - Sources live in `runtime/ext_gui/` (C/C++) plus vendored libs under `vendor/`
7
+ (gitignored; fetched on demand).
8
+ - `npm run gui` (`scripts/build-gui.ts`) fetches a pinned SDL3 (`SDL3_TAG`,
9
+ default `release-3.2.10`), builds a static SDL3, fetches and builds static
10
+ FreeType (`FREETYPE_VERSION`) and HarfBuzz (`HARFBUZZ_VERSION`), compiles the
11
+ engine and merges everything into `runtime/lib/<os>-<arch>/gui.a` (or
12
+ `gui.lib` with the MSVC ABI on Windows), same convention as `core.a`,
13
+ statically bundling all three. It also fetches, configures and builds a
14
+ minimal static **FFmpeg** (`FFMPEG_VERSION`, default `7.1`) with only the
15
+ demuxers/parsers/decoders the media elements need and merges those archives
16
+ into `gui.a`, defining `XT_GUI_WITH_FFMPEG` for the engine. The merge uses
17
+ `libtool` on macOS and `ar -M` (GNU/LLVM) elsewhere; CMake archives are
18
+ discovered under the build root or `Release/` for either generator style.
19
+ - `src/extensions/gui/index.ts` exposes the archive through `nativeObjects()`
20
+ (via `findRuntimeLibrary`, so `.a`/`.lib` both work) and the OS frameworks
21
+ through `linkerFlags()`.
22
+ - CI builds `gui.a` before assembling the release archive so it ships inside
23
+ `runtime/lib/<slug>/` (the release tarball copies the whole `runtime/` tree).
24
+ The archive is built and the example is run on Linux (under Xvfb, with the
25
+ lavapipe software Vulkan driver) and macOS; Windows is provisional.
26
+ - The existing cache fingerprint already hashes `nativeObjects()` contents, so
27
+ rebuilding `gui.a` invalidates cached binaries automatically.
28
+ - CMake build trees live *next to* their sources (`vendor/sdl-build`,
29
+ `vendor/freetype-build`, `vendor/harfbuzz-build`), never inside them: a
30
+ `CMakeCache.txt` records the machine it was configured on, and SDL's video
31
+ backends are `option()`s, so the first configure to run wins. A cached
32
+ `vendor/SDL` tree that also carried `build/` therefore restored one machine's
33
+ `SDL_X11`/`SDL_WAYLAND` choices into another's configure — SDL then aborts
34
+ with "SDL could not find X11 or Wayland development libraries" even though the
35
+ headers are installed. The CI cache is keyed per OS/arch and omits the build
36
+ trees; `npm run gui` also checks the X11/Wayland headers up front on
37
+ Debian/Ubuntu and reports the missing development packages by name (set
38
+ `xbintsc_SKIP_HEADER_CHECK=1` when they live under a prefix that check does
39
+ not know).
40
+
41
+ ## Running the example
42
+
43
+ ```sh
44
+ npm run runtime # rebuild core.a after a runtime change
45
+ npm run gui # build runtime/lib/<os>-<arch>/gui.a (fetches SDL3 once)
46
+ xbintsc run examples/gui/hello.ts --ext gui
47
+ ```
48
+
49
+ Two examples live here: `hello.ts` (HTML/CSS + a reactive counter) and
50
+ [`pelican-bike/`](../../examples/gui/pelican-bike), a playable 2D game that uses
51
+ positioned layers, CSS sprites, `requestAnimationFrame` and keyboard input — see
52
+ its README for controls and the geometry report it prints.
53
+
54
+ `media.ts` (with `media.html`) demonstrates `<video>`/`<audio>`: it overlays a
55
+ CSS progress bar on the video and drives playback with `play()` / `pause()` /
56
+ `currentTime` / `volume`. Media `src` paths resolve relative to the process
57
+ working directory, so run it from the repository root:
58
+ `xbintsc run examples/gui/media.ts --ext gui`.
59
+
60
+ Set `XT_GUI_AUTOCLOSE_MS=<n>` to close all windows after `n` milliseconds,
61
+ which the e2e test (`tests/e2e/gui-*.test.ts`) uses to run headlessly.
62
+
63
+ Those suites are skipped on Windows by default (the archive is built there but
64
+ the run is not validated in CI). With a `gui.lib` and a working GPU, opt in with
65
+ `xbintsc_GUI_TESTS=1`:
66
+
67
+ ```bat
68
+ set xbintsc_GUI_TESTS=1
69
+ npx vitest run tests/e2e/gui-layout.test.ts tests/e2e/gui-example.test.ts
70
+ ```
71
+
72
+ On a headless Linux box, install the SDL3 build headers and run under Xvfb with a
73
+ software Vulkan driver:
74
+
75
+ ```sh
76
+ sudo sh scripts/install-gui-deps.sh # clang, cmake, X11/Wayland headers, Vulkan, Xvfb
77
+ npm run runtime && npm run gui
78
+ xvfb-run -a --server-args="-screen 0 1280x720x24" \
79
+ npx tsx src/cli/main.ts run examples/gui/hello.ts --ext gui
80
+ ```
81
+
82
+ `scripts/install-gui-deps.sh` holds the package list (`apt-get install -y clang
83
+ cmake libx11-dev libxext-dev libxrandr-dev libxcursor-dev libxi-dev
84
+ libxinerama-dev libxfixes-dev libxkbcommon-dev libwayland-dev wayland-protocols
85
+ libdecor-0-dev libasound2-dev libpulse-dev libdbus-1-dev libudev-dev libdrm-dev
86
+ libgbm-dev libgl1-mesa-dev libegl1-mesa-dev libvulkan-dev mesa-vulkan-drivers
87
+ xvfb`) so that every Linux job which builds `gui.a` installs the same set: the
88
+ X11 backend is used by default, Wayland is enabled too but not yet exercised.
89
+ Install it before `npm run gui` anywhere the archive is built — SDL3 cannot
90
+ find a window backend without those headers.
91
+
@@ -0,0 +1,98 @@
1
+ # Canvas: `<canvas>` and the 2D context (M14)
2
+
3
+ > Part of the [GUI extension](../gui.md); the engine overview, locked
4
+ > decisions and roadmap live there.
5
+
6
+ `<canvas>` is a **replaced element** laid out like `<img>`: its intrinsic size
7
+ comes from the `width`/`height` attributes (default `300x150`, the CSS default
8
+ canvas size), an explicit CSS `width`/`height` wins, and the aspect ratio is
9
+ preserved when only one axis is constrained. Paint emits one `PaintImage` per
10
+ canvas, carrying a `canvas_id` instead of a `src`/`media_id`; the renderer
11
+ uploads the bitmap to an RGBA texture through the same image path as bitmaps and
12
+ video frames (`renderer_image.cpp`), so the GPU scales it into the CSS box.
13
+
14
+ The engine-side object is `xtgui::Canvas2D`
15
+ (`runtime/ext_gui/canvas.{h,cpp}`) — one RGBA8 bitmap plus the 2D drawing state.
16
+ The rasteriser lives in `canvas_raster.cpp`; the DOM bindings live in
17
+ `dom_api_canvas.cpp`, and the per-window lifetime in `gui_canvas.cpp`.
18
+
19
+ ## Getting a context
20
+
21
+ ```ts
22
+ const canvas: any = document.getElementById("c");
23
+ const ctx = canvas.getContext("2d"); // or null for any other context kind
24
+ ```
25
+
26
+ `getContext("2d")` returns a context handle whose methods and accessors live on
27
+ a shared prototype. The handle is cached on the element, so every call returns
28
+ the **same** object and the drawing state (transform, styles, path) is per
29
+ canvas, not per call. `ctx.canvas` is the owning element handle.
30
+
31
+ ## Drawing model
32
+
33
+ The rasteriser is deliberately small and self-written (no Skia/Cairo). It
34
+ anti-aliases with **4 vertical sub-scanlines** per pixel row and an **analytic
35
+ horizontal coverage** pass, and fills paths with the **non-zero winding rule**
36
+ (so `arc`-shaped holes work when wound the other way). An axis-aligned rectangle
37
+ (`b == 0 && c == 0` in the transform) takes a direct coverage fast path.
38
+ Blending is non-premultiplied source-over, scaled by `globalAlpha`.
39
+
40
+ Canvas pixels are **not** touched by the garbage collector: a canvas is owned by
41
+ the window, keyed by its DOM node (the same shape as the `<video>`/`<audio>`
42
+ decode table). It is released when the node is dropped (`loadHTML`, window
43
+ close/quit, or a mutation that discards the subtree), which also releases the
44
+ matching GPU texture.
45
+
46
+ ## Supported API
47
+
48
+ State and transforms: `save` / `restore` (which restore the transform, line
49
+ width, alpha, font/text settings **and** the fill/stroke styles),
50
+ `translate`, `rotate`, `scale`, `transform(a,b,c,d,e,f)`, `setTransform`,
51
+ `resetTransform`.
52
+
53
+ Styles and settings: `fillStyle` / `strokeStyle` (CSS colors understood by the
54
+ cascade's color parser), `lineWidth`, `globalAlpha`, `font` (a CSS font
55
+ shorthand resolving to family, size and bold/italic — default
56
+ `10px sans-serif`), `textAlign` (`start`/`left`, `center`, `right`/`end`) and
57
+ `textBaseline` (`alphabetic`, `top`, `middle`, `bottom`, `hanging`,
58
+ `ideographic`). Reading a property returns the value last assigned; assigning an
59
+ unparseable color keeps the previous one.
60
+
61
+ Paths: `beginPath`, `closePath`, `moveTo`, `lineTo`, `rect`, `arc`,
62
+ `quadraticCurveTo`, `bezierCurveTo`, then `fill` (multi-loop, non-zero winding)
63
+ or `stroke`. Bezier and quadratic segments are flattened into line segments;
64
+ arcs are flattened too and each subpath is stroked as segment quads plus a disc
65
+ at each joint.
66
+
67
+ Rectangles: `clearRect`, `fillRect`, `strokeRect`.
68
+
69
+ Text: `measureText(text)` (returns `{ width }`), `fillText(text, x, y)` and
70
+ `strokeText(text, x, y)`. Text is shaped with HarfBuzz and rasterised with
71
+ FreeType (the same stack as HTML text) at the transform's approximate scale.
72
+ **Only translation and uniform scale are honoured** — a rotated or skewed
73
+ transform still places each glyph at its transformed baseline point but does not
74
+ rotate the glyph bitmaps. `strokeText` currently fills with the stroke style
75
+ rather than producing an outline.
76
+
77
+ Images: `drawImage(source, …)` in the 3-argument (`dx, dy`), 5-argument
78
+ (`dx, dy, dw, dh`) and 9-argument (`dx, dy, dw, dh, sx, sy, sw, sh`) forms.
79
+ `source` may be an `<img>` element (decoded through the image module) or another
80
+ `<canvas>` element (its current bitmap). Sampling is nearest-neighbour, so a
81
+ large upscale of a small source is blocky — fine for icons and pixel art, not a
82
+ photo pipeline.
83
+
84
+ Test hook: `getImageData(x, y, w, h)` copies a region to
85
+ `{ width, height, data }` where `data` is a row-major RGBA8 byte array.
86
+ Out-of-bounds pixels are transparent.
87
+
88
+ ## Not implemented
89
+
90
+ Gradients and patterns, shadows, `clip()`, `globalCompositeOperation`, line
91
+ joins/caps/dashes/mitre, `setLineDash`, `createLinearGradient`/`createPattern`,
92
+ `putImageData`, `createImageData`, `isPointInPath`, `getTransform`,
93
+ `setTransform` with a `DOMMatrix`, `drawImage` with an image smoothing toggle,
94
+ `toDataURL`/`toBlob`, `<canvas>` as a CSS `background-image`, and offscreen
95
+ canvases (`OffscreenCanvas`, `transferControlToOffscreen`).
96
+
97
+ See also [Media](./media.md) for the image/video/audio decode stack that
98
+ `drawImage` and the texture upload share.
package/doc/gui/dom.md ADDED
@@ -0,0 +1,123 @@
1
+ # DOM, HTML, CSS and AOT scripts
2
+
3
+ > Part of the [GUI extension](../gui.md); the engine overview, locked
4
+ > decisions and roadmap live there. The scripting surface has its own
5
+ > document, [gui-scripts.md](../gui-scripts.md).
6
+
7
+ ## HTML/CSS subset (M3a)
8
+
9
+ **HTML parser** (`runtime/ext_gui/dom.{h,cpp}`): tags/attributes/text,
10
+ comments and doctype skipped, entity decoding (named + numeric/hex), void
11
+ elements, raw-text elements (`<style>`/`<script>`), and the common implicit
12
+ close rules (`li`, `dt`/`dd`, `option`, `p`, headings, table cells/rows).
13
+
14
+ **CSS parser** (`runtime/ext_gui/css.{h,cpp}`): comments and at-rules skipped
15
+ (for now), rules with multiple selectors, and declarations. Selectors: type,
16
+ `.class`, `#id`, attribute (`=`, `~=`, `|=`, `^=`, `$=`, `*=`), the four
17
+ combinators (descendant, child, adjacent and general sibling) and the
18
+ pseudo-classes `:first-child`, `:last-child`, `:only-child`, `:empty`,
19
+ `:root`, `:not(...)`, `:nth-child(an+b)`, `:disabled`, `:checked`, plus the
20
+ stateful `:hover` and `:focus` (see [Input](./input.md)). Values:
21
+ lengths (`px`, `%`, `em`, `rem`, `vw`, `vh`, `pt`, `pc`, `in`, `cm`, `mm`, `q`),
22
+ colors (hex, `rgb()`/`rgba()`, a named subset), numbers, keywords and shorthands
23
+ (`margin`/`padding`/`border`/`flex`).
24
+
25
+ **Cascade & computed style** (`runtime/ext_gui/style.{h,cpp}`): a small built-in
26
+ UA stylesheet, author rules sorted by `(!important, specificity, source order)`,
27
+ then inline `style=""` (highest specificity, but non-`!important` inline loses to
28
+ `!important`), plus CSS inheritance of the text properties. Relative lengths are
29
+ kept unresolved until layout, except `font-size` (resolved against the *parent*
30
+ font size) and `line-height`.
31
+
32
+ ## Interactive DOM (M8)
33
+
34
+ `win.document` returns the document handle; element handles have stable identity
35
+ and read/write properties, attributes, traversal and geometry:
36
+
37
+ ```ts
38
+ const doc = win.document;
39
+ const box = doc.querySelector("#box");
40
+ const inner = doc.querySelector("#inner");
41
+ inner.textContent = "hi"; // write
42
+ inner.classList.add("hot");
43
+ inner.style.setProperty("color", "#0f0");
44
+ inner.setAttribute("data-role", "lead");
45
+ console.log(inner.id, inner.tagName, doc.querySelector("#box") === box);
46
+
47
+ const created = doc.createElement("div");
48
+ created.textContent = "added";
49
+ box.appendChild(created);
50
+ box.removeChild(created);
51
+
52
+ inner.addEventListener("click", (e) => console.log(e.target.id, e.currentTarget.id));
53
+ inner.click(); // synthesise a click at the element
54
+ console.log(box.offsetWidth, box.offsetHeight); // rounded border box
55
+ console.log(box.contains(inner)); // descendant test
56
+ ```
57
+
58
+ Mutations mark the document dirty; the engine restyles + relayouts lazily (before
59
+ the next read or frame). `inner = …`/`innerHTML = …` do **not** run scripts.
60
+ Element events support capture and bubble phases, `stopPropagation`, `once`, and
61
+ bubble up to `document`/`window`. The legacy `win.on(type, fn)` payload keeps its
62
+ **string** `e.target` (`div#id.class`); the element `Event.target` is a handle
63
+ whose descriptor matches the same string.
64
+
65
+ ### Node lifetime
66
+
67
+ A mutation decides whether the nodes it removes can still be reached, and the
68
+ engine releases exactly those it cannot:
69
+
70
+ | Mutation | Removed nodes |
71
+ | --- | --- |
72
+ | `innerHTML = …`, `inner = …`, `textContent = …`, `replaceChild` | **released** — the script has no way to refer to them again |
73
+ | `removeChild`, `remove` | **kept** in the document's detached pool — the DOM keeps a removed node alive and usable, so it can be re-attached |
74
+
75
+ Released nodes free their C++ subtree as well as their handle, so the counters
76
+ from `win.stats()` (`detachedNodes`, `handles`, `handleSlots`) return to their
77
+ previous values. That is what makes the usual game pattern — rewriting a sprite
78
+ with `innerHTML` every frame — run in constant memory; before, every replaced
79
+ subtree stayed pooled forever and a loop writing ten nodes per frame leaked
80
+ several megabytes per second.
81
+
82
+ The one case the engine cannot see is a node the script creates and then drops
83
+ without ever attaching it: `document.createElement("div")` parks the node in the
84
+ pool *and* keeps its handle reachable, because the mark-sweep collector has no
85
+ weak references, so the engine cannot tell that the script threw the handle
86
+ away. A loop that creates elements without attaching them therefore grows the
87
+ pool. Reuse the elements (or build the content with `innerHTML`, which releases
88
+ what it replaces) instead of creating fresh ones per frame.
89
+
90
+ ## AOT scripts (M9)
91
+
92
+ Import an `.html` file that contains inline `<script lang="ts">` bodies; the
93
+ loader compiles each body into a native function and `win.loadHTML(page)` runs
94
+ them once the document is parsed (no JavaScript engine, no runtime `eval`):
95
+
96
+ ```ts
97
+ import { createWindow, run } from "gui";
98
+ import page from "./page.html";
99
+
100
+ const win = createWindow({ title: "counter", width: 320, height: 240 });
101
+ win.loadHTML(page);
102
+ run();
103
+ ```
104
+
105
+ ```html
106
+ <button id="b">0</button>
107
+ <script lang="ts">
108
+ const b = document.getElementById("b");
109
+ let n = 0;
110
+ b.addEventListener("click", () => { b.textContent = String(++n); });
111
+ </script>
112
+ ```
113
+
114
+ The two parameters (`window`, `document`) are ordinary function arguments, so
115
+ script locals need no global-object machinery. External scripts work too:
116
+ `<script src="./counter.ts">` is read at compile time, its imports are hoisted
117
+ (rewritten to resolve from the HTML file) and its body is wrapped the same way —
118
+ so a script module can `import` helpers and still see `document`. Scripts are
119
+ **compile-time assets**: HTML created at runtime (`innerHTML`, fetched over the
120
+ network) never executes, and `src` URLs (`https://…`, `data:…`) are ignored.
121
+ Everything runs in document order after parsing (effectively deferred). See
122
+ `doc/gui-scripts.md` for the full design.
123
+
@@ -0,0 +1,22 @@
1
+ # Input and events (M5)
2
+
3
+ > Part of the [GUI extension](../gui.md); the engine overview, locked
4
+ > decisions and roadmap live there. The public event API is in
5
+ > [api.md](./api.md).
6
+
7
+ `LayoutTree::hitTest` returns the deepest box containing a point (probing later
8
+ siblings first so the topmost element wins), and `xt_dom_describe` turns the
9
+ element into the `div#id.class` descriptor carried by event payloads.
10
+
11
+ - SDL pointer/wheel/key events are routed to the owning window, hit tested, and
12
+ delivered to the handlers registered with `on`. `mousedown` also moves focus.
13
+ - `:hover` matches the hovered element **and its ancestors** (so hovering a child
14
+ lights up its parents); `:focus` matches the focused element. When either
15
+ changes, `XtDocument::setHover`/`setFocus` recompute styles and layout and mark
16
+ the window's geometry dirty, so the change is painted on the next frame.
17
+ - `win.hitTest(x, y)` and `win.sendEvent(type, options)` expose hit testing and
18
+ synthetic input so the whole path is testable headlessly (the e2e suite drives
19
+ clicks, wheels and keys without a real mouse).
20
+
21
+ Still to do for full input: text selection, drag, IME and clipboard.
22
+
@@ -0,0 +1,66 @@
1
+ # Known issues and open questions
2
+
3
+ > Part of the [GUI extension](../gui.md); the engine overview, locked
4
+ > decisions and roadmap live there.
5
+
6
+ ## Known issues
7
+
8
+ - **Intermittent crash at auto-close.** `XT_GUI_AUTOCLOSE_MS` can end a run
9
+ with a native access violation in roughly two runs out of five; a clean exit
10
+ looks identical otherwise. Windows reports it as the NTSTATUS exit code
11
+ `0xC0000005`; on Linux/macOS the process dies with a signal — an access
12
+ violation is `SIGSEGV`, so `spawnSync` reports `status: null` plus a
13
+ `signal`. It only reproduces with a *running animation-frame loop* (a static
14
+ document closes fine) and it is **not** the game's own code: no exception is
15
+ reported, the crash usually lands inside the first second regardless of the
16
+ deadline, and raising `XT_GC_THRESHOLD` hides it, so it looks like a
17
+ GC/teardown race. Interactive runs (no auto-close) are stable for minutes.
18
+ A leading cause was that the engine cached `xt_value`s in C++ containers
19
+ the mark-sweep collector cannot see — the AOT `<script>` registry, element
20
+ handles, element listeners and the queued/currently-running
21
+ `requestAnimationFrame` callbacks. They are now marked by GC root providers
22
+ (`xt_gui_script_gc_scan` and `xt_gui_gc_scan_roots`), which removes that
23
+ class of use-after-free. Since the M12 node-lifetime work it has not
24
+ reproduced at all: 44 auto-close runs of `examples/gui/pelican-bike` (deadlines
25
+ 800 ms–4 s, with the default, a 1 MiB and a 64 MiB collector threshold, where
26
+ the old report said roughly two in five) all exited cleanly, which points at
27
+ the same root cause — a detached-node pool and handle table that grew with the
28
+ frame count while the collector's threshold ratcheted up. It stays listed
29
+ because the archive is built per platform and the failure is timing-sensitive;
30
+ `tests/e2e/gui-example.test.ts` still treats either crash spelling as an
31
+ acceptable outcome, and the behavioural assertions live in
32
+ `tests/e2e/gui-example-browser.test.ts` (no GPU, deterministic).
33
+ - **`display: none -> flex` restyle.** A hidden element being shown by a
34
+ *class change* on a flex container has also been observed to crash the same
35
+ way; `examples/gui/pelican-bike` reveals its game-over overlay with `opacity`
36
+ instead.
37
+
38
+ ## Open questions
39
+
40
+ - Whether Linux ships X11, Wayland, or both in the first cut. (Decision:
41
+ X11 first, Wayland later.)
42
+ - **SVG:** SVG *files* are supported through the image loader
43
+ (`<img src="logo.svg">`): `runtime/ext_gui/svg.cpp` wraps the vendored
44
+ **NanoSVG** parser/rasteriser and `image.{h,cpp}` treats an SVG as a vector
45
+ image, rasterised at the drawn pixel size. Inline `<svg>` **trees** in a
46
+ document are still not supported: the HTML parser knows elements and
47
+ attributes, the layout engine knows boxes, and paint knows
48
+ rectangles/text/images, so `<svg>` subtrees would need a whole second
49
+ geometry+paint path. Vector art therefore has to arrive as an SVG/PNG file or
50
+ be drawn from boxes, as `examples/gui/pelican-bike` does.
51
+ - Windows: an MSVC-compatible `gui.lib` (COFF objects + `ar -M`/`llvm-ar`) is
52
+ produced by `scripts/build-gui.ts`, but it has not been validated in CI yet,
53
+ so the Windows step is provisional (the job stays green) and `package` skips
54
+ it. It also needs an SDL3 build with the D3D12/DXIL backend (DXIL requires
55
+ `dxc`).
56
+ - **Media on Windows:** the media decoders (FFmpeg) are only built and merged
57
+ into the POSIX `gui.a`; the Windows `gui.lib` is produced without them, so
58
+ `<video>`/`<audio>` are inert there (failures fire `error`). Enabling it needs
59
+ an MSVC-compatible FFmpeg build and is deferred until the Windows GUI path
60
+ itself is validated. `<video controls>` is likewise deferred — the intended
61
+ pattern is TS + a CSS overlay, as in `examples/gui/media.ts`.
62
+ - **D3D12 shaders:** the SPIR-V (Vulkan) and DXIL (Direct3D 12) blobs are built
63
+ and embedded, so the non-Metal path no longer skips geometry. DXIL remains
64
+ unusable on **SDL 3.2.10**: its D3D12 backend rejects any graphics pipeline
65
+ whose shaders declare a uniform buffer, and the engine passes the viewport that
66
+ way. Re-check after an SDL upgrade.