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,428 @@
1
+ /*
2
+ * xbintsc GUI engine — CSS transitions and `@keyframes` animations.
3
+ *
4
+ * The animatable value switch lives in `animation_values.cpp`; this unit owns
5
+ * the document clock: which transitions/animations are active, how local time
6
+ * maps to an iteration progress, and how the displayed style map is rebuilt
7
+ * before layout.
8
+ */
9
+
10
+ #include "document.h"
11
+
12
+ #include <algorithm>
13
+ #include <cmath>
14
+ #include <string>
15
+ #include <vector>
16
+
17
+ #include "style_internal.h"
18
+
19
+ namespace xtgui {
20
+
21
+ namespace {
22
+
23
+ /** A cubic Bézier with endpoints (0,0) and (1,1), evaluated at `t` on the *x*
24
+ * axis. CSS uses this for `cubic-bezier()` and for its `ease*` keywords. The
25
+ * solver is a few bisection steps: timing functions are evaluated at most once
26
+ * per animated property per frame, so a closed form is not worth the edge cases
27
+ * of a degenerate curve. */
28
+ float cubicBezier(float t, float x1, float y1, float x2, float y2) {
29
+ if (t <= 0.0f) return 0.0f;
30
+ if (t >= 1.0f) return 1.0f;
31
+ auto sample = [](float u, float p1, float p2) {
32
+ float v = 1.0f - u;
33
+ return 3.0f * v * v * u * p1 + 3.0f * v * u * u * p2 + u * u * u;
34
+ };
35
+ float low = 0.0f;
36
+ float high = 1.0f;
37
+ float u = t;
38
+ for (int i = 0; i < 24; i++) {
39
+ float x = sample(u, x1, x2);
40
+ if (x < t) low = u;
41
+ else high = u;
42
+ u = (low + high) * 0.5f;
43
+ }
44
+ return sample(u, y1, y2);
45
+ }
46
+
47
+ float easeProgress(float t, const TimingFunction &timing) {
48
+ if (t <= 0.0f) {
49
+ if (timing.kind == TimingFunction::Kind::StepsStart) {
50
+ float steps = timing.steps > 0 ? (float)timing.steps : 1.0f;
51
+ return 1.0f / steps;
52
+ }
53
+ return 0.0f;
54
+ }
55
+ if (t >= 1.0f) return 1.0f;
56
+ switch (timing.kind) {
57
+ case TimingFunction::Kind::Linear:
58
+ return t;
59
+ case TimingFunction::Kind::EaseIn:
60
+ return cubicBezier(t, 0.42f, 0.0f, 1.0f, 1.0f);
61
+ case TimingFunction::Kind::EaseOut:
62
+ return cubicBezier(t, 0.0f, 0.0f, 0.58f, 1.0f);
63
+ case TimingFunction::Kind::EaseInOut:
64
+ return cubicBezier(t, 0.42f, 0.0f, 0.58f, 1.0f);
65
+ case TimingFunction::Kind::CubicBezier:
66
+ return cubicBezier(t, timing.x1, timing.y1, timing.x2, timing.y2);
67
+ case TimingFunction::Kind::StepsStart: {
68
+ float steps = timing.steps > 0 ? (float)timing.steps : 1.0f;
69
+ float value = std::ceil(t * steps) / steps;
70
+ return value > 1.0f ? 1.0f : value;
71
+ }
72
+ case TimingFunction::Kind::StepsEnd: {
73
+ float steps = timing.steps > 0 ? (float)timing.steps : 1.0f;
74
+ if (t >= 1.0f) return 1.0f;
75
+ return std::floor(t * steps) / steps;
76
+ }
77
+ case TimingFunction::Kind::Ease:
78
+ default:
79
+ return cubicBezier(t, 0.25f, 0.1f, 0.25f, 1.0f);
80
+ }
81
+ }
82
+
83
+ bool timingEqual(const TimingFunction &a, const TimingFunction &b) {
84
+ return a.kind == b.kind && a.x1 == b.x1 && a.y1 == b.y1 && a.x2 == b.x2 && a.y2 == b.y2 &&
85
+ a.steps == b.steps;
86
+ }
87
+
88
+ bool animationSpecEqual(const AnimationSpec &a, const AnimationSpec &b) {
89
+ return a.name == b.name && a.duration == b.duration && a.delay == b.delay &&
90
+ a.iterations == b.iterations && a.direction == b.direction && a.fill == b.fill &&
91
+ a.paused == b.paused && timingEqual(a.timing, b.timing);
92
+ }
93
+
94
+ bool specMatches(const std::string &spec, const std::string &property) {
95
+ return spec == "all" || spec == property;
96
+ }
97
+
98
+ bool directionReversed(const AnimationSpec &spec, float iteration) {
99
+ int parity = (int)std::fmod(std::floor(iteration), 2.0f);
100
+ switch (spec.direction) {
101
+ case AnimationDirection::Normal:
102
+ return false;
103
+ case AnimationDirection::Reverse:
104
+ return true;
105
+ case AnimationDirection::Alternate:
106
+ return parity != 0;
107
+ case AnimationDirection::AlternateReverse:
108
+ return parity == 0;
109
+ }
110
+ return false;
111
+ }
112
+
113
+ float directionProgress(const AnimationSpec &spec, float iteration, float fraction) {
114
+ return directionReversed(spec, iteration) ? 1.0f - fraction : fraction;
115
+ }
116
+
117
+ struct AnimationProgress {
118
+ bool apply = false;
119
+ float progress = 0.0f;
120
+ };
121
+
122
+ /** Resolve the animation's local time to a progress in [0,1], honouring the
123
+ * iteration count, direction and fill mode. The timing function is applied per
124
+ * keyframe interval by the caller, as CSS specifies. */
125
+ AnimationProgress animationProgress(const AnimationSpec &spec, float local_ms) {
126
+ AnimationProgress result;
127
+ if (spec.name == "none" || spec.iterations == 0.0f) return result;
128
+ const float duration = spec.duration * 1000.0f;
129
+ const bool backwards =
130
+ spec.fill == AnimationFillMode::Backwards || spec.fill == AnimationFillMode::Both;
131
+ const bool forwards =
132
+ spec.fill == AnimationFillMode::Forwards || spec.fill == AnimationFillMode::Both;
133
+ if (local_ms < 0.0f) {
134
+ if (!backwards) return result;
135
+ result.apply = true;
136
+ result.progress = directionProgress(spec, 0.0f, 0.0f);
137
+ return result;
138
+ }
139
+ if (duration <= 0.0f) {
140
+ if (!forwards) return result;
141
+ result.apply = true;
142
+ result.progress = directionProgress(spec, 0.0f, 1.0f);
143
+ return result;
144
+ }
145
+ const bool infinite = spec.iterations < 0.0f;
146
+ const float iterations = infinite ? 0.0f : spec.iterations;
147
+ float t = local_ms / duration;
148
+ if (!infinite && t >= iterations) {
149
+ if (!forwards) return result;
150
+ float last = std::ceil(iterations) - 1.0f;
151
+ if (last < 0.0f) last = 0.0f;
152
+ float fraction = iterations - last;
153
+ result.apply = true;
154
+ result.progress = directionProgress(spec, last, fraction);
155
+ return result;
156
+ }
157
+ if (spec.paused) t = 0.0f;
158
+ float index = std::floor(t);
159
+ float fraction = t - index;
160
+ result.apply = true;
161
+ result.progress = directionProgress(spec, index, fraction);
162
+ return result;
163
+ }
164
+
165
+ void applyKeyframeDeclarations(XtStyle *style, const std::vector<Declaration> &declarations,
166
+ const ApplyContext &context) {
167
+ for (const Declaration &declaration : declarations) {
168
+ /* Custom properties and animation bookkeeping cannot change a keyframe
169
+ * result; the engine's own value parser handles everything else. */
170
+ if (declaration.property.size() > 2 && declaration.property[0] == '-' &&
171
+ declaration.property[1] == '-')
172
+ continue;
173
+ if (declaration.property.compare(0, 9, "animation") == 0) continue;
174
+ xt_style_apply_declaration(style, declaration.property, declaration.value, context);
175
+ }
176
+ }
177
+
178
+ } // namespace
179
+
180
+ void XtDocument::startTransitions() {
181
+ if (display_.empty()) return; // first style computation: nothing to animate from
182
+ /* Without a `transition` declaration anywhere in the sheet (or in an inline
183
+ * style) no transition can start, so skip the O(nodes) scan `restyle` would
184
+ * otherwise do every time. */
185
+ if (!has_transitions_) return;
186
+ const std::vector<std::string> &properties = animatableProperties();
187
+ for (auto &entry : styles_) {
188
+ const Node *node = entry.first;
189
+ const XtStyle &target = entry.second;
190
+ /* Generated boxes are rebuilt on every restyle; a transition keyed by one
191
+ * would address a node that no longer exists. */
192
+ if (node->is_pseudo) continue;
193
+ if (target.transitions.empty()) continue;
194
+ auto prev_it = display_.find(node);
195
+ if (prev_it == display_.end()) continue;
196
+ const XtStyle &current = prev_it->second;
197
+ for (const TransitionSpec &spec : target.transitions) {
198
+ if (spec.duration <= 0.0f) continue;
199
+ for (const std::string &property : properties) {
200
+ if (!specMatches(spec.property, property)) continue;
201
+ AnimatableValue from;
202
+ AnimatableValue to;
203
+ if (!readAnimatable(current, property, &from)) continue;
204
+ if (!readAnimatable(target, property, &to)) continue;
205
+ if (animatableEqual(from, to)) continue;
206
+ AnimatableValue probe;
207
+ /* Values that cannot be mixed (a unit change, a different shadow list
208
+ * shape) snap instead of animating, as in CSS's discrete fallback. */
209
+ if (!interpolateAnimatable(from, to, 0.5f, &probe)) continue;
210
+ ActiveTransition running;
211
+ running.node = node;
212
+ running.property = property;
213
+ running.from = from;
214
+ running.to = to;
215
+ running.timing = spec.timing;
216
+ running.duration_ms = spec.duration * 1000.0f;
217
+ running.delay_ms = spec.delay * 1000.0f;
218
+ running.start_ms = (float)clock_ms_ + running.delay_ms;
219
+ /* Retarget an existing transition (mid-flight) instead of stacking. */
220
+ bool replaced = false;
221
+ for (ActiveTransition &existing : transitions_) {
222
+ if (existing.node == node && existing.property == property) {
223
+ existing = running;
224
+ replaced = true;
225
+ break;
226
+ }
227
+ }
228
+ if (!replaced) transitions_.push_back(running);
229
+ }
230
+ }
231
+ }
232
+ }
233
+
234
+ void XtDocument::startAnimations() {
235
+ if (!has_animations_) {
236
+ animations_.clear();
237
+ return;
238
+ }
239
+ std::vector<ActiveAnimation> active;
240
+ for (auto &entry : styles_) {
241
+ const Node *node = entry.first;
242
+ for (const AnimationSpec &spec : entry.second.animations) {
243
+ if (spec.name.empty() || spec.name == "none") continue;
244
+ const ActiveAnimation *existing = nullptr;
245
+ for (const ActiveAnimation &candidate : animations_) {
246
+ if (candidate.node == node && candidate.name == spec.name) {
247
+ existing = &candidate;
248
+ break;
249
+ }
250
+ }
251
+ ActiveAnimation running;
252
+ running.node = node;
253
+ running.name = spec.name;
254
+ running.spec = spec;
255
+ running.start_ms =
256
+ (existing != nullptr && animationSpecEqual(existing->spec, spec)) ? existing->start_ms
257
+ : (float)clock_ms_;
258
+ active.push_back(std::move(running));
259
+ }
260
+ }
261
+ animations_ = std::move(active);
262
+ }
263
+
264
+ bool declarationsAffect(const std::vector<Declaration> &declarations,
265
+ const std::string &property) {
266
+ for (const Declaration &declaration : declarations) {
267
+ const std::string &name = declaration.property;
268
+ if (name == property) return true;
269
+ if (name == "background" && property.compare(0, 11, "background-") == 0) return true;
270
+ if (name == "border" && property.compare(0, 7, "border-") == 0) return true;
271
+ if (name == "margin" && property.compare(0, 7, "margin-") == 0) return true;
272
+ if (name == "padding" && property.compare(0, 8, "padding-") == 0) return true;
273
+ if (name == "inset" &&
274
+ (property == "top" || property == "right" || property == "bottom" ||
275
+ property == "left"))
276
+ return true;
277
+ if (name == "gap" && (property == "row-gap" || property == "column-gap")) return true;
278
+ if (name == "flex" && property == "flex-basis") return true;
279
+ if (name == "font" && property == "font-size") return true;
280
+ }
281
+ return false;
282
+ }
283
+
284
+ void XtDocument::applyAnimations(std::unordered_map<const Node *, XtStyle> *out) {
285
+ if (animations_.empty() || out == nullptr) return;
286
+ const std::vector<std::string> &properties = animatableProperties();
287
+ for (const ActiveAnimation &animation : animations_) {
288
+ auto node_it = out->find(animation.node);
289
+ if (node_it == out->end()) continue;
290
+ const KeyframesRule *rule = xt_css_find_keyframes(sheet_, animation.name);
291
+ if (rule == nullptr || rule->keyframes.empty()) continue;
292
+ AnimationProgress progress = animationProgress(
293
+ animation.spec, (float)clock_ms_ - animation.start_ms - animation.spec.delay * 1000.0f);
294
+ if (!progress.apply) continue;
295
+
296
+ ApplyContext context;
297
+ const XtStyle *base_style = &node_it->second;
298
+ auto target_it = styles_.find(animation.node);
299
+ if (target_it != styles_.end()) base_style = &target_it->second;
300
+ context.rootFontSize = 16.0f;
301
+ context.parentFontSize = base_style->font_size;
302
+ context.viewportWidth = viewportWidth_;
303
+ context.viewportHeight = viewportHeight_;
304
+ /* One style per keyframe step: applying the declarations once lets the
305
+ * per-property search below see through shorthands such as `background`.
306
+ * Missing keyframe values come from the cascade target, not from a
307
+ * transition that may be mid-flight on another property. */
308
+ std::vector<XtStyle> step_styles(rule->keyframes.size(), *base_style);
309
+ for (size_t i = 0; i < rule->keyframes.size(); i++) {
310
+ applyKeyframeDeclarations(&step_styles[i], rule->keyframes[i].declarations, context);
311
+ }
312
+
313
+ for (const std::string &property : properties) {
314
+ /* Missing keyframes are the underlying style, not the adjacent step that
315
+ * happens to declare other properties. A transform declared at 0% and
316
+ * 100% therefore animates across the whole run even if 50% only changes
317
+ * `opacity`. */
318
+ long lower = -1;
319
+ long upper = -1;
320
+ for (size_t i = 0; i < rule->keyframes.size(); i++) {
321
+ if (!declarationsAffect(rule->keyframes[i].declarations, property)) continue;
322
+ if (rule->keyframes[i].offset <= progress.progress) lower = (long)i;
323
+ if (upper < 0 && rule->keyframes[i].offset >= progress.progress) upper = (long)i;
324
+ }
325
+ if (lower < 0 && upper < 0) continue;
326
+ const XtStyle &from_style = lower >= 0 ? step_styles[(size_t)lower] : *base_style;
327
+ const XtStyle &to_style = upper >= 0 ? step_styles[(size_t)upper] : *base_style;
328
+ float segment_start = lower >= 0 ? rule->keyframes[(size_t)lower].offset : 0.0f;
329
+ float segment_end = upper >= 0 ? rule->keyframes[(size_t)upper].offset : 1.0f;
330
+ float segment = 1.0f;
331
+ float span = segment_end - segment_start;
332
+ if (span > 0.00001f) {
333
+ segment = (progress.progress - segment_start) / span;
334
+ if (segment < 0.0f) segment = 0.0f;
335
+ if (segment > 1.0f) segment = 1.0f;
336
+ }
337
+ float eased = easeProgress(segment, animation.spec.timing);
338
+
339
+ AnimatableValue from;
340
+ AnimatableValue to;
341
+ if (!readAnimatable(from_style, property, &from)) continue;
342
+ if (!readAnimatable(to_style, property, &to)) continue;
343
+ AnimatableValue current;
344
+ bool has_current = readAnimatable(node_it->second, property, &current);
345
+ if (animatableEqual(from, to) && has_current && animatableEqual(current, from)) continue;
346
+ AnimatableValue mixed;
347
+ if (!interpolateAnimatable(from, to, eased, &mixed)) {
348
+ mixed = eased < 0.5f ? from : to;
349
+ }
350
+ writeAnimatable(&node_it->second, property, mixed);
351
+ }
352
+ }
353
+ }
354
+
355
+ /** True while an animation can still change the painted style. A finished
356
+ * `forwards`/`both` animation stays in `animations_` (its value must survive a
357
+ * later restyle) but no longer makes every frame dirty. */
358
+ bool animationRunning(const ActiveAnimation &animation, double clock_ms) {
359
+ if (animation.spec.paused) return false;
360
+ if (animation.spec.name == "none" || animation.spec.iterations == 0.0f) return false;
361
+ if (animation.spec.iterations < 0.0f) return true;
362
+ double end = (double)animation.start_ms + (double)animation.spec.delay * 1000.0 +
363
+ (double)animation.spec.duration * 1000.0 * (double)animation.spec.iterations;
364
+ return clock_ms < end;
365
+ }
366
+
367
+ void XtDocument::updateDisplay() {
368
+ /* Without a transition or animation the displayed style *is* the cascade
369
+ * result: skip the per-node deep copy of the whole style map (and keep
370
+ * `display_` empty, which `styleOf` already falls back from). */
371
+ if (!has_transitions_ && !has_animations_) {
372
+ if (!transitions_.empty()) transitions_.clear();
373
+ if (!animations_.empty()) animations_.clear();
374
+ if (!display_.empty()) display_.clear();
375
+ layout_.compute(root_.get(), styles_, viewportWidth_, viewportHeight_, &mediaSizes_,
376
+ &canvasInfo_);
377
+ return;
378
+ }
379
+ display_ = styles_;
380
+ for (size_t i = 0; i < transitions_.size();) {
381
+ ActiveTransition &transition = transitions_[i];
382
+ auto node_it = display_.find(transition.node);
383
+ if (node_it == display_.end()) {
384
+ transitions_.erase(transitions_.begin() + (long)i);
385
+ continue;
386
+ }
387
+ float elapsed = (float)clock_ms_ - transition.start_ms;
388
+ float progress = transition.duration_ms > 0.0f ? elapsed / transition.duration_ms : 1.0f;
389
+ if (progress > 1.0f) progress = 1.0f;
390
+ float eased = easeProgress(progress, transition.timing);
391
+ AnimatableValue mixed;
392
+ if (interpolateAnimatable(transition.from, transition.to, eased, &mixed)) {
393
+ writeAnimatable(&node_it->second, transition.property, mixed);
394
+ }
395
+ if (progress >= 1.0f) {
396
+ transitions_.erase(transitions_.begin() + (long)i);
397
+ } else {
398
+ i++;
399
+ }
400
+ }
401
+ applyAnimations(&display_);
402
+ layout_.compute(root_.get(), display_, viewportWidth_, viewportHeight_, &mediaSizes_,
403
+ &canvasInfo_);
404
+ }
405
+
406
+ bool XtDocument::advance(double delta_ms) {
407
+ const double previous = clock_ms_;
408
+ clock_ms_ += delta_ms;
409
+ if (transitions_.empty() && animations_.empty()) return false;
410
+ bool running = !transitions_.empty();
411
+ bool just_finished = false;
412
+ for (const ActiveAnimation &animation : animations_) {
413
+ if (animationRunning(animation, clock_ms_)) {
414
+ running = true;
415
+ continue;
416
+ }
417
+ if (animation.spec.paused || animation.spec.iterations < 0.0f) continue;
418
+ double end = (double)animation.start_ms + (double)animation.spec.delay * 1000.0 +
419
+ (double)animation.spec.duration * 1000.0 * (double)animation.spec.iterations;
420
+ if (previous < end && clock_ms_ >= end) just_finished = true;
421
+ }
422
+ if (!running && !just_finished) return false;
423
+ updateDisplay();
424
+ return true;
425
+ }
426
+
427
+
428
+ } // namespace xtgui
@@ -15,6 +15,7 @@
15
15
 
16
16
  #include "canvas.h"
17
17
  #include "css.h"
18
+ #include "image_api.h"
18
19
 
19
20
  #define METHOD(name) xt_value name(xt_value self, xt_value env, int32_t argc, xt_value *argv)
20
21
 
@@ -87,6 +88,8 @@ METHOD(node_get_context) {
87
88
  xtgui::Node *node = resolve_node(self, &win);
88
89
  if (mode == "webgl" || mode == "experimental-webgl" || mode == "webgl2") {
89
90
  const bool webgl2 = mode == "webgl2";
91
+ const XtWebglApi *webgl = xt_webgl_api();
92
+ if (webgl == nullptr) return XT_NULL;
90
93
  if (node == nullptr) return XT_NULL;
91
94
  if (!node->isElement() || !node->isTag("canvas")) return XT_NULL;
92
95
  if (xt_gui_canvas_find(win, node) != nullptr) return XT_NULL; // already a 2D canvas
@@ -95,13 +98,14 @@ METHOD(node_get_context) {
95
98
  const bool cached_webgl2 = value_truthy(xt_object_get_cstr(cached, "__xt_gl_webgl2"));
96
99
  return cached_webgl2 == webgl2 ? cached : XT_NULL;
97
100
  }
98
- xtgui::WebGLContext *context = xt_gui_webgl_get(win, node, webgl2);
99
- return webgl_context_handle(self, context);
101
+ void *context = webgl->get(win, node, webgl2);
102
+ return webgl->handle(self, context);
100
103
  }
101
104
  if (mode != "2d") return XT_NULL;
102
105
  if (node == nullptr) return xt_canvas_pending_context(self, win);
103
106
  if (!node->isElement() || !node->isTag("canvas")) return XT_NULL;
104
- if (xt_gui_webgl_find(win, node) != nullptr) return XT_NULL; // already a WebGL canvas
107
+ const XtWebglApi *webgl_api = xt_webgl_api();
108
+ if (webgl_api != nullptr && webgl_api->find(win, node) != nullptr) return XT_NULL; // already WebGL
105
109
  xt_value cached = xt_object_get_cstr(self, "__xt_gui_ctx");
106
110
  if (XT_IS_OBJECT(cached)) return cached;
107
111
  xtgui::Canvas2D *canvas = xt_gui_canvas_get(win, node);
@@ -483,12 +487,15 @@ METHOD(canvas_draw_image) {
483
487
  XtGuiWindow *win = nullptr;
484
488
  xt_value source_value = xt_arg(argc, argv, 0);
485
489
  const xtgui::Image *image = nullptr;
490
+ const XtImageApi *images = xt_image_api();
486
491
  if (XT_IS_STRING(source_value)) {
487
492
  /* Engine extension: the source may be a path or a `data:` URL itself, so a
488
493
  * data-URL bitmap/SVG can be drawn without an `<img>` element. */
489
494
  const std::string source_url(xt_string_data(source_value));
490
- image = xtgui::xt_image_load(source_url, raster_target(destination_width),
491
- raster_target(destination_height));
495
+ if (images != nullptr) {
496
+ image = images->load(source_url, raster_target(destination_width),
497
+ raster_target(destination_height));
498
+ }
492
499
  } else {
493
500
  /* An `OffscreenCanvas` (or a transferred one) has no DOM node; its bitmap
494
501
  * is reachable through the process-wide canvas id. */
@@ -503,9 +510,9 @@ METHOD(canvas_draw_image) {
503
510
  xtgui::Node *source = resolve_node(source_value, &win);
504
511
  if (source != nullptr && source->isElement() && source->isTag("img")) {
505
512
  const std::string *src = source->attr("src");
506
- if (src != nullptr) {
507
- image = xtgui::xt_image_load(*src, raster_target(destination_width),
508
- raster_target(destination_height));
513
+ if (src != nullptr && images != nullptr) {
514
+ image = images->load(*src, raster_target(destination_width),
515
+ raster_target(destination_height));
509
516
  }
510
517
  } else if (source != nullptr) {
511
518
  xtgui::Canvas2D *other = xt_gui_canvas_find(win, source);
@@ -290,8 +290,14 @@ METHOD(node_width_get) {
290
290
  xtgui::Node *node = resolve_node(self, &win);
291
291
  if (node == nullptr || !node->isElement()) return xt_number(0);
292
292
  if (node->isTag("canvas")) {
293
- if (xtgui::WebGLContext *webgl = xt_gui_webgl_find(win, node)) {
294
- return xt_number(webgl->width());
293
+ if (const XtWebglApi *webgl = xt_webgl_api()) {
294
+ if (void *context = webgl->find(win, node)) {
295
+ float width = 0.0f;
296
+ float height = 0.0f;
297
+ uint64_t id = 0;
298
+ webgl->info(context, &width, &height, &id);
299
+ return xt_number(width);
300
+ }
295
301
  }
296
302
  xtgui::Canvas2D *canvas = xt_gui_canvas_find(win, node);
297
303
  if (canvas != nullptr) return xt_number(canvas->width());
@@ -310,16 +316,22 @@ METHOD(node_width_set) {
310
316
  if (node == nullptr || !node->isElement()) return XT_UNDEFINED;
311
317
  double value = canvas_number_arg(argc, argv, 0, 0.0);
312
318
  if (node->isTag("canvas")) {
313
- if (xtgui::WebGLContext *webgl = xt_gui_webgl_find(win, node)) {
314
- webgl->resize((int)value, webgl->height());
315
- /* A resize replaces the colour texture; the page has to re-upload the
316
- * image op even when the box size did not change. */
317
- mark_dirty(win);
318
- if (win != nullptr && win->document != nullptr) {
319
- win->document->setCanvasInfo(node, (float)webgl->width(), (float)webgl->height(),
320
- webgl->id());
319
+ if (const XtWebglApi *webgl = xt_webgl_api()) {
320
+ if (void *context = webgl->find(win, node)) {
321
+ float width = 0.0f;
322
+ float height = 0.0f;
323
+ uint64_t id = 0;
324
+ webgl->info(context, &width, &height, &id);
325
+ webgl->resize(context, (int)value, (int)height);
326
+ /* A resize replaces the colour texture; the page has to re-upload the
327
+ * image op even when the box size did not change. */
328
+ mark_dirty(win);
329
+ if (win->document != nullptr) {
330
+ webgl->info(context, &width, &height, &id);
331
+ win->document->setCanvasInfo(node, width, height, id);
332
+ }
333
+ return XT_UNDEFINED;
321
334
  }
322
- return XT_UNDEFINED;
323
335
  }
324
336
  xtgui::Canvas2D *canvas = xt_gui_canvas_find(win, node);
325
337
  if (canvas != nullptr) {
@@ -355,8 +367,14 @@ METHOD(node_height_get) {
355
367
  xtgui::Node *node = resolve_node(self, &win);
356
368
  if (node == nullptr || !node->isElement()) return xt_number(0);
357
369
  if (node->isTag("canvas")) {
358
- if (xtgui::WebGLContext *webgl = xt_gui_webgl_find(win, node)) {
359
- return xt_number(webgl->height());
370
+ if (const XtWebglApi *webgl = xt_webgl_api()) {
371
+ if (void *context = webgl->find(win, node)) {
372
+ float width = 0.0f;
373
+ float height = 0.0f;
374
+ uint64_t id = 0;
375
+ webgl->info(context, &width, &height, &id);
376
+ return xt_number(height);
377
+ }
360
378
  }
361
379
  xtgui::Canvas2D *canvas = xt_gui_canvas_find(win, node);
362
380
  if (canvas != nullptr) return xt_number(canvas->height());
@@ -374,14 +392,20 @@ METHOD(node_height_set) {
374
392
  if (node == nullptr || !node->isElement()) return XT_UNDEFINED;
375
393
  double value = canvas_number_arg(argc, argv, 0, 0.0);
376
394
  if (node->isTag("canvas")) {
377
- if (xtgui::WebGLContext *webgl = xt_gui_webgl_find(win, node)) {
378
- webgl->resize(webgl->width(), (int)value);
379
- mark_dirty(win);
380
- if (win != nullptr && win->document != nullptr) {
381
- win->document->setCanvasInfo(node, (float)webgl->width(), (float)webgl->height(),
382
- webgl->id());
395
+ if (const XtWebglApi *webgl = xt_webgl_api()) {
396
+ if (void *context = webgl->find(win, node)) {
397
+ float width = 0.0f;
398
+ float height = 0.0f;
399
+ uint64_t id = 0;
400
+ webgl->info(context, &width, &height, &id);
401
+ webgl->resize(context, (int)width, (int)value);
402
+ mark_dirty(win);
403
+ if (win->document != nullptr) {
404
+ webgl->info(context, &width, &height, &id);
405
+ win->document->setCanvasInfo(node, width, height, id);
406
+ }
407
+ return XT_UNDEFINED;
383
408
  }
384
- return XT_UNDEFINED;
385
409
  }
386
410
  xtgui::Canvas2D *canvas = xt_gui_canvas_find(win, node);
387
411
  if (canvas != nullptr) {
@@ -15,6 +15,7 @@
15
15
 
16
16
  #include "canvas.h"
17
17
  #include "image.h"
18
+ #include "image_api.h"
18
19
 
19
20
  namespace domapi {
20
21
 
@@ -68,7 +69,8 @@ bool source_image(XtGuiWindow *win, xt_value value, xtgui::Image *out) {
68
69
  if (node->isTag("img")) {
69
70
  const std::string *src = node->attr("src");
70
71
  if (src == nullptr) return false;
71
- const xtgui::Image *image = xtgui::xt_image_load(*src);
72
+ const XtImageApi *images = xt_image_api();
73
+ const xtgui::Image *image = images != nullptr ? images->load(*src, 0, 0) : nullptr;
72
74
  if (image == nullptr || image->width <= 0 || image->height <= 0) return false;
73
75
  *out = *image;
74
76
  return true;
@@ -137,6 +137,16 @@ METHOD(node_text_content_set) {
137
137
  xtgui::Node *node = resolve_node(self, &win);
138
138
  if (node == nullptr || !node->isElement()) return XT_UNDEFINED;
139
139
  std::string text = argc >= 1 ? to_string_value(xt_arg(argc, argv, 0)) : "";
140
+ /* Writing the value the node already shows is a no-op. Rebuilding the subtree
141
+ * would drop and re-allocate a text node, dirty the document and force a full
142
+ * restyle — and HUD-style code assigns the same string every frame (a clock
143
+ * that only changes once a second, a health label that does not move). */
144
+ if (node->children.empty()) {
145
+ if (text.empty()) return XT_UNDEFINED;
146
+ } else if (node->children.size() == 1 && node->children[0]->isText() &&
147
+ node->children[0]->text == text) {
148
+ return XT_UNDEFINED;
149
+ }
140
150
  std::vector<xtgui::Node *> existing;
141
151
  for (const std::unique_ptr<xtgui::Node> &child : node->children) existing.push_back(child.get());
142
152
  for (xtgui::Node *child : existing) win->document->removeChild(node, child);
@@ -200,7 +200,9 @@ METHOD(canvas_transfer_control_to_offscreen) {
200
200
  if (win == nullptr || node == nullptr || !node->isElement() || !node->isTag("canvas")) {
201
201
  return XT_NULL;
202
202
  }
203
- if (xt_gui_webgl_find(win, node) != nullptr) return XT_NULL;
203
+ if (const XtWebglApi *webgl = xt_webgl_api()) {
204
+ if (webgl->find(win, node) != nullptr) return XT_NULL;
205
+ }
204
206
  xtgui::Canvas2D *canvas = xt_gui_canvas_get(win, node);
205
207
  if (canvas == nullptr) return XT_NULL;
206
208
  xt_value handle = make_offscreen(canvas, win->object);
@@ -56,6 +56,11 @@ static void set_style_property(XtGuiWindow *win, xtgui::Node *node, const std::s
56
56
  bool found = false;
57
57
  for (xtgui::Declaration &decl : decls) {
58
58
  if (decl.property == name) {
59
+ /* Assigning the value that is already there must not dirty the document:
60
+ * a restyle costs O(nodes × rules) and HUD code rewrites the same width /
61
+ * transform / offset every frame. Rewriting the attribute is also a
62
+ * parse-and-rebuild of the whole inline style string. */
63
+ if (decl.value == value && decl.important == important) return;
59
64
  decl.value = value;
60
65
  decl.important = important;
61
66
  found = true;
@@ -102,6 +107,8 @@ static void remove_style_property(XtGuiWindow *win, xtgui::Node *node, const std
102
107
  for (const xtgui::Declaration &decl : decls) {
103
108
  if (decl.property != name) kept.push_back(decl);
104
109
  }
110
+ /* Removing a property that was never set is a no-op, exactly like the CSSOM. */
111
+ if (kept.size() == decls.size()) return;
105
112
  write_declarations(node, kept);
106
113
  mark_dirty(win);
107
114
  }
@@ -14,6 +14,10 @@
14
14
 
15
15
  #include "gui_engine.h"
16
16
 
17
+ namespace xtgui {
18
+ class WebGLContext;
19
+ }
20
+
17
21
  namespace domapi {
18
22
 
19
23
  /** The engine WebGL context behind a context handle, or null. */