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
@@ -0,0 +1,438 @@
1
+ /*
2
+ * xbintsc GUI engine — optional HarfBuzz + FreeType shaping backend.
3
+ *
4
+ * Installed by the `shaping` link feature through `text_bridge.c`. HarfBuzz does
5
+ * shaping (ligatures/kerning/complex scripts), FreeType owns the glyph outlines
6
+ * and the vertical metrics. We create one HarfBuzz font per (file, collection
7
+ * index, pixel size) and scale it so that advances come back in 26.6 pixels.
8
+ *
9
+ * A run is shaped with a *single* face, but that face rarely covers every
10
+ * codepoint in it — a CJK face such as Microsoft YaHei / PingFang / Noto Sans
11
+ * CJK has no geometric shapes (U+25AE ▮, U+25AF ▯, …), and a Latin face has no
12
+ * CJK. Each codepoint therefore resolves to the first face in a small fallback
13
+ * chain that has it, the run is shaped in segments (one per face), and each
14
+ * shaped glyph carries the face it came from so the rasteriser draws it from
15
+ * the right atlas entry. Without this a missing glyph came back as `.notdef`
16
+ * (glyph 0) and the renderer silently dropped it.
17
+ *
18
+ * The CSS family/weight/style lookup lives in `text_fonts.cpp`; this backend
19
+ * only loads the concrete `(path, collection index)` candidates it is given, so
20
+ * CFF/OTF and arbitrary system families work here even though the default
21
+ * stb_truetype backend cannot outline them.
22
+ */
23
+
24
+ #include "text_api.h"
25
+ #include "text_fonts.h"
26
+
27
+ #include <ft2build.h>
28
+ #include FT_FREETYPE_H
29
+
30
+ #include <hb.h>
31
+ #include <hb-ot.h>
32
+
33
+ #include <cstddef>
34
+ #include <cstdint>
35
+ #include <cstdio>
36
+ #include <cstring>
37
+ #include <memory>
38
+ #include <string>
39
+ #include <unordered_map>
40
+ #include <vector>
41
+
42
+ namespace xtgui {
43
+
44
+ /* Opaque to callers (declared in text.h); defined here so the loading helpers
45
+ * in the anonymous namespace can build it. */
46
+ struct HbFont {
47
+ FT_Face ft_face = nullptr;
48
+ hb_font_t *hb_font = nullptr;
49
+ std::string path;
50
+ int face_index = 0;
51
+ float pixel_size = 16.0f;
52
+ float ascent = 0;
53
+ float descent = 0;
54
+ float line_height = 0;
55
+
56
+ ~HbFont() {
57
+ if (hb_font != nullptr) hb_font_destroy(hb_font);
58
+ if (ft_face != nullptr) FT_Done_Face(ft_face);
59
+ }
60
+ };
61
+
62
+ namespace {
63
+
64
+ struct Engine {
65
+ bool initialized = false;
66
+ FT_Library ft = nullptr;
67
+ bool ft_ok = false;
68
+ std::unordered_map<std::string, std::unique_ptr<HbFont>> cache; // key -> face (may be null)
69
+ std::unordered_map<std::string, std::vector<HbFont *>> fallbacks; // face+size -> faces
70
+ std::unordered_map<std::string, hb_face_t *> hb_faces; // path#index -> face
71
+ /** Primary face by (family, weight, style, pixel size): `resolve` and
72
+ * `metrics` are called per text box per layout, and rebuilding the key each
73
+ * time showed up in the profile. */
74
+ std::unordered_map<std::string, HbFont *> primary;
75
+ };
76
+
77
+ Engine &engine() {
78
+ static Engine instance;
79
+ return instance;
80
+ }
81
+
82
+ /* Decode one UTF-8 sequence starting at `index`; advances `index`. */
83
+ uint32_t decodeUtf8(const std::string &text, size_t &index) {
84
+ unsigned char first = (unsigned char)text[index];
85
+ if (first < 0x80) {
86
+ index += 1;
87
+ return first;
88
+ }
89
+ uint32_t code_point = 0;
90
+ size_t extra = 0;
91
+ if ((first & 0xe0) == 0xc0) {
92
+ code_point = first & 0x1f;
93
+ extra = 1;
94
+ } else if ((first & 0xf0) == 0xe0) {
95
+ code_point = first & 0x0f;
96
+ extra = 2;
97
+ } else if ((first & 0xf8) == 0xf0) {
98
+ code_point = first & 0x07;
99
+ extra = 3;
100
+ } else {
101
+ index += 1;
102
+ return 0xfffd;
103
+ }
104
+ if (index + extra >= text.size()) {
105
+ index = text.size();
106
+ return 0xfffd;
107
+ }
108
+ for (size_t i = 1; i <= extra; i++) {
109
+ unsigned char next = (unsigned char)text[index + i];
110
+ if ((next & 0xc0) != 0x80) {
111
+ index += 1;
112
+ return 0xfffd;
113
+ }
114
+ code_point = (code_point << 6) | (next & 0x3f);
115
+ }
116
+ index += extra + 1;
117
+ return code_point;
118
+ }
119
+
120
+ void ensureInitialized() {
121
+ Engine &e = engine();
122
+ if (e.initialized) return;
123
+ e.initialized = true;
124
+ if (FT_Init_FreeType(&e.ft) != 0) {
125
+ e.ft = nullptr;
126
+ return;
127
+ }
128
+ e.ft_ok = true;
129
+ }
130
+
131
+ hb_face_t *hbFaceForPath(const std::string &path, int face_index) {
132
+ Engine &e = engine();
133
+ std::string key = path;
134
+ key += '#';
135
+ key += std::to_string(face_index);
136
+ auto it = e.hb_faces.find(key);
137
+ if (it != e.hb_faces.end()) return it->second;
138
+ hb_blob_t *blob = hb_blob_create_from_file(path.c_str());
139
+ hb_face_t *face = nullptr;
140
+ if (blob != nullptr && hb_blob_get_length(blob) > 0) {
141
+ face = hb_face_create(blob, (unsigned)face_index);
142
+ }
143
+ if (blob != nullptr) hb_blob_destroy(blob);
144
+ e.hb_faces.emplace(std::move(key), face);
145
+ return face;
146
+ }
147
+
148
+ std::unique_ptr<HbFont> createFont(const std::string &path, int face_index, float pixel_size) {
149
+ Engine &e = engine();
150
+ if (!e.ft_ok || path.empty()) return nullptr;
151
+
152
+ FT_Face face = nullptr;
153
+ if (FT_New_Face(e.ft, path.c_str(), face_index, &face) != 0) return nullptr;
154
+ FT_Set_Pixel_Sizes(face, 0, (FT_UInt)(pixel_size + 0.5f));
155
+
156
+ auto font = std::make_unique<HbFont>();
157
+ font->ft_face = face;
158
+ font->path = path;
159
+ font->face_index = face_index;
160
+ font->pixel_size = pixel_size;
161
+
162
+ FT_Size_Metrics metrics = face->size->metrics;
163
+ font->ascent = metrics.ascender / 64.0f;
164
+ font->descent = -metrics.descender / 64.0f;
165
+ font->line_height = metrics.height / 64.0f;
166
+
167
+ hb_face_t *hb_face = hbFaceForPath(path, face_index);
168
+ if (hb_face != nullptr) {
169
+ font->hb_font = hb_font_create(hb_face);
170
+ hb_ot_font_set_funcs(font->hb_font);
171
+ int scale = (int)(pixel_size * 64.0f + 0.5f);
172
+ hb_font_set_scale(font->hb_font, scale, scale);
173
+ unsigned ppem = (unsigned)(pixel_size + 0.5f);
174
+ hb_font_set_ppem(font->hb_font, ppem, ppem);
175
+ }
176
+ return font;
177
+ }
178
+
179
+ /* Resolve one face by path+index, keyed by path+collection index+size so the
180
+ * primary resolved through a `FontSpec` and the same face resolved as a
181
+ * fallback share one `HbFont` (and one glyph-atlas key). */
182
+ HbFont *resolveFacePath(const std::string &path, int face_index, float pixel_size) {
183
+ Engine &e = engine();
184
+ if (!e.ft_ok || path.empty()) return nullptr;
185
+ char header[64];
186
+ std::snprintf(header, sizeof(header), "p|%d|%.2f|", face_index, (double)pixel_size);
187
+ std::string key = header;
188
+ key += path;
189
+ auto it = e.cache.find(key);
190
+ if (it != e.cache.end()) return it->second.get();
191
+ std::unique_ptr<HbFont> font = createFont(path, face_index, pixel_size);
192
+ HbFont *raw = font.get();
193
+ e.cache.emplace(std::move(key), std::move(font));
194
+ return raw;
195
+ }
196
+
197
+ /* The resolved fallback faces for a primary, in order, cached per face+size. */
198
+ const std::vector<HbFont *> &fallbackFaces(HbFont *font) {
199
+ static const std::vector<HbFont *> kEmpty;
200
+ if (font == nullptr) return kEmpty;
201
+ Engine &e = engine();
202
+ char header[64];
203
+ std::snprintf(header, sizeof(header), "f|%.2f|", (double)font->pixel_size);
204
+ std::string key = header;
205
+ key += font->path;
206
+ key += '#';
207
+ key += std::to_string(font->face_index);
208
+ auto it = e.fallbacks.find(key);
209
+ if (it != e.fallbacks.end()) return it->second;
210
+
211
+ std::vector<FontFaceRef> candidates;
212
+ xt_font_fallback_faces(FontFaceRef{font->path, font->face_index}, false, candidates);
213
+ std::vector<HbFont *> faces;
214
+ for (const FontFaceRef &ref : candidates) {
215
+ HbFont *face = resolveFacePath(ref.path, ref.index, font->pixel_size);
216
+ if (face != nullptr) faces.push_back(face);
217
+ }
218
+ return e.fallbacks.emplace(std::move(key), std::move(faces)).first->second;
219
+ }
220
+
221
+ /* The face that draws `code_point`: the primary when it has the glyph, else the
222
+ * first fallback that does, else the primary (so `.notdef` is still skipped). */
223
+ HbFont *faceForCodePoint(HbFont *primary, uint32_t code_point) {
224
+ if (primary != nullptr && FT_Get_Char_Index(primary->ft_face, code_point) != 0) return primary;
225
+ for (HbFont *fallback : fallbackFaces(primary)) {
226
+ if (FT_Get_Char_Index(fallback->ft_face, code_point) != 0) return fallback;
227
+ }
228
+ return primary;
229
+ }
230
+
231
+ /* Shape one face's segment of a run and append its glyphs. */
232
+ float shapeSegment(const std::string &segment, HbFont *font, std::vector<ShapedGlyph> &out) {
233
+ if (font == nullptr || segment.empty()) return 0;
234
+ float total = 0;
235
+ if (font->hb_font == nullptr) {
236
+ /* HarfBuzz unavailable: fall back to FreeType's per-codepoint advances. */
237
+ size_t index = 0;
238
+ while (index < segment.size()) {
239
+ uint32_t code_point = decodeUtf8(segment, index);
240
+ FT_UInt glyph = FT_Get_Char_Index(font->ft_face, code_point);
241
+ float advance = 0;
242
+ if (FT_Load_Glyph(font->ft_face, glyph, FT_LOAD_DEFAULT) == 0) {
243
+ advance = font->ft_face->glyph->advance.x / 64.0f;
244
+ }
245
+ ShapedGlyph shaped;
246
+ shaped.glyph = glyph;
247
+ shaped.x_advance = advance;
248
+ shaped.font = reinterpret_cast<Font *>(font);
249
+ total += advance;
250
+ out.push_back(shaped);
251
+ }
252
+ return total;
253
+ }
254
+
255
+ hb_buffer_t *buffer = hb_buffer_create();
256
+ hb_buffer_add_utf8(buffer, segment.c_str(), (int)segment.size(), 0, -1);
257
+ hb_buffer_guess_segment_properties(buffer);
258
+ hb_shape(font->hb_font, buffer, nullptr, 0);
259
+ unsigned count = hb_buffer_get_length(buffer);
260
+ hb_glyph_info_t *infos = hb_buffer_get_glyph_infos(buffer, nullptr);
261
+ hb_glyph_position_t *positions = hb_buffer_get_glyph_positions(buffer, nullptr);
262
+ for (unsigned i = 0; i < count; i++) {
263
+ ShapedGlyph shaped;
264
+ shaped.glyph = infos[i].codepoint;
265
+ shaped.x_advance = positions[i].x_advance / 64.0f;
266
+ shaped.x_offset = positions[i].x_offset / 64.0f;
267
+ shaped.y_offset = positions[i].y_offset / 64.0f;
268
+ shaped.font = reinterpret_cast<Font *>(font);
269
+ total += shaped.x_advance;
270
+ out.push_back(shaped);
271
+ }
272
+ hb_buffer_destroy(buffer);
273
+ return total;
274
+ }
275
+
276
+ } // namespace
277
+
278
+ HbFont *xt_text_hb_resolve(const FontSpec &spec) {
279
+ ensureInitialized();
280
+ Engine &e = engine();
281
+ if (!e.ft_ok) return nullptr;
282
+
283
+ char suffix[80];
284
+ std::snprintf(suffix, sizeof(suffix), "|%d|%d|%.4f", spec.weight, spec.italic ? 1 : 0,
285
+ (double)spec.pixel_size);
286
+ std::string key = spec.family;
287
+ key += suffix;
288
+ auto cached = e.primary.find(key);
289
+ if (cached != e.primary.end()) return cached->second;
290
+
291
+ std::vector<FontFaceRef> faces;
292
+ xt_font_face_candidates(spec, false, faces);
293
+ HbFont *font = nullptr;
294
+ for (const FontFaceRef &ref : faces) {
295
+ font = resolveFacePath(ref.path, ref.index, spec.pixel_size);
296
+ if (font != nullptr) break;
297
+ }
298
+ e.primary.emplace(std::move(key), font);
299
+ return font;
300
+ }
301
+
302
+ HbFont *xt_text_hb_scale(HbFont *font, float pixel_size) {
303
+ if (font == nullptr) return nullptr;
304
+ return resolveFacePath(font->path, font->face_index, pixel_size);
305
+ }
306
+
307
+ float xt_text_hb_shape(const std::string &utf8, HbFont *font, std::vector<ShapedGlyph> &out) {
308
+ if (font == nullptr) return 0;
309
+ float total = 0;
310
+ size_t index = 0;
311
+ size_t segment_start = 0;
312
+ HbFont *segment_font = nullptr;
313
+ while (index < utf8.size()) {
314
+ size_t char_start = index;
315
+ uint32_t code_point = decodeUtf8(utf8, index);
316
+ HbFont *chosen = faceForCodePoint(font, code_point);
317
+ if (segment_font == nullptr) {
318
+ segment_font = chosen;
319
+ segment_start = char_start;
320
+ } else if (chosen != segment_font) {
321
+ total += shapeSegment(utf8.substr(segment_start, char_start - segment_start), segment_font, out);
322
+ segment_font = chosen;
323
+ segment_start = char_start;
324
+ }
325
+ }
326
+ if (segment_font != nullptr) {
327
+ total += shapeSegment(utf8.substr(segment_start), segment_font, out);
328
+ }
329
+ return total;
330
+ }
331
+
332
+ bool xt_text_hb_rasterize(HbFont *font, uint32_t glyph, GlyphImage &out) {
333
+ out.width = 0;
334
+ out.height = 0;
335
+ out.left = 0;
336
+ out.top = 0;
337
+ out.pixels.clear();
338
+ if (font == nullptr || font->ft_face == nullptr) return false;
339
+ if (FT_Load_Glyph(font->ft_face, glyph, FT_LOAD_DEFAULT) != 0) return false;
340
+ if (FT_Render_Glyph(font->ft_face->glyph, FT_RENDER_MODE_NORMAL) != 0) return false;
341
+ FT_GlyphSlot slot = font->ft_face->glyph;
342
+ const FT_Bitmap &bitmap = slot->bitmap;
343
+ out.left = slot->bitmap_left;
344
+ out.top = slot->bitmap_top;
345
+ if (bitmap.width == 0 || bitmap.rows == 0) return false;
346
+ out.width = (int)bitmap.width;
347
+ out.height = (int)bitmap.rows;
348
+ out.pixels.resize((size_t)bitmap.width * bitmap.rows);
349
+ for (unsigned row = 0; row < bitmap.rows; row++) {
350
+ const unsigned char *source = bitmap.buffer + (ptrdiff_t)row * bitmap.pitch;
351
+ std::memcpy(out.pixels.data() + (size_t)row * bitmap.width, source, bitmap.width);
352
+ }
353
+ return true;
354
+ }
355
+
356
+ void xt_text_hb_metrics(const FontSpec &spec, float *ascent, float *descent, float *line_height) {
357
+ HbFont *font = xt_text_hb_resolve(spec);
358
+ if (font != nullptr) {
359
+ if (ascent != nullptr) *ascent = font->ascent;
360
+ if (descent != nullptr) *descent = font->descent;
361
+ if (line_height != nullptr) *line_height = font->line_height;
362
+ return;
363
+ }
364
+ if (ascent != nullptr) *ascent = spec.pixel_size * 0.8f;
365
+ if (descent != nullptr) *descent = spec.pixel_size * 0.2f;
366
+ if (line_height != nullptr) *line_height = spec.pixel_size * 1.2f;
367
+ }
368
+
369
+ bool xt_text_hb_ready() {
370
+ FontSpec spec;
371
+ return xt_text_hb_resolve(spec) != nullptr;
372
+ }
373
+
374
+ void xt_text_hb_shutdown() {
375
+ Engine &e = engine();
376
+ /* Cached shaped runs point at faces, so drop them before the faces go. */
377
+ e.primary.clear();
378
+ e.fallbacks.clear();
379
+ e.cache.clear();
380
+ for (auto &entry : e.hb_faces) {
381
+ if (entry.second != nullptr) hb_face_destroy(entry.second);
382
+ }
383
+ e.hb_faces.clear();
384
+ if (e.ft != nullptr) {
385
+ FT_Done_FreeType(e.ft);
386
+ e.ft = nullptr;
387
+ }
388
+ e.ft_ok = false;
389
+ e.initialized = false;
390
+ }
391
+
392
+ } // namespace xtgui
393
+ namespace xtgui {
394
+
395
+ namespace {
396
+
397
+ Font *api_resolve(const FontSpec &spec) {
398
+ return reinterpret_cast<Font *>(xt_text_hb_resolve(spec));
399
+ }
400
+
401
+ Font *api_scale(Font *font, float pixel_size) {
402
+ return reinterpret_cast<Font *>(
403
+ xt_text_hb_scale(reinterpret_cast<HbFont *>(font), pixel_size));
404
+ }
405
+
406
+ float api_shape(const std::string &utf8, Font *font, std::vector<ShapedGlyph> &out) {
407
+ return xt_text_hb_shape(utf8, reinterpret_cast<HbFont *>(font), out);
408
+ }
409
+
410
+ bool api_rasterize(Font *font, uint32_t glyph, GlyphImage &out) {
411
+ return xt_text_hb_rasterize(reinterpret_cast<HbFont *>(font), glyph, out);
412
+ }
413
+
414
+ void api_metrics(const FontSpec &spec, float *ascent, float *descent, float *line_height) {
415
+ xt_text_hb_metrics(spec, ascent, descent, line_height);
416
+ }
417
+
418
+ bool api_ready() { return xt_text_hb_ready(); }
419
+
420
+ void api_shutdown() { xt_text_hb_shutdown(); }
421
+
422
+ } // namespace
423
+
424
+ const TextApi kTextApi = {
425
+ api_resolve,
426
+ api_scale,
427
+ api_shape,
428
+ api_rasterize,
429
+ api_metrics,
430
+ api_ready,
431
+ api_shutdown,
432
+ };
433
+
434
+ } // namespace xtgui
435
+
436
+ extern "C" void xt_text_feature_register(void) {
437
+ xtgui::xt_text_api_install(&xtgui::kTextApi);
438
+ }