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
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Layout
|
|
2
|
+
|
|
3
|
+
> Part of the [GUI extension](../gui.md); the engine overview, locked
|
|
4
|
+
> decisions and roadmap live there.
|
|
5
|
+
|
|
6
|
+
## Layout engine (M3)
|
|
7
|
+
|
|
8
|
+
`runtime/ext_gui/layout.{h,cpp}` turns the styled DOM into a `LayoutBox` tree
|
|
9
|
+
with absolute (viewport-relative) geometry:
|
|
10
|
+
|
|
11
|
+
- **Block flow** — block-level children stack vertically; `display: none`
|
|
12
|
+
generates no box; `width: auto` fills the containing block, `height: auto`
|
|
13
|
+
wraps the content. The box model (margin/padding/border) is resolved,
|
|
14
|
+
including percentages against the containing block width. Adjoining vertical
|
|
15
|
+
margins between siblings collapse to `max(positive) + min(negative)`; a
|
|
16
|
+
parent's bottom margin does not collapse with its last child's (see the
|
|
17
|
+
limitations below).
|
|
18
|
+
- **Inline flow** — consecutive inline-level children form an anonymous inline
|
|
19
|
+
formatting context with greedy, word-based line breaking, `text-align` and
|
|
20
|
+
`line-height`. Inline elements get the union of their descendants' geometry;
|
|
21
|
+
`display: inline-block` is laid out atomically with a shrink-to-fit width.
|
|
22
|
+
Each line shrinks to the band left free by any floats (`float: left|right`)
|
|
23
|
+
that overlap it, so later lines wrap around them.
|
|
24
|
+
Each text fragment remembers the run it covers, so paint can shape it. Text is
|
|
25
|
+
measured with the HarfBuzz/FreeType stack (see [Text stack](./rendering.md#text-stack-m4b)).
|
|
26
|
+
- **Flexbox** — `row`/`column` (and the `-reverse` variants) with `gap`,
|
|
27
|
+
`flex-basis`/`flex-grow`/`flex-shrink`, `justify-content` and `align-items`
|
|
28
|
+
(including `stretch` when the cross size is definite). `flex-wrap: wrap`
|
|
29
|
+
breaks items into several lines, `align-content`
|
|
30
|
+
(`stretch`/`flex-start`/`flex-end`/`center`/`space-between`/`space-around`)
|
|
31
|
+
distributes the lines on the cross axis and `wrap-reverse` flips that axis.
|
|
32
|
+
Item margins are part of the item's outer size on both axes, so a negative
|
|
33
|
+
`margin-top` pulls an item up instead of being ignored. Whitespace-only text
|
|
34
|
+
between items (source indentation) does not become a flex item.
|
|
35
|
+
- **Positioning** — `position: relative` offsets a box (and its subtree) by
|
|
36
|
+
`top`/`right`/`bottom`/`left` without changing the space it reserved;
|
|
37
|
+
`position: absolute` (and `fixed`, which resolves against the viewport) takes
|
|
38
|
+
the box out of flow and places it against the padding box of the nearest
|
|
39
|
+
positioned ancestor — the viewport when there is none. An `auto` inset keeps
|
|
40
|
+
the static position on that axis, an `auto` width is shrink-to-fit unless both
|
|
41
|
+
`left` and `right` are set (then it fills the space between them), and
|
|
42
|
+
percentages resolve against the containing block. Stacking is approximated by
|
|
43
|
+
sorting each parent's children by `z-index` (negative → `auto` → positive)
|
|
44
|
+
before painting; this is a single level, not a full stacking context, so a
|
|
45
|
+
child cannot escape its parent's level.
|
|
46
|
+
Note the consequence of that shrink-to-fit rule: an absolutely positioned box
|
|
47
|
+
with `width: auto` and no explicit size gets **no** available width for its own
|
|
48
|
+
inline content (its preferred width comes from its text or its non-absolute
|
|
49
|
+
children only), so inline runs inside it are laid out at zero available width
|
|
50
|
+
and each one wraps onto its own line. Give such a box an explicit `width`, or
|
|
51
|
+
place its children with `left`/`width` (as
|
|
52
|
+
`examples/gui/pelican-bike` does for its sprite runs), instead of relying on
|
|
53
|
+
line flow inside it.
|
|
54
|
+
- **Floats and clearance** — `float: left|right` takes a box out of flow,
|
|
55
|
+
shrink-to-fits its width, places it against the current line and lets later
|
|
56
|
+
inline lines wrap around it; `clear: left|right|both` pushes a following block
|
|
57
|
+
below the relevant floats, and a block that contains only floats grows to
|
|
58
|
+
enclose them.
|
|
59
|
+
- **`overflow` clipping** — a box with `overflow: hidden` (or `scroll`/`auto`)
|
|
60
|
+
clips its subtree to its padding box, intersected with any ancestor clip; the
|
|
61
|
+
renderer applies the result with a GPU scissor rectangle. There is no
|
|
62
|
+
scrolling yet, so `scroll`/`auto` clip like `hidden`.
|
|
63
|
+
|
|
64
|
+
Not yet implemented: parent/child and empty-block margin collapsing, anonymous
|
|
65
|
+
flex items from non-whitespace text, full `z-index` stacking contexts, and
|
|
66
|
+
`float` affecting block children (only inline content wraps; a block must
|
|
67
|
+
`clear`).
|
|
68
|
+
|
|
69
|
+
**Document** (`runtime/ext_gui/document.{h,cpp}`): owns the DOM tree, gathers
|
|
70
|
+
`<style>` text into one stylesheet, computes styles and layout for a viewport and
|
|
71
|
+
offers `querySelector`/`querySelectorAll`/`styleOf`/`boxOf`.
|
|
72
|
+
|
package/doc/gui/media.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Media: images, video and audio
|
|
2
|
+
|
|
3
|
+
> Part of the [GUI extension](../gui.md); the engine overview, locked
|
|
4
|
+
> decisions and roadmap live there.
|
|
5
|
+
|
|
6
|
+
## Images (M6a)
|
|
7
|
+
|
|
8
|
+
`runtime/ext_gui/image.{h,cpp}` wraps the vendored **stb_image** header
|
|
9
|
+
decoding PNG/JPEG/BMP/GIF/TGA to RGBA8, with a decode cache and a header-only
|
|
10
|
+
size cache (`stbi_info`). Paths accept a `file://` prefix and percent-encoding.
|
|
11
|
+
`runtime/ext_gui/svg.{h,cpp}` adds SVG on top: it wraps the vendored **NanoSVG**
|
|
12
|
+
library (parse + anti-aliased scanline rasteriser), detects an SVG by extension
|
|
13
|
+
or by its contents, and produces the same RGBA8 `Image` the pipeline already
|
|
14
|
+
consumes. `stb_image` cannot decode SVG, so this is what makes
|
|
15
|
+
`<img src="logo.svg">` work.
|
|
16
|
+
|
|
17
|
+
- `<img>` is a replaced element (`display: inline-block`): layout gives it the
|
|
18
|
+
CSS size when set, otherwise the intrinsic pixel size, and preserves the
|
|
19
|
+
aspect ratio when only one axis is constrained. The intrinsic size is read
|
|
20
|
+
from the file header (no full decode) while building the box tree, and for an
|
|
21
|
+
SVG it comes from the parsed `width`/`height` or the `viewBox` (falling back
|
|
22
|
+
to 300x150 when neither is present).
|
|
23
|
+
- Paint emits one `PaintImage` per `<img>`; the renderer uploads each unique
|
|
24
|
+
`src` to an RGBA texture and draws textured quads, batching consecutive quads
|
|
25
|
+
that share a texture. A **bitmap** decodes at its intrinsic resolution and is
|
|
26
|
+
cached by path; a **vector** (SVG) is rasterised for the drawn size in device
|
|
27
|
+
pixels (logical size × the window's pixel density), preserving the viewBox
|
|
28
|
+
aspect ratio, and cached by that size, so scaling or a HiDPI display produces
|
|
29
|
+
a crisp render instead of a magnified bitmap.
|
|
30
|
+
- The parse is cached per path; the parse and rasteriser state are released in
|
|
31
|
+
`xt_svg_shutdown` (`xt_image_shutdown`).
|
|
32
|
+
|
|
33
|
+
Still to do for images: CSS `background-image: url(...)`, `data:` URIs,
|
|
34
|
+
`object-fit`, 9-slice borders and inline `<svg>` elements (only SVG *files* are
|
|
35
|
+
supported; see [Open questions](./known-issues.md#open-questions)).
|
|
36
|
+
|
|
37
|
+
## Video & audio (M13)
|
|
38
|
+
|
|
39
|
+
`runtime/ext_gui/media.{h,cpp}` wraps a statically vendored **FFmpeg** build and
|
|
40
|
+
`runtime/ext_gui/gui_media.cpp` connects the DOM tree to it. `<video>` and
|
|
41
|
+
`<audio>` really decode and play; there is no WebView and no platform media
|
|
42
|
+
framework in the path.
|
|
43
|
+
|
|
44
|
+
- **Decoding.** `avformat_open_input` opens the `src` (a path — `file://` is
|
|
45
|
+
accepted, but media paths are *not* percent-decoded, unlike images), the
|
|
46
|
+
best stream is selected for each kind, and a software decoder yields
|
|
47
|
+
`yuv/rgb` frames which `sws_scale` converts to RGBA8 at the coded size. The
|
|
48
|
+
supported containers/codecs are the minimal FFmpeg configuration from
|
|
49
|
+
`scripts/build-gui.ts`: MP4/MOV/MKV/AVI/FLV/MPEG-TS/M4V/ASF, MP3/OGG/WAV/FLAC/
|
|
50
|
+
AAC; H.264, HEVC, VP8, VP9, AV1, MPEG-4, MJPEG video; AAC, MP3, Opus, Vorbis,
|
|
51
|
+
FLAC, ALAC, AC3/E-AC3, WMAv2 and the raw PCM formats audio.
|
|
52
|
+
- **Video.** `<video>` is a replaced element like `<img>` (UA default
|
|
53
|
+
`300x150`, overridden by the decoded size via `setMediaSize`). Paint emits the
|
|
54
|
+
same `PaintImage` as an image; the renderer uploads each new decoded frame to
|
|
55
|
+
an RGBA texture (`ensureMediaTexture`) and draws a textured quad, so the GPU
|
|
56
|
+
scales it to the CSS box instead of the CPU resizing. Playback timing is the
|
|
57
|
+
wall clock (`SDL_GetTicks`), not the frame delta.
|
|
58
|
+
- **Audio.** There is no audio callback: the decoder is pushed on the main
|
|
59
|
+
thread as it is ticked and the PCM is queued with
|
|
60
|
+
`SDL_OpenAudioDeviceStream` in push mode (`NULL` callback), buffering about
|
|
61
|
+
0.25 s ahead at 48 kHz stereo `SDL_AUDIO_F32`. `volume` maps to
|
|
62
|
+
`SDL_SetAudioStreamGain`; audio is best-effort and is skipped when no device
|
|
63
|
+
is available (headless CI).
|
|
64
|
+
- **Threading.** The engine is single-threaded and not thread-aware, so all
|
|
65
|
+
decoding happens on the main thread inside `xt_gui_media_tick` — no decode
|
|
66
|
+
thread, no cross-thread GC exposure.
|
|
67
|
+
- **DOM API.** Element handles expose the media surface directly (see
|
|
68
|
+
`runtime/ext_gui/dom_api_media.cpp` and the prototype in
|
|
69
|
+
`dom_api_proto.cpp`): `play()` / `pause()` / `load()`, and the
|
|
70
|
+
`paused` / `ended` / `duration` / `currentTime` / `volume` / `muted` / `loop`
|
|
71
|
+
/ `autoplay` / `videoWidth` / `videoHeight` / `readyState` / `src`
|
|
72
|
+
properties. Media elements also emit `loadedmetadata`, `play`, `pause`,
|
|
73
|
+
`ended`, `timeupdate` (every 250 ms) and `error`, so the usual
|
|
74
|
+
`addEventListener` pattern works.
|
|
75
|
+
- **No built-in controls.** `<video controls>` is deliberately *not*
|
|
76
|
+
implemented: the element always renders the raw frame, and the program draws
|
|
77
|
+
its own chrome (a progress bar, buttons) with ordinary elements and CSS. See
|
|
78
|
+
`examples/gui/media.html` (`examples/gui/media.ts`) for the pattern.
|
|
79
|
+
- **Lifecycle.** Players are owned per window (`XtGuiMediaEntry`, keyed by
|
|
80
|
+
node). `xt_gui_media_sync` drops entries whose node is gone, reopens on a
|
|
81
|
+
`src` change, and opens on first use; `xt_gui_media_shutdown` runs on window
|
|
82
|
+
close/quit/`loadHTML`.
|
|
83
|
+
|
|
84
|
+
Still to do for media: `<video controls>`, separate audio/video clock drift
|
|
85
|
+
correction, hardware decode, streaming protocols (only `file` is enabled),
|
|
86
|
+
subtitles, `playbackRate` and per-frame `requestVideoFrameCallback`.
|
|
87
|
+
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Paint, GPU and text
|
|
2
|
+
|
|
3
|
+
> Part of the [GUI extension](../gui.md); the engine overview, locked
|
|
4
|
+
> decisions and roadmap live there.
|
|
5
|
+
|
|
6
|
+
## Paint and display list (M4a/M4b)
|
|
7
|
+
|
|
8
|
+
`runtime/ext_gui/paint.{h,cpp}` walks the layout tree in painter's order and
|
|
9
|
+
emits a backend-agnostic `DisplayList`: a painter-order `ops` list of
|
|
10
|
+
**rectangles** (backgrounds and four solid border edges, with `border-radius`),
|
|
11
|
+
**text runs** (each carrying its text, colour and resolved `FontSpec`) and
|
|
12
|
+
**images**, backed by parallel payload arrays. Each op also carries the active
|
|
13
|
+
clip rectangle (the padding box of the nearest `overflow: hidden` ancestor), so
|
|
14
|
+
`overflow` clipping and `z-index` ordering survive into the renderer.
|
|
15
|
+
`DisplayList::dump()` feeds `paintList()`/`paintCount()`.
|
|
16
|
+
|
|
17
|
+
`opacity` is honoured only at its extremes: a box whose computed `opacity` is
|
|
18
|
+
`0` (and its whole subtree) paints nothing, and anything above `0` paints fully
|
|
19
|
+
opaque. A shape carries its colour's alpha only, so there is no compositing of a
|
|
20
|
+
partly transparent box — the `opacity` transitions the CSS parser accepts are
|
|
21
|
+
therefore not visible either. Hiding and revealing a layer with `opacity: 0` on
|
|
22
|
+
the hidden state is what `examples/gui/pelican-bike`'s game-over overlay does.
|
|
23
|
+
|
|
24
|
+
`runtime/ext_gui/renderer.{h,cpp}` turns that list into batched vertex buffers
|
|
25
|
+
per window (shapes, glyph quads and images) and merges consecutive ops that
|
|
26
|
+
share a pipeline, clip and texture into **draw runs**. Each run is drawn with
|
|
27
|
+
its own GPU scissor rectangle (scaled to device pixels), so painter order and
|
|
28
|
+
`overflow` clipping are applied per op through the SDL_GPU graphics pipelines:
|
|
29
|
+
|
|
30
|
+
- The shared pipelines are created lazily from the device's supported shader
|
|
31
|
+
format. Each of SDL_GPU's three backends consumes a different binary and none
|
|
32
|
+
of them compiles GLSL/HLSL at runtime, so the engine ships all three and
|
|
33
|
+
`selectShader` (`renderer_shaders.h`) picks the one the device accepts:
|
|
34
|
+
**MSL** for Metal (compiled by SDL from the embedded source),
|
|
35
|
+
**SPIR-V** for Vulkan (`runtime/ext_gui/spirv/*.{vert,frag}`, compiled by
|
|
36
|
+
`glslc`), and **DXIL** for Direct3D 12 (`shaders.hlsl`, compiled by `dxc`).
|
|
37
|
+
`scripts/build-gui-shaders.mjs` regenerates the embedded blobs and the result
|
|
38
|
+
is committed, so nothing extra is needed to build the engine.
|
|
39
|
+
The descriptor bindings differ per format — SPIR-V follows what
|
|
40
|
+
`SDL_gpu_vulkan.c` builds (uniforms `set=1, binding=0`, samplers
|
|
41
|
+
`set=2, binding=0`, textures `set=2, binding=1`) — and glslc names every entry
|
|
42
|
+
point `main` where MSL/DXIL keep the descriptive names.
|
|
43
|
+
Note that SDL 3.2.10's Direct3D 12 backend cannot create a graphics pipeline
|
|
44
|
+
whose shaders declare a uniform buffer (it fails with `E_INVALIDARG`); the
|
|
45
|
+
engine passes the viewport that way, so the DXIL path is built but not usable
|
|
46
|
+
until that is resolved upstream.
|
|
47
|
+
- A **rounded-rectangle distance field** in the shape fragment shader gives
|
|
48
|
+
antialiased fills; the vertex carries `position`, `local`, `half extents`,
|
|
49
|
+
`radius` and colour, and a viewport-size push constant does the projection.
|
|
50
|
+
Alpha blending is enabled.
|
|
51
|
+
- The text pipeline samples a **single shared grayscale glyph atlas**
|
|
52
|
+
(`R8_UNORM`, 2048², shelf-packed, LINEAR filtering) and draws each glyph as a
|
|
53
|
+
textured quad (`position`, `uv`, colour), modulating alpha by the coverage.
|
|
54
|
+
- Geometry is uploaded only when the document or viewport changes
|
|
55
|
+
(`geometry.dirty`), so steady-state frames are bind-and-draw.
|
|
56
|
+
|
|
57
|
+
The window background (`setBackground`) is the render-pass clear colour.
|
|
58
|
+
|
|
59
|
+
Still to do in M4: gradients.
|
|
60
|
+
|
|
61
|
+
## Text stack (M4b)
|
|
62
|
+
|
|
63
|
+
`runtime/ext_gui/text.{h,cpp}` wraps **HarfBuzz** (shaping) and **FreeType**
|
|
64
|
+
(metrics + eventual rasterisation). Both are built statically and linked into
|
|
65
|
+
`gui.a` by `scripts/build-gui.ts`.
|
|
66
|
+
|
|
67
|
+
- Fonts are resolved from well-known system paths (Helvetica/Arial on macOS,
|
|
68
|
+
DejaVu/Liberation on Linux, Segoe UI/Arial on Windows), overridable with
|
|
69
|
+
`XT_GUI_FONT` (and `XT_GUI_FONT_MONO`), and cached per `(family class, size)`.
|
|
70
|
+
Only regular upright faces are used for now; weight/italic selection is a
|
|
71
|
+
later refinement.
|
|
72
|
+
- The default candidates prefer a **CJK-capable** face (Microsoft YaHei on
|
|
73
|
+
Windows, PingFang/STHeiti on macOS, Noto Sans CJK/WenQuanYi on Linux) before
|
|
74
|
+
the Latin-only ones. Setting `XT_GUI_FONT` to a Latin-only face gives its
|
|
75
|
+
Latin design instead (at the cost of CJK coverage).
|
|
76
|
+
- **Codepoints the primary face lacks fall back to another face.** The engine
|
|
77
|
+
keeps a small fallback chain — the platform's symbol face (Segoe UI Symbol,
|
|
78
|
+
Apple Symbols, Noto Sans Symbols, DejaVu Sans) first, then the other family
|
|
79
|
+
class — and resolves each codepoint to the first face in it that has the
|
|
80
|
+
glyph. The run is shaped in segments, one per face, so a mixed run (Latin
|
|
81
|
+
plus an arrow, box-drawing or geometric shape, or Latin inside CJK) draws
|
|
82
|
+
every glyph instead of dropping the missing ones as `.notdef`. This is what
|
|
83
|
+
makes a character such as `▮`/`▯` (U+25AE/U+25AF, used by the pelican-bike
|
|
84
|
+
speed bar) visible even when the CJK primary has no geometric shapes. Each
|
|
85
|
+
shaped glyph carries its face (`ShapedGlyph::font`) so the renderer rasterises
|
|
86
|
+
it from the right atlas entry; `xt_text_scale` maps a face to the display's
|
|
87
|
+
scale factor.
|
|
88
|
+
- `xt_text_measure_width` shapes the run with HarfBuzz (so kerning and
|
|
89
|
+
ligatures are honoured), `xt_text_metrics` returns FreeType's ascent /
|
|
90
|
+
descent / normal line height. When no font file can be found the module falls
|
|
91
|
+
back to a deterministic per-byte approximation, so layout still works.
|
|
92
|
+
- `xt_text_shape_run` returns positioned glyphs and `xt_text_rasterize` renders
|
|
93
|
+
an 8-bit bitmap; the renderer packs those into the atlas. On HiDPI displays
|
|
94
|
+
glyphs are rasterised at `font_size * SDL_GetWindowPixelDensity` while quads
|
|
95
|
+
are positioned in logical pixels, so text stays crisp.
|
|
96
|
+
- Layout uses these real metrics for text widths, line breaking and
|
|
97
|
+
`line-height: normal`; `measureText`/`fontMetrics` expose them to tests.
|
|
98
|
+
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Runtime integration and memory
|
|
2
|
+
|
|
3
|
+
> Part of the [GUI extension](../gui.md); the engine overview, locked
|
|
4
|
+
> decisions and roadmap live there.
|
|
5
|
+
|
|
6
|
+
## Event loop integration
|
|
7
|
+
|
|
8
|
+
The runtime change (already landed):
|
|
9
|
+
|
|
10
|
+
- `int xt_loop_poll(int timeout_ms)` — one reactor iteration; `0` polls without
|
|
11
|
+
blocking, `< 0` blocks.
|
|
12
|
+
- `void xt_loop_set_main(xt_main_loop_fn fn)` — a host may take over the main
|
|
13
|
+
loop. `xt_run_event_loop()` delegates to it; the generated `main` is unchanged.
|
|
14
|
+
- `xt_loop_set_main(NULL)` restores the default `select(2)` loop.
|
|
15
|
+
|
|
16
|
+
The GUI engine can either register its own loop through `xt_loop_set_main`, or
|
|
17
|
+
expose an explicit `run()`; **M2 uses the explicit `run()`** so the window is a
|
|
18
|
+
plain native call the TypeScript program controls. Each tick it:
|
|
19
|
+
|
|
20
|
+
1. pumps SDL window/input events for every window,
|
|
21
|
+
2. builds/uploads the display list when it changed and renders every open
|
|
22
|
+
window (clear pass + shape geometry + text geometry),
|
|
23
|
+
3. calls `xt_loop_poll(0)` and `xt_drain_microtasks()` so sockets/timers and
|
|
24
|
+
`await` continuations keep making progress,
|
|
25
|
+
4. repeats until all windows close or `quit()` is called.
|
|
26
|
+
|
|
27
|
+
The `xt_loop_set_main` hook remains available for hosts that want to own the
|
|
28
|
+
loop themselves.
|
|
29
|
+
|
|
30
|
+
This keeps network I/O, timers and `await` working inside a GUI program.
|
|
31
|
+
|
|
32
|
+
## Native ↔ TS bridge
|
|
33
|
+
|
|
34
|
+
- **TS → engine**: direct `xt_gui_*` calls / window methods.
|
|
35
|
+
- **Engine → TS**: `xt_call_with_this(fn, thisValue, argc, argv)` with function
|
|
36
|
+
values captured from TS (e.g. event handlers registered with `win.on(...)`).
|
|
37
|
+
- **Future page→native RPC**: not needed while there is no page JS; native TS is
|
|
38
|
+
the controller. If a declarative layer is added later, it will use the same
|
|
39
|
+
`xt_call_*` entry points.
|
|
40
|
+
|
|
41
|
+
## Memory & GC
|
|
42
|
+
|
|
43
|
+
The engine owns most of a GUI program's memory outside the runtime heap: the DOM
|
|
44
|
+
tree, the detached-node pool, the element-handle tables, the layout box tree and
|
|
45
|
+
the display list are C++, so the mark-sweep collector cannot see them. `stats()`
|
|
46
|
+
(see [Diagnostics](./api.md#diagnostics)) reports the parts that can grow:
|
|
47
|
+
|
|
48
|
+
| Counter | Expected in a steady state |
|
|
49
|
+
| --- | --- |
|
|
50
|
+
| `treeNodes` | constant — the authored document |
|
|
51
|
+
| `detachedRoots`, `detachedNodes` | 0, or constant for nodes the script removed itself and still holds |
|
|
52
|
+
| `handles`, `handleSlots` | constant — one per element the script still refers to |
|
|
53
|
+
| `listeners` | constant — one entry per element with listeners |
|
|
54
|
+
| `heapBytes`, `heapAllocations` | sawtooth around the collector's threshold |
|
|
55
|
+
|
|
56
|
+
Everything else is per-frame scratch (boxes, display list, vertex staging) that is
|
|
57
|
+
freed before the next frame. A counter that climbs with the frame count is the
|
|
58
|
+
leak signature to look for first; `tests/e2e/gui-retention.test.ts` asserts that
|
|
59
|
+
the usual mutation patterns leave all of them unchanged.
|
|
60
|
+
|
|
61
|
+
Two engine details matter when reading those numbers:
|
|
62
|
+
|
|
63
|
+
- **What releases nodes.** `innerHTML`/`inner`/`textContent`/`replaceChild`
|
|
64
|
+
release the subtrees they drop; `removeChild`/`remove` keep them (see [Node
|
|
65
|
+
lifetime](./dom.md#node-lifetime)). A game that rebuilds a sprite with
|
|
66
|
+
`innerHTML` therefore runs in constant memory.
|
|
67
|
+
- **What can still accumulate.** A node created with `createElement` and never
|
|
68
|
+
attached stays in the pool, because the collector has no weak references and
|
|
69
|
+
the engine cannot tell that the script dropped its handle. The same applies to
|
|
70
|
+
a node removed and then forgotten by the script. Both are bounded by the number
|
|
71
|
+
of *live* handles, but a program that creates elements per frame without
|
|
72
|
+
attaching them grows the pool with the frame count.
|
|
73
|
+
- **Vector images are cached by pixel size.** A bitmap `<img>` decodes once and
|
|
74
|
+
is cached by path, but an SVG is rasterised at the drawn device size, so the
|
|
75
|
+
decoded-pixel and GPU-texture caches hold one entry per distinct size. That is
|
|
76
|
+
constant for a static layout (the usual case) and small for a handful of
|
|
77
|
+
sizes, but a program that reflows one SVG to a new integer size every frame
|
|
78
|
+
accumulates entries; prefer animating a bitmap, or keep the vector to a fixed
|
|
79
|
+
size.
|
|
80
|
+
|
|
81
|
+
The collector behind `heapBytes` is a non-moving mark-sweep (`runtime/xt_alloc.c`).
|
|
82
|
+
Its trigger follows the live set: after every collection the threshold becomes
|
|
83
|
+
twice the surviving heap (floor 1 MiB), so a small live set means small, frequent
|
|
84
|
+
collections and a bounded resident set. `XT_GC_THRESHOLD=<bytes>` pins the
|
|
85
|
+
threshold instead, and `XT_GC_TRACE=1` prints one line per collection (heap and
|
|
86
|
+
live-allocation counts before and after, the next trigger, and the live set by
|
|
87
|
+
object kind) to stderr — that is how the DOM leak was traced to the node pool.
|
|
88
|
+
|
package/doc/gui-scripts.md
CHANGED
|
@@ -241,6 +241,9 @@ frees, so cycles are harmless).
|
|
|
241
241
|
- mutation: `appendChild` / `insertBefore` / `removeChild` / `replaceChild` /
|
|
242
242
|
`remove` / `cloneNode(deep?)`.
|
|
243
243
|
- geometry/focus: `getBoundingClientRect`, `focus`, `blur`, `click`.
|
|
244
|
+
- media (`<video>` / `<audio>`): `play()` / `pause()` / `load()` and the
|
|
245
|
+
`src`, `paused`, `ended`, `duration`, `currentTime`, `volume`, `muted`,
|
|
246
|
+
`loop`, `autoplay`, `videoWidth`, `videoHeight`, `readyState` properties.
|
|
244
247
|
- events: `addEventListener(type, fn, optionsOrCapture?)`,
|
|
245
248
|
`removeEventListener`, `dispatchEvent`.
|
|
246
249
|
|
|
@@ -342,9 +345,10 @@ legacy `target` field over the handle.
|
|
|
342
345
|
|
|
343
346
|
## Docs to update
|
|
344
347
|
|
|
345
|
-
- `doc/gui.md` — Non-goals ("Executing page
|
|
346
|
-
page scripts; compile-time scripts are
|
|
347
|
-
|
|
348
|
+
- `doc/gui.md` (overview) and `doc/gui/dom.md` — Non-goals ("Executing page
|
|
349
|
+
`<script>`" → "Executing *runtime* page scripts; compile-time scripts are
|
|
350
|
+
AOT-compiled"), locked decision #1, the AOT script section, milestones and
|
|
351
|
+
the progress log. The TS-facing API is in `doc/gui/api.md`.
|
|
348
352
|
- `doc/implemented.md` / `doc/unimplemented.md` — move the new DOM/script
|
|
349
353
|
capabilities across.
|
|
350
354
|
- `README.md` — mention the DOM API + AOT scripts.
|