xbintsc 0.5.45 → 0.5.52

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 (286) hide show
  1. package/AGENTS.md +1 -1
  2. package/README.md +5 -4
  3. package/README.zh-CN.md +4 -3
  4. package/dist/src/cli/extensions.d.ts +24 -0
  5. package/dist/src/cli/extensions.js +62 -0
  6. package/dist/src/cli/extensions.js.map +1 -0
  7. package/dist/src/cli/help.d.ts +1 -1
  8. package/dist/src/cli/help.js +3 -1
  9. package/dist/src/cli/help.js.map +1 -1
  10. package/dist/src/cli/hints.js +8 -3
  11. package/dist/src/cli/hints.js.map +1 -1
  12. package/dist/src/cli/main.js +21 -28
  13. package/dist/src/cli/main.js.map +1 -1
  14. package/dist/src/diagnostics/diagnostic.d.ts +3 -1
  15. package/dist/src/diagnostics/diagnostic.js +2 -0
  16. package/dist/src/diagnostics/diagnostic.js.map +1 -1
  17. package/dist/src/driver/build-key.d.ts +2 -0
  18. package/dist/src/driver/build-key.js +1 -0
  19. package/dist/src/driver/build-key.js.map +1 -1
  20. package/dist/src/driver/compiler.js +61 -8
  21. package/dist/src/driver/compiler.js.map +1 -1
  22. package/dist/src/driver/config.d.ts +4 -0
  23. package/dist/src/driver/config.js +2 -0
  24. package/dist/src/driver/config.js.map +1 -1
  25. package/dist/src/driver/link-features.d.ts +37 -0
  26. package/dist/src/driver/link-features.js +123 -0
  27. package/dist/src/driver/link-features.js.map +1 -0
  28. package/dist/src/driver/options.d.ts +10 -0
  29. package/dist/src/driver/runtime-objects.d.ts +5 -0
  30. package/dist/src/driver/runtime-objects.js +20 -1
  31. package/dist/src/driver/runtime-objects.js.map +1 -1
  32. package/dist/src/driver/usage.d.ts +23 -0
  33. package/dist/src/driver/usage.js +353 -0
  34. package/dist/src/driver/usage.js.map +1 -0
  35. package/dist/src/driver/version.d.ts +1 -1
  36. package/dist/src/driver/version.js +1 -1
  37. package/dist/src/extensions/catalog.js +1 -2
  38. package/dist/src/extensions/catalog.js.map +1 -1
  39. package/dist/src/extensions/gui/features.d.ts +34 -0
  40. package/dist/src/extensions/gui/features.js +165 -0
  41. package/dist/src/extensions/gui/features.js.map +1 -0
  42. package/dist/src/extensions/gui/index.js +10 -4
  43. package/dist/src/extensions/gui/index.js.map +1 -1
  44. package/dist/src/extensions/link-feature.d.ts +70 -0
  45. package/dist/src/extensions/link-feature.js +18 -0
  46. package/dist/src/extensions/link-feature.js.map +1 -0
  47. package/dist/src/extensions/registry.d.ts +24 -4
  48. package/dist/src/extensions/registry.js +26 -3
  49. package/dist/src/extensions/registry.js.map +1 -1
  50. package/dist/src/index.d.ts +3 -2
  51. package/dist/src/index.js +0 -1
  52. package/dist/src/index.js.map +1 -1
  53. package/dist/tests/cli/hints.test.js +5 -7
  54. package/dist/tests/cli/hints.test.js.map +1 -1
  55. package/dist/tests/driver/link-features.test.js +105 -0
  56. package/dist/tests/driver/link-features.test.js.map +1 -0
  57. package/dist/tests/driver/usage.test.d.ts +1 -0
  58. package/dist/tests/driver/usage.test.js +88 -0
  59. package/dist/tests/driver/usage.test.js.map +1 -0
  60. package/dist/tests/e2e/gui-audio.test.js +4 -4
  61. package/dist/tests/e2e/gui-audio.test.js.map +1 -1
  62. package/dist/tests/e2e/gui-css-coverage.test.d.ts +9 -0
  63. package/dist/tests/e2e/gui-css-coverage.test.js +333 -0
  64. package/dist/tests/e2e/gui-css-coverage.test.js.map +1 -0
  65. package/dist/tests/e2e/gui-css-features.test.d.ts +6 -0
  66. package/dist/tests/e2e/gui-css-features.test.js +270 -0
  67. package/dist/tests/e2e/gui-css-features.test.js.map +1 -0
  68. package/dist/tests/e2e/gui-effects.test.js +92 -11
  69. package/dist/tests/e2e/gui-effects.test.js.map +1 -1
  70. package/dist/tests/e2e/gui-feature-linking.test.d.ts +9 -0
  71. package/dist/tests/e2e/gui-feature-linking.test.js +68 -0
  72. package/dist/tests/e2e/gui-feature-linking.test.js.map +1 -0
  73. package/dist/tests/e2e/gui-font-face.test.d.ts +10 -0
  74. package/dist/tests/e2e/gui-font-face.test.js +142 -0
  75. package/dist/tests/e2e/gui-font-face.test.js.map +1 -0
  76. package/dist/tests/e2e/gui-fonts.test.d.ts +10 -0
  77. package/dist/tests/e2e/gui-fonts.test.js +150 -0
  78. package/dist/tests/e2e/gui-fonts.test.js.map +1 -0
  79. package/dist/tests/e2e/gui-helpers.d.ts +10 -1
  80. package/dist/tests/e2e/gui-helpers.js +6 -0
  81. package/dist/tests/e2e/gui-helpers.js.map +1 -1
  82. package/dist/tests/e2e/gui-layout.test.js +7 -3
  83. package/dist/tests/e2e/gui-layout.test.js.map +1 -1
  84. package/dist/tests/e2e/gui-media-disabled.test.d.ts +5 -4
  85. package/dist/tests/e2e/gui-media-disabled.test.js +7 -6
  86. package/dist/tests/e2e/gui-media-disabled.test.js.map +1 -1
  87. package/dist/tests/e2e/gui-media.test.d.ts +5 -4
  88. package/dist/tests/e2e/gui-media.test.js +9 -9
  89. package/dist/tests/e2e/gui-media.test.js.map +1 -1
  90. package/dist/tests/e2e/gui-paint.test.js +5 -2
  91. package/dist/tests/e2e/gui-paint.test.js.map +1 -1
  92. package/dist/tests/e2e/gui-shaping.test.d.ts +5 -0
  93. package/dist/tests/e2e/gui-shaping.test.js +47 -0
  94. package/dist/tests/e2e/gui-shaping.test.js.map +1 -0
  95. package/dist/tests/e2e/gui-track-sizing.test.d.ts +6 -0
  96. package/dist/tests/e2e/gui-track-sizing.test.js +173 -0
  97. package/dist/tests/e2e/gui-track-sizing.test.js.map +1 -0
  98. package/dist/tests/e2e/gui-transform.test.js +3 -2
  99. package/dist/tests/e2e/gui-transform.test.js.map +1 -1
  100. package/dist/tests/e2e/gui-webgl-pipeline-cache.test.d.ts +18 -0
  101. package/dist/tests/e2e/gui-webgl-pipeline-cache.test.js +120 -0
  102. package/dist/tests/e2e/gui-webgl-pipeline-cache.test.js.map +1 -0
  103. package/dist/tests/extensions/gui-features.test.d.ts +1 -0
  104. package/dist/tests/extensions/gui-features.test.js +175 -0
  105. package/dist/tests/extensions/gui-features.test.js.map +1 -0
  106. package/doc/ai/cli.md +5 -1
  107. package/doc/ai/extensions.md +56 -26
  108. package/doc/ai/troubleshooting.md +10 -7
  109. package/doc/ai/zh-CN/cli.md +5 -1
  110. package/doc/ai/zh-CN/extensions.md +48 -22
  111. package/doc/ai/zh-CN/troubleshooting.md +6 -5
  112. package/doc/gui/animation.md +39 -9
  113. package/doc/gui/api.md +2 -1
  114. package/doc/gui/build.md +137 -19
  115. package/doc/gui/canvas.md +1 -1
  116. package/doc/gui/dom.md +20 -4
  117. package/doc/gui/known-issues.md +63 -26
  118. package/doc/gui/layout.md +31 -8
  119. package/doc/gui/media.md +13 -7
  120. package/doc/gui/rendering.md +114 -39
  121. package/doc/gui/runtime.md +5 -0
  122. package/doc/gui/webgl.md +24 -6
  123. package/doc/gui.md +110 -47
  124. package/doc/implemented/cli.md +2 -0
  125. package/doc/implemented/extensions.md +9 -3
  126. package/doc/npm-package.md +4 -4
  127. package/doc/xbintsc.config.schema.json +10 -0
  128. package/doc/zh-CN/gui/animation.md +33 -10
  129. package/doc/zh-CN/gui/api.md +2 -1
  130. package/doc/zh-CN/gui/build.md +110 -16
  131. package/doc/zh-CN/gui/canvas.md +1 -1
  132. package/doc/zh-CN/gui/dom.md +16 -3
  133. package/doc/zh-CN/gui/known-issues.md +45 -15
  134. package/doc/zh-CN/gui/layout.md +23 -6
  135. package/doc/zh-CN/gui/media.md +11 -6
  136. package/doc/zh-CN/gui/rendering.md +84 -26
  137. package/doc/zh-CN/gui/runtime.md +3 -0
  138. package/doc/zh-CN/gui/webgl.md +3 -3
  139. package/doc/zh-CN/gui.md +92 -41
  140. package/doc/zh-CN/implemented/cli.md +2 -0
  141. package/doc/zh-CN/implemented/extensions.md +9 -3
  142. package/doc/zh-CN/npm-package.md +4 -3
  143. package/package.json +7 -7
  144. package/runtime/ext_gui/animation_values.cpp +425 -0
  145. package/runtime/ext_gui/animation_values.h +64 -0
  146. package/runtime/ext_gui/browser.cpp +22 -15
  147. package/runtime/ext_gui/browser_loop.cpp +13 -1
  148. package/runtime/ext_gui/canvas.h +1 -1
  149. package/runtime/ext_gui/canvas_raster.cpp +1 -1
  150. package/runtime/ext_gui/canvas_text.cpp +1 -1
  151. package/runtime/ext_gui/css.cpp +172 -21
  152. package/runtime/ext_gui/css.h +75 -4
  153. package/runtime/ext_gui/css_match.cpp +4 -0
  154. package/runtime/ext_gui/css_sheet.cpp +218 -2
  155. package/runtime/ext_gui/document.cpp +42 -192
  156. package/runtime/ext_gui/document.h +25 -12
  157. package/runtime/ext_gui/document_animation.cpp +428 -0
  158. package/runtime/ext_gui/dom_api_canvas.cpp +15 -8
  159. package/runtime/ext_gui/dom_api_canvas_extra.cpp +44 -20
  160. package/runtime/ext_gui/dom_api_canvas_image.cpp +3 -1
  161. package/runtime/ext_gui/dom_api_node_read.cpp +10 -0
  162. package/runtime/ext_gui/dom_api_offscreen.cpp +3 -1
  163. package/runtime/ext_gui/dom_api_style.cpp +7 -0
  164. package/runtime/ext_gui/dom_api_webgl_internal.h +4 -0
  165. package/runtime/ext_gui/dom_api_webgl_source.cpp +5 -2
  166. package/runtime/ext_gui/effect_values.cpp +245 -21
  167. package/runtime/ext_gui/gpu_bridge.c +28 -0
  168. package/runtime/ext_gui/gpu_feature.cpp +30 -0
  169. package/runtime/ext_gui/gpu_shader.cpp +22 -0
  170. package/runtime/ext_gui/gpu_shader_api.h +50 -0
  171. package/runtime/ext_gui/gradient_values.cpp +24 -0
  172. package/runtime/ext_gui/grid_values.cpp +23 -0
  173. package/runtime/ext_gui/gui.cpp +6 -37
  174. package/runtime/ext_gui/gui_canvas.cpp +14 -6
  175. package/runtime/ext_gui/gui_cursor.cpp +50 -0
  176. package/runtime/ext_gui/gui_engine.h +1 -18
  177. package/runtime/ext_gui/gui_events.cpp +19 -6
  178. package/runtime/ext_gui/gui_internal.h +3 -0
  179. package/runtime/ext_gui/gui_webgl.cpp +25 -10
  180. package/runtime/ext_gui/image_api.cpp +18 -0
  181. package/runtime/ext_gui/image_api.h +43 -0
  182. package/runtime/ext_gui/image_bitmap_browser.cpp +20 -11
  183. package/runtime/ext_gui/image_bridge.c +22 -0
  184. package/runtime/ext_gui/image_feature.cpp +107 -0
  185. package/runtime/ext_gui/inline_svg.cpp +95 -8
  186. package/runtime/ext_gui/inline_svg.h +5 -1
  187. package/runtime/ext_gui/layout.cpp +15 -9
  188. package/runtime/ext_gui/layout.h +8 -2
  189. package/runtime/ext_gui/layout_flex.cpp +134 -29
  190. package/runtime/ext_gui/layout_flow.cpp +58 -7
  191. package/runtime/ext_gui/layout_flow_internal.h +39 -4
  192. package/runtime/ext_gui/layout_grid.cpp +31 -0
  193. package/runtime/ext_gui/layout_inline.cpp +26 -2
  194. package/runtime/ext_gui/layout_vertical.cpp +196 -0
  195. package/runtime/ext_gui/media.cpp +4 -2
  196. package/runtime/ext_gui/media.h +3 -3
  197. package/runtime/ext_gui/media_api.h +7 -6
  198. package/runtime/ext_gui/paint.cpp +67 -9
  199. package/runtime/ext_gui/paint.h +61 -8
  200. package/runtime/ext_gui/paint_backdrop.cpp +211 -0
  201. package/runtime/ext_gui/paint_background.cpp +175 -97
  202. package/runtime/ext_gui/paint_effects.cpp +12 -6
  203. package/runtime/ext_gui/paint_internal.h +27 -0
  204. package/runtime/ext_gui/paint_outline.cpp +44 -0
  205. package/runtime/ext_gui/paint_shadow.cpp +368 -0
  206. package/runtime/ext_gui/renderer.cpp +15 -7
  207. package/runtime/ext_gui/renderer.h +11 -2
  208. package/runtime/ext_gui/renderer_backdrop.cpp +374 -0
  209. package/runtime/ext_gui/renderer_bitmap.cpp +82 -0
  210. package/runtime/ext_gui/renderer_gradient.cpp +51 -17
  211. package/runtime/ext_gui/renderer_image.cpp +19 -7
  212. package/runtime/ext_gui/renderer_internal.h +21 -0
  213. package/runtime/ext_gui/renderer_layer.cpp +37 -6
  214. package/runtime/ext_gui/renderer_text.cpp +2 -1
  215. package/runtime/ext_gui/stb_truetype.h +5077 -0
  216. package/runtime/ext_gui/style.cpp +62 -8
  217. package/runtime/ext_gui/style.h +111 -4
  218. package/runtime/ext_gui/style_cascade.cpp +83 -0
  219. package/runtime/ext_gui/style_internal.h +13 -1
  220. package/runtime/ext_gui/style_length.cpp +92 -0
  221. package/runtime/ext_gui/style_print.cpp +83 -2
  222. package/runtime/ext_gui/style_svg.cpp +40 -0
  223. package/runtime/ext_gui/style_text.cpp +302 -0
  224. package/runtime/ext_gui/style_ui.cpp +150 -0
  225. package/runtime/ext_gui/style_values.cpp +76 -62
  226. package/runtime/ext_gui/style_values_internal.h +23 -0
  227. package/runtime/ext_gui/text.cpp +27 -499
  228. package/runtime/ext_gui/text.h +18 -9
  229. package/runtime/ext_gui/text_api.h +45 -0
  230. package/runtime/ext_gui/text_bridge.c +23 -0
  231. package/runtime/ext_gui/text_cache.cpp +12 -3
  232. package/runtime/ext_gui/text_font_db.cpp +427 -0
  233. package/runtime/ext_gui/text_font_paths.cpp +250 -0
  234. package/runtime/ext_gui/text_font_paths.h +39 -0
  235. package/runtime/ext_gui/text_font_sfnt.cpp +396 -0
  236. package/runtime/ext_gui/text_font_sfnt.h +44 -0
  237. package/runtime/ext_gui/text_fonts.cpp +348 -0
  238. package/runtime/ext_gui/text_fonts.h +78 -0
  239. package/runtime/ext_gui/text_fonts_internal.h +77 -0
  240. package/runtime/ext_gui/text_glyph_cache.cpp +2 -2
  241. package/runtime/ext_gui/text_hb.cpp +438 -0
  242. package/runtime/ext_gui/text_stb.cpp +369 -0
  243. package/runtime/ext_gui/transform.cpp +107 -9
  244. package/runtime/ext_gui/transform.h +5 -0
  245. package/runtime/ext_gui/webgl_api.cpp +19 -0
  246. package/runtime/ext_gui/webgl_api.h +53 -0
  247. package/runtime/ext_gui/webgl_bridge.c +25 -0
  248. package/runtime/ext_gui/webgl_draw_setup.cpp +42 -1
  249. package/runtime/ext_gui/webgl_feature.cpp +82 -0
  250. package/runtime/ext_gui/webgl_feature.h +28 -0
  251. package/runtime/ext_gui/webgl_msl.cpp +41 -0
  252. package/runtime/ext_gui/webgl_msl.h +6 -0
  253. package/runtime/ext_gui/webgl_shader.cpp +40 -32
  254. package/runtime/ext_gui/window.cpp +7 -7
  255. package/runtime/ext_media/decode_audio.cpp +1 -1
  256. package/runtime/ext_media/media.cpp +1 -1
  257. package/runtime/ext_media/media_bridge.c +7 -7
  258. package/scripts/build-gui.ts +97 -98
  259. package/scripts/check-file-length.ts +1 -0
  260. package/scripts/gui-build-options.ts +122 -0
  261. package/src/cli/extensions.ts +72 -0
  262. package/src/cli/help.ts +3 -1
  263. package/src/cli/hints.ts +9 -3
  264. package/src/cli/main.ts +21 -27
  265. package/src/diagnostics/diagnostic.ts +2 -0
  266. package/src/driver/build-key.ts +3 -0
  267. package/src/driver/compiler.ts +63 -8
  268. package/src/driver/config.ts +6 -0
  269. package/src/driver/link-features.ts +163 -0
  270. package/src/driver/options.ts +10 -0
  271. package/src/driver/runtime-objects.ts +26 -1
  272. package/src/driver/usage.ts +376 -0
  273. package/src/driver/version.ts +1 -1
  274. package/src/extensions/catalog.ts +1 -2
  275. package/src/extensions/gui/features.ts +166 -0
  276. package/src/extensions/gui/index.ts +10 -4
  277. package/src/extensions/link-feature.ts +72 -0
  278. package/src/extensions/registry.ts +45 -4
  279. package/src/index.ts +3 -2
  280. package/dist/src/extensions/media/index.d.ts +0 -28
  281. package/dist/src/extensions/media/index.js +0 -59
  282. package/dist/src/extensions/media/index.js.map +0 -1
  283. package/dist/tests/extensions/media.test.js +0 -63
  284. package/dist/tests/extensions/media.test.js.map +0 -1
  285. package/src/extensions/media/index.ts +0 -65
  286. /package/dist/tests/{extensions/media.test.d.ts → driver/link-features.test.d.ts} +0 -0
package/doc/gui/layout.md CHANGED
@@ -22,10 +22,13 @@ with absolute (viewport-relative) geometry:
22
22
  Each line shrinks to the band left free by any floats (`float: left|right`)
23
23
  that overlap it, so later lines wrap around them.
24
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)).
25
+ measured with the stb_truetype stack (see [Text stack](./rendering.md#text-stack-m4b)).
26
26
  - **Flexbox** — `row`/`column` (and the `-reverse` variants) with `gap`,
27
27
  `flex-basis`/`flex-grow`/`flex-shrink`, `justify-content` and `align-items`
28
- (including `stretch` when the cross size is definite). `flex-wrap: wrap`
28
+ (including `stretch` when the cross size is definite). On both axes the main
29
+ size comes from `flex-basis` (or the measured size when it is `auto`), positive
30
+ free space is split by `flex-grow` and a deficit by `flex-shrink`, and an
31
+ item's `align-self` overrides the container's `align-items`. `flex-wrap: wrap`
29
32
  breaks items into several lines, `align-content`
30
33
  (`stretch`/`flex-start`/`flex-end`/`center`/`space-between`/`space-around`)
31
34
  distributes the lines on the cross axis and `wrap-reverse` flips that axis.
@@ -37,14 +40,34 @@ with absolute (viewport-relative) geometry:
37
40
  `row-gap` / `column-gap`, row-major auto-placement, simple `grid-column` /
38
41
  `grid-row` line placement, and `align/justify-items` plus
39
42
  `align/justify-self` (including `place-items` / `place-self`). Implicit rows
40
- and columns grow from the items; a single-cell grid with `place-items: center` centres a card in the whole overlay. Item measurement is a two-pass
43
+ and columns grow from the items; a definite grid height is shared across `fr`
44
+ rows after the fixed and auto rows are sized, so `aspect-ratio` can drive a
45
+ `repeat(n, 1fr)` grid. A single-cell grid with `place-items: center` centres a
46
+ card in the whole overlay. Item measurement is a two-pass
41
47
  layout (intrinsic size, then final placement), so text fragments are cleared
42
48
  between passes.
43
- - **CSS value functions and custom properties** — `clamp(min, value, max)`,
44
- `min(...)` and `max(...)` keep their arguments unresolved in the computed style
45
- and are evaluated at layout time against the element's font size and the
46
- viewport (`vw`/`vh`); `env(safe-area-inset-*)` resolves to zero in a desktop
47
- window. `var(--name[, fallback])` is substituted from inherited custom
49
+ - **Inline-level containers, auto margins and aspect ratio** — `display:
50
+ inline-flex`/`inline-grid` lay their children out with the flex/grid algorithm
51
+ but participate in the parent line as one atomic inline box (`vertical-align`
52
+ shifts it by its length value). A block-level box with `margin-left: auto` /
53
+ `margin-right: auto` absorbs the leftover inline space (both auto → centred);
54
+ a flex item's auto main-axis margins absorb the line's free space before
55
+ `justify-content`, which is what pushes a footer or button hint to the end.
56
+ `aspect-ratio` derives a `height: auto` box from its used width (the
57
+ width-from-height direction is not implemented). `border-radius` accepts a
58
+ percentage and resolves against the smaller border-box side.
59
+ - **Vertical writing mode** — `writing-mode: vertical-rl|vertical-lr` lays text
60
+ out one character per column step: CJK/kana/Hangul glyphs stay upright and
61
+ other scripts are rotated 90° at paint time. A definite height starts a new
62
+ column; a flex container's row/column axes swap with the writing mode. This is
63
+ a text-focused implementation: nested blocks and mixed horizontal content in a
64
+ vertical container still fall back to horizontal layout.
65
+ - **CSS value functions and custom properties** — `calc()` arithmetic (including
66
+ nested `calc()` and `calc()` inside the other functions), `clamp(min, value,
67
+ max)`, `min(...)` and `max(...)` keep their expression unresolved in the
68
+ computed style and are evaluated at layout time against the element's font
69
+ size and the viewport (`vw`/`vh`); `env(safe-area-inset-*)` resolves to zero in
70
+ a desktop window. `var(--name[, fallback])` is substituted from inherited custom
48
71
  properties during the cascade. `inset` is the four-value
49
72
  `top/right/bottom/left` shorthand, `box-sizing: border-box` subtracts padding
50
73
  and borders from an explicit width/height, `visibility: hidden` skips painting
package/doc/gui/media.md CHANGED
@@ -74,8 +74,9 @@ clip paths are not rendered (see [Known issues](./known-issues.md)).
74
74
 
75
75
  ## Video & audio (M13)
76
76
 
77
- Media playback is split across two extensions so a GUI program does not pay for
78
- a decoder it never uses:
77
+ Media playback is split inside the `gui` extension so a GUI program does not pay
78
+ for a decoder it never uses. Decoding is an optional **`media` link feature**,
79
+ not a separate extension:
79
80
 
80
81
  - **`gui`** (`gui.a`/`gui.lib`) owns the `<video>`/`<audio>` elements, the DOM
81
82
  API, the renderer integration and the SDL audio sink, but contains **no
@@ -85,13 +86,18 @@ a decoder it never uses:
85
86
  - **`media`** (`media.a`, or `media.lib` with the MSVC ABI on Windows;
86
87
  `runtime/ext_media/`) is the optional decoder backend:
87
88
  `runtime/ext_media/media.cpp` wraps a statically vendored **FFmpeg** build,
88
- and `runtime/ext_media/media_bridge.c` registers it with the engine. Enable it
89
- with `--ext media` (or `--ext gui,media`). `media` requires `gui`; the
90
- registry rejects the combination in the wrong order.
89
+ and `runtime/ext_media/media_bridge.c` registers it with the engine. The
90
+ compiler selects the feature when the program may use it (a `<video>`/`<audio>`
91
+ element in imported HTML, `createElement("video"/"audio")`, `new Audio()`,
92
+ `decodeAudioData`, or runtime-built markup) and only then compiles the bridge
93
+ and links `media.a`. `--feature media` forces it and `--no-feature media`
94
+ keeps it out; see [Link features](./build.md#link-features).
91
95
 
92
- Without the `media` extension the element still exists and its DOM API is
96
+ Without the feature the element still exists and its DOM API is
93
97
  callable, but opening a source fails and fires `error`. The same happens on any
94
- host where the archive has not been built (run `npm run media`); Windows is no
98
+ host where the archive has not been built (run `npm run media`) — an
99
+ auto-detected feature with no archive produces a build warning and degrades this
100
+ way, while `--feature media` fails the build. Windows is no
95
101
  longer special: `media.lib` is FFmpeg compiled with clang-cl for the MSVC ABI,
96
102
  so it links against `gui.lib` and plays normally. There is no WebView and no
97
103
  platform media framework in the path either way.
@@ -76,17 +76,27 @@ divided back into logical units, exactly the way HiDPI scaling already worked.
76
76
 
77
77
  The window background (`setBackground`) is the render-pass clear colour.
78
78
 
79
+ `background-size` on a `url()` layer emits one image quad per tile (default
80
+ `repeat`, 4096-tile cap) instead of stretching the picture to the box;
81
+ percentage sizes resolve against the box, `auto` keeps the box dimension.
82
+ `outline` paints four edge rectangles outside the border box (offset included)
83
+ after the border and before the content. A vertical-writing-mode text run is
84
+ painted by rotating its glyph quads 90° (CJK glyphs stay upright and are centred
85
+ in their column).
86
+
79
87
  ### Backgrounds, generated content and effects (M19)
80
88
 
81
89
  `background-color` and every `background-image` layer are painted. A
82
90
  comma-separated layer list is painted bottom-up (the first declaration layer is
83
91
  topmost), and both gradient kinds are supported:
84
92
 
85
- - a **linear** gradient is sampled at 18 points (64 when repeating) and emitted
86
- as solid rectangles; `to <side>`, `deg`/`grad`/`rad`/`turn`, percentage or
87
- length (`px`/`em`/…) offsets, the `color <start> <end>` shorthand and
88
- multi-stop colours work (the band walk is axis-aligned, so an oblique angle
89
- uses the nearest axis);
93
+ - a **linear** gradient follows the CSS gradient line (through the box centre,
94
+ at the declared angle, with the box extent projected onto it as its length, so
95
+ `%` and `%`-inside-`calc()` offsets resolve against that length) and is
96
+ resolved per pixel like a radial one. The ramp only varies along the line, so
97
+ the texture is one row (or one column) of device pixels. `to <side>`,
98
+ `deg`/`grad`/`rad`/`turn`, percentage or length (`px`/`em`/…) offsets, the
99
+ `color <start> <end>` shorthand and multi-stop colours work;
90
100
  - a **radial** gradient is resolved per pixel in `renderer_gradient.cpp` into one
91
101
  RGBA texture that is drawn as a single image quad (`PaintGradient`). Nesting the
92
102
  bands instead would composite their alpha once per band — since every band
@@ -98,6 +108,14 @@ topmost), and both gradient kinds are supported:
98
108
  the side or corner, as CSS does. Stops interpolate in premultiplied alpha, and
99
109
  the last stop extends past the ending shape over the rest of the box.
100
110
 
111
+ Both kinds share that per-pixel path: a walk of solid rectangles composites a
112
+ translucent stop once per band and cannot place a stop narrower than a band, so a
113
+ `transparent … calc(100% - 1px), rgba(…) calc(100% - 1px)` divider line either
114
+ fell between two samples (and vanished) or widened into a full band. A stop
115
+ position given as `calc()` keeps its percentage relative to the gradient line,
116
+ and a stop whose resolved offset is negative stays where the declaration put it
117
+ instead of being treated as "no offset declared".
118
+
101
119
  `repeating-linear-gradient()` and `repeating-radial-gradient()` tile the stop
102
120
  pattern from 0 to the last stop's offset across the whole box; the stop list is
103
121
  not forced to end at 100%, so `red 0%, blue 10%` repeats every 10%. A normal
@@ -105,18 +123,34 @@ gradient whose last stop is below 100% holds that colour for the rest of the box
105
123
  as in CSS.
106
124
 
107
125
  The background colour sits under every layer. Gradients are partially
108
- transparent if their stops are; a radial gradient is exact per pixel, while a
109
- linear one still samples 18 bands and can show banding on a large soft ramp.
126
+ transparent if their stops are; both kinds are exact per pixel, so hard stops and
127
+ 1px details stay hard instead of banding.
110
128
 
111
129
  **Generated content** needs no new display-list kind: `::before`/`::after` are
112
130
  ordinary layout boxes owned by the per-restyle arena in `style.cpp`, so their
113
131
  background, border, text and transforms flow through the same paint walk.
114
132
  `content` is resolved from strings, `attr()` and open/close quotes.
115
133
 
116
- **`text-shadow`** is emitted as shadow `PaintText` runs *before* the real run, so
117
- the text pipeline draws them without a new shader. A blurred shadow is nine
118
- translucent copies spread around the offset (the same approximation `filter:
119
- blur()` uses).
134
+ **`text-shadow`** with no blur is emitted as shadow `PaintText` runs *before* the
135
+ real run, so the text pipeline draws it crisply. A blurred shadow is a real
136
+ Gaussian: `paint_shadow.cpp` rasterises the glyph coverage, convolves it with a
137
+ separable kernel (`sigma = blur / 2`, cropped at ±3σ), and emits one tinted image
138
+ quad. The mask is cached by text/font/blur, so an animated colour only changes
139
+ the quad's tint.
140
+
141
+ **`box-shadow`** uses the same Gaussian mask: the rounded-rectangle silhouette is
142
+ rasterised (spread expands or shrinks it), convolved, and composited behind the
143
+ background; `inset` shadows invert the mask, crop it to the padding box and paint
144
+ above the background. A zero-blur outer shadow stays an exact rounded rect. The
145
+ declaration list is painted last-layer-first, matching CSS.
146
+
147
+ **`backdrop-filter`** is composited by the same offscreen machinery as
148
+ `mix-blend-mode`: when the painter reaches the box, the renderer copies the
149
+ scene painted so far into a scratch texture, applies the Gaussian and one
150
+ combined colour matrix (brightness/contrast/grayscale/sepia/invert/saturate/
151
+ hue-rotate/opacity multiplied together) in a full-screen shader, and composites
152
+ the result back through the box's rounded border-box mask. A `backdrop-filter`
153
+ therefore shows as a `backdrop …` op in `paintList()`.
120
154
 
121
155
  **`clip-path`** is resolved in `paint_clip.cpp` to a device-space polygon (the
122
156
  box transform is applied to every point), triangulated with ear clipping, and
@@ -133,8 +167,8 @@ colour, so a box and its descendants see the filter. `blur()` and
133
167
  `drop-shadow()` are approximations at the display-list level: blur spreads nine
134
168
  translucent copies per shape, drop-shadow is a box/text silhouette. This keeps
135
169
  the ordinary painter single-pass at the cost of group semantics: overlapping
136
- descendants of a blurred/filtered group can darken, and `backdrop-filter` cannot
137
- sample what is behind the element, so it parses and cascades but paints nothing.
170
+ descendants of a blurred/filtered group can darken. `backdrop-filter` is the
171
+ exception — it has its own offscreen snapshot/filter pass (see above).
138
172
 
139
173
  **`mix-blend-mode`** is the exception that does need an offscreen layer. A box
140
174
  with a non-`normal` mode is wrapped in `LayerBegin`/`LayerEnd` markers (visible
@@ -149,37 +183,59 @@ SPIRV-Cross (Metal); the two scene textures per nesting level are reused across
149
183
  frames and ping-pong so the backdrop stays sampleable while it is written.
150
184
  Backends that consume neither format — the D3D12/DXIL path, which has no shader
151
185
  compiler here — keep painting the group with ordinary source-over blending.
152
- `backdrop-filter` still paints nothing.
186
+ `backdrop-filter` uses the same runtime-compiled path; when the shader backend is
187
+ unavailable it is skipped while the rest of the page still paints.
153
188
 
154
189
  ## Text stack (M4b)
155
190
 
156
- `runtime/ext_gui/text.{h,cpp}` wraps **HarfBuzz** (shaping) and **FreeType**
157
- (metrics + eventual rasterisation). Both are built statically and linked into
158
- `gui.a` by `scripts/build-gui.ts`.
159
-
160
- - Fonts are resolved from well-known system paths (Helvetica/Arial on macOS,
161
- DejaVu/Liberation on Linux, Segoe UI/Arial on Windows), overridable with
162
- `XT_GUI_FONT` (and `XT_GUI_FONT_MONO`), and cached per `(family class, size)`.
163
- Only regular upright faces are used for now; weight/italic selection is a
164
- later refinement.
165
- - The default candidates prefer a **CJK-capable** face (Microsoft YaHei on
166
- Windows, PingFang/STHeiti on macOS, Noto Sans CJK/WenQuanYi on Linux) before
167
- the Latin-only ones. Setting `XT_GUI_FONT` to a Latin-only face gives its
168
- Latin design instead (at the cost of CJK coverage).
191
+ `runtime/ext_gui/text.{h,cpp}` is the front-end; it dispatches through a small
192
+ backend table. The default backend is the bundled **stb_truetype**
193
+ (`text_stb.cpp`, `runtime/ext_gui/stb_truetype.h`). The optional **`shaping`**
194
+ link feature installs `text_hb.cpp`, a HarfBuzz + FreeType backend, and is
195
+ selected automatically for non-ASCII text or an author `@font-face` rule. Both
196
+ backends share the font database in `text_font_db.cpp` (discovery and indexing)
197
+ and the CSS matcher in `text_fonts.cpp`, plus the sfnt metadata reader in
198
+ `text_font_sfnt.cpp`, so a CSS style resolves to the same real face whichever
199
+ shaper is linked.
200
+
201
+ - Fonts are resolved from the installed system faces. The `font-family` list is
202
+ matched case-insensitively; `sans-serif`, `serif`, `monospace` (and the
203
+ `system-ui`/`ui-*` aliases) map to platform defaults, while any other family
204
+ name discovers the platform font directories once. `font-weight` (1–1000) and
205
+ `font-style: italic` select that family's real bold/italic face using the CSS
206
+ Fonts weight-matching order; a missing weight picks the nearest face and a
207
+ family without an italic face uses its upright face — there is no synthetic
208
+ bold or oblique. Faces are cached per `(file, collection index, size)`.
209
+ `XT_GUI_FONT` / `XT_GUI_FONT_MONO` still override the generic defaults.
210
+ - Author **`@font-face`** rules load local sfnt faces (`.ttf`, `.ttc`, `.otf`,
211
+ `.otc`). The `src` list is tried from left to right; `local(...)`, remote and
212
+ `data:` entries are skipped. A relative URL is a local `file:` path resolved
213
+ against the process working directory — the same policy as image sources — and
214
+ `file://` URLs (including percent-encoded paths) are accepted. The
215
+ `font-weight`/`font-style` descriptors select the face; a weight range
216
+ (`font-weight: 100 900`) is honoured, and later rules with the same family and
217
+ descriptors replace earlier ones. CSS matches the declared family name even
218
+ when it differs from the font file's internal name. When no source is
219
+ readable the family simply falls through to the next family in the CSS list.
220
+ - The default candidates prefer a **CJK-capable TrueType/TTC** face (Microsoft
221
+ YaHei on Windows, PingFang/STHeiti on macOS, Noto Sans CJK/WenQuanYi on Linux)
222
+ before the Latin-only ones. A family's weight/style variants are resolved
223
+ across its files, and a candidate stb_truetype cannot parse (for example a
224
+ CFF/OTF collection) is skipped in favour of the next one; the `shaping`
225
+ backend loads it instead.
169
226
  - **Codepoints the primary face lacks fall back to another face.** The engine
170
227
  keeps a small fallback chain — the platform's symbol face (Segoe UI Symbol,
171
- Apple Symbols, Noto Sans Symbols, DejaVu Sans) first, then the other family
172
- class — and resolves each codepoint to the first face in it that has the
173
- glyph. The run is shaped in segments, one per face, so a mixed run (Latin
228
+ Apple Symbols, Noto Sans Symbols, DejaVu Sans) first, then the default sans,
229
+ serif and monospace faces — and resolves each codepoint to the first face in
230
+ it that has the glyph. The run is shaped in segments, one per face, so a mixed
231
+ run (Latin
174
232
  plus an arrow, box-drawing or geometric shape, or Latin inside CJK) draws
175
- every glyph instead of dropping the missing ones as `.notdef`. This is what
176
- makes a character such as `▮`/`▯` (U+25AE/U+25AF, used by the pelican-bike
177
- speed bar) visible even when the CJK primary has no geometric shapes. Each
178
- shaped glyph carries its face (`ShapedGlyph::font`) so the renderer rasterises
179
- it from the right atlas entry; `xt_text_scale` maps a face to the display's
180
- scale factor.
181
- - `xt_text_measure_width` shapes the run with HarfBuzz (so kerning and
182
- ligatures are honoured), `xt_text_metrics` returns FreeType's ascent /
233
+ every glyph instead of dropping the missing ones as `.notdef`. Each shaped
234
+ glyph carries its face (`ShapedGlyph::font`) so the renderer rasterises it
235
+ from the right atlas entry; `xt_text_scale` maps a face to the display's scale
236
+ factor.
237
+ - `xt_text_measure_width` maps each codepoint to a glyph and adds its advance
238
+ plus the font's `kern` table; `xt_text_metrics` returns stb_truetype's ascent /
183
239
  descent / normal line height. When no font file can be found the module falls
184
240
  back to a deterministic per-byte approximation, so layout still works.
185
241
  - `xt_text_shape_run` returns positioned glyphs and `xt_text_rasterize` renders
@@ -189,6 +245,15 @@ compiler here — keep painting the group with ordinary source-over blending.
189
245
  - Layout uses these real metrics for text widths, line breaking and
190
246
  `line-height: normal`; `measureText`/`fontMetrics` expose them to tests.
191
247
 
248
+ Shape trade-off: the default is a simple shaper, not a full OpenType engine. It
249
+ handles Latin/CJK/Cyrillic/Greek, basic kerning and per-codepoint fallback, but
250
+ it does not run ligatures or complex-script shaping (Arabic/Indic/Thai), and
251
+ stb_truetype only accepts TrueType outlines, not CFF/OTF or variable fonts. The
252
+ choice keeps a DOM-only binary near 3 MiB. The optional **`shaping`** link
253
+ feature installs the HarfBuzz + FreeType backend instead (about 1.2 MiB) and
254
+ restores full OpenType shaping and CFF/OTF/variable-font support; it is selected
255
+ automatically for non-ASCII text and can be forced with `--feature shaping`.
256
+
192
257
  ## Performance (M20)
193
258
 
194
259
  The engine is a retained-mode pipeline: a DOM mutation restyles the document,
@@ -210,7 +275,17 @@ is what `tests/e2e/gui-perf.test.ts` asserts on.
210
275
  glyphs / 8,192 entries; when a generation is full the *spare* one is dropped
211
276
  and becomes current, so the hot path has no LRU bookkeeping. A word is shaped
212
277
  once and reused by every later layout pass and repaint that draws it. Primary
213
- face lookups are cached by (family class, size) as well.
278
+ face lookups are cached by (family, weight, style, size) as well; the font
279
+ database itself scans the platform directories only once, on the first
280
+ non-generic family request.
281
+ - **No-op writes.** `textContent` and `style.<property>` do nothing when the
282
+ node already shows that value: the document is not dirtied, so no restyle,
283
+ relayout, repaint or upload follows. A HUD that assigns the same clock string,
284
+ the same bar width or the same transform every frame therefore costs nothing —
285
+ before this each assignment rebuilt a text node (an allocation, and one the
286
+ collector then had to sweep) or re-serialized the inline style, and dirtied
287
+ the document for a full restyle. `classList` and `removeAttribute` already
288
+ behaved this way; `setAttribute` still dirties unconditionally.
214
289
  - **Frame loop.** The media/canvas tables are reconciled with the DOM only when
215
290
  the document changed (or on the first frame) instead of walking the tree every
216
291
  frame. A frame presents only when something happened — a mutation, a scroll,
@@ -55,6 +55,11 @@ that case with a single 16 ms `xt_set_timeout` tick:
55
55
  queued animation-frame callbacks before layout, so a page can stop itself),
56
56
  - it re-arms **only while callbacks remain**, so an idle page lets the event
57
57
  loop drain and the process exit — the browser-tab lifecycle;
58
+ - it schedules the next tick for *frame start + 16 ms*, not 16 ms after the
59
+ frame finished: the frame's own duration is not added to the interval, so a
60
+ 10 ms frame runs at a 16 ms period instead of 26 ms, and a page slower than
61
+ 16 ms runs as fast as it can instead of falling a frame's worth of time
62
+ further behind on every frame;
58
63
  - it stands aside while `xt_gui_run_active()` is set (the program called
59
64
  `run()`), so the same window is not rendered twice per tick;
60
65
  - `XT_GUI_AUTOCLOSE_MS=n` makes the pump quit after `n` milliseconds, which is
package/doc/gui/webgl.md CHANGED
@@ -48,8 +48,19 @@ reports the type as well.
48
48
  `readPixels` flips rows back, and the compositor sees an upright texture.
49
49
  - A fixed-function state change (`blendFunc`, `depthFunc`, `cullFace`,
50
50
  `colorMask`, …) changes which `SDL_GPUGraphicsPipeline` is needed, so
51
- pipelines are created lazily per (program, vertex layout, state) and cached on
52
- the program.
51
+ pipelines are created lazily and cached on the program. The key is the
52
+ *content* of the draw: the program, the primitive mode, a hash of the vertex
53
+ layout (attribute locations, formats, offsets, buffer pitches and input rates),
54
+ the reflected fixed-function state and the attachment formats. It deliberately
55
+ excludes the context's monotonic version counters (`attribs_version_`,
56
+ `framebuffer_version_`) and the GPU buffer handles: those advance on every VAO
57
+ bind, framebuffer switch or buffer re-creation without changing the pipeline,
58
+ and keying on them re-created every pipeline once per frame in a scene that
59
+ binds several layouts per frame — three.js does, and measured 248
60
+ `SDL_CreateGPUGraphicsPipeline` calls per frame while the per-program cache
61
+ grew without bound. `tests/e2e/gui-webgl-pipeline-cache.test.ts` pins the
62
+ steady state: two draws with the same layout and different buffers create no
63
+ pipeline after the first frame.
53
64
 
54
65
  ## Shaders: GLSL ES to SPIR-V (and on to MSL)
55
66
 
@@ -283,9 +294,15 @@ format — Direct3D 12, which would need DXIL — `getContext("webgl")` and
283
294
  `getContext("webgl2")` both answer `null`; the 2D canvas and the rest of the page
284
295
  still work.
285
296
 
286
- glslang and SPIRV-Cross are fetched and built by `npm run gui` and merged into
287
- `gui.a` / `gui.lib` (see [Packaging and build](./build.md)); a program that only
288
- uses the 2D canvas or the DOM pays no run-time cost beyond the archive size.
297
+ glslang is fetched and built by `npm run gui` and merged into `gui.a` /
298
+ `gui.lib`; SPIRV-Cross is merged **on macOS only**, because no other SDL_GPU
299
+ backend consumes MSL (see [Packaging and build](./build.md)). Both are reached
300
+ through the optional **`gpu` link feature**: the compiler selects it when the
301
+ program may call `getContext("webgl…")` or use `mix-blend-mode`, and only then
302
+ does the feature bridge pull the translator members out of `gui.a`. The WebGL
303
+ context and DOM bindings are the separate **`webgl` feature**, which requires
304
+ `gpu`; a program that only uses the 2D canvas or the DOM links neither glslang,
305
+ SPIRV-Cross, nor any `webgl_*` member (see [Link features](./build.md#link-features)).
289
306
 
290
307
  ## Testing
291
308
 
@@ -339,6 +356,7 @@ Canvas2D fallback is bundled any more**. Verified with three.js r160 through SDL
339
356
  Features outside this engine's subset still cannot be emulated: transform
340
357
  feedback and MSAA. Depth textures, float render targets and compressed textures
341
358
  are available, and comparative `sampler2DShadow` sampling is translated, so
342
- three.js shadow maps can use the hardware depth compare on both backends. A
359
+ three.js shadow maps can use the hardware depth compare on both backends (see
360
+ [known issues](./known-issues.md) for an Intel-Mac driver caveat). A
343
361
  scene that needs transform feedback or MSAA will not render correctly;
344
362
  everything the previous Canvas2D fallback could show now goes through the GPU.