xbintsc 0.5.37 → 0.5.41

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 (166) hide show
  1. package/dist/src/cli/help.d.ts +1 -1
  2. package/dist/src/codegen/generator/module.js +23 -7
  3. package/dist/src/codegen/generator/module.js.map +1 -1
  4. package/dist/src/codegen/generator/state.js +1 -0
  5. package/dist/src/codegen/generator/state.js.map +1 -1
  6. package/dist/src/codegen/generator/tables.js +1 -0
  7. package/dist/src/codegen/generator/tables.js.map +1 -1
  8. package/dist/src/driver/bundler/deferred.d.ts +63 -0
  9. package/dist/src/driver/bundler/deferred.js +279 -0
  10. package/dist/src/driver/bundler/deferred.js.map +1 -0
  11. package/dist/src/driver/bundler/merge.js +214 -2
  12. package/dist/src/driver/bundler/merge.js.map +1 -1
  13. package/dist/src/driver/runtime-objects.d.ts +1 -1
  14. package/dist/src/driver/runtime-objects.js +1 -0
  15. package/dist/src/driver/runtime-objects.js.map +1 -1
  16. package/dist/src/driver/version.d.ts +1 -1
  17. package/dist/src/driver/version.js +1 -1
  18. package/dist/src/extensions/gui/html.js +98 -15
  19. package/dist/src/extensions/gui/html.js.map +1 -1
  20. package/dist/src/extensions/gui/index.js +1 -0
  21. package/dist/src/extensions/gui/index.js.map +1 -1
  22. package/dist/src/extensions/gui/three.d.ts +4 -3
  23. package/dist/src/extensions/gui/three.js +4 -3
  24. package/dist/src/extensions/gui/three.js.map +1 -1
  25. package/dist/tests/codegen/closures.test.js +7 -0
  26. package/dist/tests/codegen/closures.test.js.map +1 -1
  27. package/dist/tests/e2e/compile-objects-classes.test.js +31 -0
  28. package/dist/tests/e2e/compile-objects-classes.test.js.map +1 -1
  29. package/dist/tests/e2e/gui-canvas-state.test.d.ts +9 -0
  30. package/dist/tests/e2e/gui-canvas-state.test.js +297 -0
  31. package/dist/tests/e2e/gui-canvas-state.test.js.map +1 -0
  32. package/dist/tests/e2e/gui-draw-perf.test.d.ts +22 -0
  33. package/dist/tests/e2e/gui-draw-perf.test.js +405 -0
  34. package/dist/tests/e2e/gui-draw-perf.test.js.map +1 -0
  35. package/dist/tests/e2e/gui-perf.test.d.ts +23 -0
  36. package/dist/tests/e2e/gui-perf.test.js +283 -0
  37. package/dist/tests/e2e/gui-perf.test.js.map +1 -0
  38. package/dist/tests/e2e/gui-script-import-order.test.d.ts +10 -0
  39. package/dist/tests/e2e/gui-script-import-order.test.js +169 -0
  40. package/dist/tests/e2e/gui-script-import-order.test.js.map +1 -0
  41. package/dist/tests/e2e/gui-webgl.test.js +4 -4
  42. package/dist/tests/e2e/gui-webgl.test.js.map +1 -1
  43. package/dist/tests/e2e/gui-webgl2-shadow-sampler.test.d.ts +8 -0
  44. package/dist/tests/e2e/gui-webgl2-shadow-sampler.test.js +336 -0
  45. package/dist/tests/e2e/gui-webgl2-shadow-sampler.test.js.map +1 -0
  46. package/dist/tests/e2e/gui-webgl2-shadow.test.d.ts +10 -0
  47. package/dist/tests/e2e/gui-webgl2-shadow.test.js +108 -0
  48. package/dist/tests/e2e/gui-webgl2-shadow.test.js.map +1 -0
  49. package/dist/tests/e2e/url.test.d.ts +6 -0
  50. package/dist/tests/e2e/url.test.js +114 -0
  51. package/dist/tests/e2e/url.test.js.map +1 -0
  52. package/dist/tests/extensions/gui.test.js +45 -10
  53. package/dist/tests/extensions/gui.test.js.map +1 -1
  54. package/doc/gui/api.md +28 -2
  55. package/doc/gui/canvas.md +110 -27
  56. package/doc/gui/known-issues.md +32 -10
  57. package/doc/gui/rendering.md +77 -0
  58. package/doc/gui/runtime.md +18 -3
  59. package/doc/gui/webgl.md +58 -29
  60. package/doc/gui-scripts.md +19 -19
  61. package/doc/gui.md +43 -9
  62. package/doc/implemented/fetch.md +5 -0
  63. package/doc/implemented/modules.md +13 -0
  64. package/doc/implemented/stdlib.md +17 -0
  65. package/doc/unimplemented/deviations.md +1 -0
  66. package/doc/zh-CN/gui/api.md +25 -2
  67. package/doc/zh-CN/gui/canvas.md +95 -26
  68. package/doc/zh-CN/gui/known-issues.md +21 -4
  69. package/doc/zh-CN/gui/rendering.md +59 -0
  70. package/doc/zh-CN/gui/runtime.md +13 -3
  71. package/doc/zh-CN/gui/webgl.md +30 -8
  72. package/doc/zh-CN/gui-scripts.md +14 -13
  73. package/doc/zh-CN/gui.md +32 -8
  74. package/doc/zh-CN/implemented/fetch.md +4 -0
  75. package/doc/zh-CN/implemented/modules.md +10 -0
  76. package/doc/zh-CN/implemented/stdlib.md +15 -0
  77. package/doc/zh-CN/unimplemented/deviations.md +1 -0
  78. package/package.json +7 -7
  79. package/runtime/ext_gui/browser.cpp +3 -1
  80. package/runtime/ext_gui/canvas.cpp +135 -89
  81. package/runtime/ext_gui/canvas.h +116 -0
  82. package/runtime/ext_gui/canvas_clip.cpp +143 -0
  83. package/runtime/ext_gui/canvas_composite.cpp +241 -0
  84. package/runtime/ext_gui/canvas_coverage.cpp +241 -0
  85. package/runtime/ext_gui/canvas_gradient.cpp +41 -4
  86. package/runtime/ext_gui/canvas_image_data.cpp +4 -0
  87. package/runtime/ext_gui/canvas_internal.h +34 -9
  88. package/runtime/ext_gui/canvas_raster.cpp +207 -178
  89. package/runtime/ext_gui/canvas_shadow.cpp +161 -0
  90. package/runtime/ext_gui/canvas_stroke.cpp +37 -9
  91. package/runtime/ext_gui/canvas_style.cpp +59 -0
  92. package/runtime/ext_gui/canvas_text.cpp +140 -0
  93. package/runtime/ext_gui/css.h +7 -0
  94. package/runtime/ext_gui/css_sheet.cpp +22 -0
  95. package/runtime/ext_gui/document.cpp +25 -1
  96. package/runtime/ext_gui/document.h +8 -0
  97. package/runtime/ext_gui/dom_api_canvas.cpp +16 -1
  98. package/runtime/ext_gui/dom_api_canvas_image.cpp +9 -0
  99. package/runtime/ext_gui/dom_api_canvas_state.cpp +217 -0
  100. package/runtime/ext_gui/dom_api_internal.h +36 -37
  101. package/runtime/ext_gui/dom_api_offscreen.cpp +213 -0
  102. package/runtime/ext_gui/dom_api_proto.cpp +18 -0
  103. package/runtime/ext_gui/dom_api_webgl.cpp +29 -9
  104. package/runtime/ext_gui/dom_api_webgl2.cpp +33 -1
  105. package/runtime/ext_gui/dom_api_webgl_internal.h +58 -0
  106. package/runtime/ext_gui/dom_api_webgl_source.cpp +18 -0
  107. package/runtime/ext_gui/gradient_values.cpp +12 -1
  108. package/runtime/ext_gui/gui.cpp +5 -0
  109. package/runtime/ext_gui/gui_canvas.cpp +4 -0
  110. package/runtime/ext_gui/gui_engine.h +32 -0
  111. package/runtime/ext_gui/gui_events.cpp +182 -58
  112. package/runtime/ext_gui/gui_media.cpp +3 -0
  113. package/runtime/ext_gui/image.cpp +69 -0
  114. package/runtime/ext_gui/image_bitmap_browser.cpp +11 -0
  115. package/runtime/ext_gui/layout.cpp +4 -0
  116. package/runtime/ext_gui/paint.cpp +19 -0
  117. package/runtime/ext_gui/paint_background.cpp +18 -1
  118. package/runtime/ext_gui/perf.cpp +171 -0
  119. package/runtime/ext_gui/perf.h +127 -0
  120. package/runtime/ext_gui/renderer.cpp +109 -41
  121. package/runtime/ext_gui/renderer.h +33 -9
  122. package/runtime/ext_gui/renderer_image.cpp +135 -65
  123. package/runtime/ext_gui/renderer_internal.h +15 -3
  124. package/runtime/ext_gui/renderer_text.cpp +24 -12
  125. package/runtime/ext_gui/script.cpp +31 -0
  126. package/runtime/ext_gui/style.cpp +101 -30
  127. package/runtime/ext_gui/style.h +6 -3
  128. package/runtime/ext_gui/style_print.cpp +7 -1
  129. package/runtime/ext_gui/text.cpp +23 -4
  130. package/runtime/ext_gui/text.h +29 -0
  131. package/runtime/ext_gui/text_cache.cpp +118 -0
  132. package/runtime/ext_gui/text_glyph_cache.cpp +100 -0
  133. package/runtime/ext_gui/webgl.h +48 -146
  134. package/runtime/ext_gui/webgl_blit.cpp +12 -1
  135. package/runtime/ext_gui/webgl_blit_depth.cpp +270 -0
  136. package/runtime/ext_gui/webgl_buffer.cpp +59 -1
  137. package/runtime/ext_gui/webgl_buffer_objects.cpp +106 -9
  138. package/runtime/ext_gui/webgl_clear.cpp +3 -0
  139. package/runtime/ext_gui/webgl_context.cpp +29 -3
  140. package/runtime/ext_gui/webgl_draw.cpp +75 -270
  141. package/runtime/ext_gui/webgl_draw_buffers.cpp +1 -0
  142. package/runtime/ext_gui/webgl_draw_setup.cpp +321 -0
  143. package/runtime/ext_gui/webgl_draw_state.h +130 -0
  144. package/runtime/ext_gui/webgl_glsl.h +3 -0
  145. package/runtime/ext_gui/webgl_glsl_scan.cpp +3 -8
  146. package/runtime/ext_gui/webgl_glsl_unroll.cpp +283 -0
  147. package/runtime/ext_gui/webgl_msl.cpp +4 -0
  148. package/runtime/ext_gui/webgl_objects.h +148 -0
  149. package/runtime/ext_gui/webgl_pipeline.cpp +8 -8
  150. package/runtime/ext_gui/webgl_program_internal.h +35 -0
  151. package/runtime/ext_gui/webgl_shader.cpp +34 -36
  152. package/runtime/ext_gui/webgl_webgl2.cpp +36 -0
  153. package/runtime/ext_gui/window.cpp +56 -1
  154. package/runtime/rt.h +3 -0
  155. package/runtime/xt_fetch/async.inc +60 -0
  156. package/runtime/xt_url_object.c +463 -0
  157. package/src/codegen/generator/module.ts +22 -7
  158. package/src/codegen/generator/state.ts +1 -0
  159. package/src/codegen/generator/tables.ts +1 -0
  160. package/src/driver/bundler/deferred.ts +351 -0
  161. package/src/driver/bundler/merge.ts +219 -2
  162. package/src/driver/runtime-objects.ts +1 -0
  163. package/src/driver/version.ts +1 -1
  164. package/src/extensions/gui/html.ts +100 -17
  165. package/src/extensions/gui/index.ts +1 -0
  166. package/src/extensions/gui/three.ts +4 -3
@@ -9,6 +9,7 @@
9
9
  #include "style_internal.h"
10
10
 
11
11
  #include "css_internal.h"
12
+ #include "perf.h"
12
13
 
13
14
  #include <algorithm>
14
15
  #include <cctype>
@@ -45,13 +46,17 @@ struct Candidate {
45
46
  int important = 0;
46
47
  int specificity = 0;
47
48
  int order = 0;
49
+ /** Push order, so two candidates with the same `order` (the inline
50
+ * declarations all use `INT_MAX`) keep their declaration order. */
51
+ int sequence = 0;
48
52
  const Declaration *declaration = nullptr;
49
53
  };
50
54
 
51
55
  bool candidateBefore(const Candidate &a, const Candidate &b) {
52
56
  if (a.important != b.important) return a.important < b.important;
53
57
  if (a.specificity != b.specificity) return a.specificity < b.specificity;
54
- return a.order < b.order;
58
+ if (a.order != b.order) return a.order < b.order;
59
+ return a.sequence < b.sequence;
55
60
  }
56
61
 
57
62
 
@@ -156,7 +161,9 @@ void inheritFrom(XtStyle *style, const XtStyle *parent) {
156
161
  style->color = parent->color;
157
162
  style->visible = parent->visible;
158
163
  style->pointer_events = parent->pointer_events;
159
- style->variables = parent->variables;
164
+ /* Most documents define no custom properties; copying the (empty) map for
165
+ * every element is pure overhead. */
166
+ if (!parent->variables.empty()) style->variables = parent->variables;
160
167
  style->font_size = parent->font_size;
161
168
  style->font_weight = parent->font_weight;
162
169
  style->font_family = parent->font_family;
@@ -167,14 +174,12 @@ void inheritFrom(XtStyle *style, const XtStyle *parent) {
167
174
  style->text_shadow = parent->text_shadow;
168
175
  }
169
176
 
170
- bool ruleApplies(const Rule &rule, const ApplyContext &context) {
171
- return rule.media.empty() ||
172
- xt_css_media_matches(rule.media, context.viewportWidth, context.viewportHeight);
173
- }
174
-
175
177
  void applyCandidates(XtStyle *style, std::vector<Candidate> *candidates,
176
178
  const ApplyContext &nodeContext) {
177
- std::stable_sort(candidates->begin(), candidates->end(), candidateBefore);
179
+ /* `candidateBefore` is a total order now (the sequence breaks ties), so an
180
+ * unstable `std::sort` is equivalent to the old `stable_sort` without its
181
+ * temporary buffer. */
182
+ std::sort(candidates->begin(), candidates->end(), candidateBefore);
178
183
  /* Custom properties participate in the cascade first; their final values are
179
184
  * then available to every normal declaration on this element. */
180
185
  for (const Candidate &candidate : *candidates) {
@@ -332,11 +337,35 @@ void resetPseudoPointers(const Node *node) {
332
337
  for (const std::unique_ptr<Node> &child : node->children) resetPseudoPointers(child.get());
333
338
  }
334
339
 
340
+ /** Per-computation scratch buffers. The cascade runs depth-first, so one
341
+ * candidate list (and one inline-declaration list) can be reused for every
342
+ * node — the old code allocated both per element. */
343
+ struct ComputeScratch {
344
+ std::vector<Candidate> candidates;
345
+ std::vector<Declaration> inline_declarations;
346
+ int sequence = 0;
347
+ /** Set when any computed style declares a `transition` (including an inline
348
+ * `style="transition: …"`), so the document knows whether it needs the
349
+ * transition snapshot path at all. */
350
+ bool has_transitions = false;
351
+ };
352
+
353
+ void pushCandidates(std::vector<Candidate> *candidates, ComputeScratch *scratch,
354
+ const Declaration &declaration, int important, int specificity, int order) {
355
+ Candidate candidate;
356
+ candidate.important = important;
357
+ candidate.specificity = specificity;
358
+ candidate.order = order;
359
+ candidate.sequence = scratch->sequence++;
360
+ candidate.declaration = &declaration;
361
+ candidates->push_back(candidate);
362
+ }
363
+
335
364
  /** Compute one `::before`/`::after` style and attach its generated content. */
336
365
  void computePseudo(const Node *origin, PseudoElement side, const XtStyle &originStyle,
337
- const StyleSheet &sheet, std::unordered_map<const Node *, XtStyle> &out,
338
- const ApplyContext &context, const MatchState &state,
339
- PseudoNodeSet *pseudos) {
366
+ const StyleSheet &sheet, const std::vector<char> &applies,
367
+ std::unordered_map<const Node *, XtStyle> &out, const ApplyContext &context,
368
+ const MatchState &state, PseudoNodeSet *pseudos, ComputeScratch *scratch) {
340
369
  if (pseudos == nullptr || originStyle.display == Display::None) return;
341
370
  Node *pseudo = pseudos->create(const_cast<Node *>(origin), side);
342
371
  if (side == PseudoElement::Before) const_cast<Node *>(origin)->pseudo_before = pseudo;
@@ -350,9 +379,11 @@ void computePseudo(const Node *origin, PseudoElement side, const XtStyle &origin
350
379
  nodeContext.parentFontSize = originStyle.font_size;
351
380
 
352
381
  std::string pseudoName = side == PseudoElement::Before ? "before" : "after";
353
- std::vector<Candidate> candidates;
354
- for (const Rule &rule : sheet.rules) {
355
- if (!ruleApplies(rule, nodeContext)) continue;
382
+ std::vector<Candidate> &candidates = scratch->candidates;
383
+ candidates.clear();
384
+ for (size_t ruleIndex = 0; ruleIndex < sheet.rules.size(); ruleIndex++) {
385
+ if (applies[ruleIndex] == 0) continue;
386
+ const Rule &rule = sheet.rules[ruleIndex];
356
387
  for (const ComplexSelector &selector : rule.selectors) {
357
388
  if (selector.compounds.empty() || selector.compounds.back().pseudo_element != pseudoName)
358
389
  continue;
@@ -360,15 +391,17 @@ void computePseudo(const Node *origin, PseudoElement side, const XtStyle &origin
360
391
  * pseudo-element suffix removed: `.card.info::before` is `.card.info`. */
361
392
  ComplexSelector copy = selector;
362
393
  copy.compounds.back().pseudo_element.clear();
394
+ g_perf_frame.rulesEvaluated += 1;
363
395
  if (!xt_css_match(origin, copy, state)) continue;
364
396
  for (const Declaration &declaration : rule.declarations) {
365
- candidates.push_back({declaration.important ? 1 : 0, selector.specificity, rule.order,
366
- &declaration});
397
+ pushCandidates(&candidates, scratch, declaration, declaration.important ? 1 : 0,
398
+ selector.specificity, rule.order);
367
399
  }
368
400
  }
369
401
  }
370
402
  applyCandidates(&style, &candidates, nodeContext);
371
403
  finalizeStyle(&style, nodeContext);
404
+ if (!style.transitions.empty()) scratch->has_transitions = true;
372
405
 
373
406
  bool generated = false;
374
407
  std::string text;
@@ -380,8 +413,9 @@ void computePseudo(const Node *origin, PseudoElement side, const XtStyle &origin
380
413
  }
381
414
 
382
415
  void computeNode(const Node *node, const XtStyle *parent, const StyleSheet &sheet,
383
- std::unordered_map<const Node *, XtStyle> &out, const ApplyContext &context,
384
- const MatchState &state, PseudoNodeSet *pseudos) {
416
+ const std::vector<char> &applies, std::unordered_map<const Node *, XtStyle> &out,
417
+ const ApplyContext &context, const MatchState &state, PseudoNodeSet *pseudos,
418
+ ComputeScratch *scratch) {
385
419
  const XtStyle *childParent = parent;
386
420
  if (node->isElement() && !node->is_pseudo) {
387
421
  XtStyle style = xt_style_initial();
@@ -395,35 +429,48 @@ void computeNode(const Node *node, const XtStyle *parent, const StyleSheet &shee
395
429
  ApplyContext nodeContext = context;
396
430
  nodeContext.parentFontSize = (parent != nullptr) ? parent->font_size : context.rootFontSize;
397
431
 
398
- std::vector<Candidate> candidates;
399
- for (const Rule &rule : sheet.rules) {
400
- if (!ruleApplies(rule, nodeContext)) continue;
432
+ std::vector<Candidate> &candidates = scratch->candidates;
433
+ candidates.clear();
434
+ for (size_t ruleIndex = 0; ruleIndex < sheet.rules.size(); ruleIndex++) {
435
+ if (applies[ruleIndex] == 0) continue;
436
+ const Rule &rule = sheet.rules[ruleIndex];
401
437
  for (const ComplexSelector &selector : rule.selectors) {
438
+ g_perf_frame.rulesEvaluated += 1;
402
439
  if (!xt_css_match(node, selector, state)) continue;
403
440
  for (const Declaration &declaration : rule.declarations) {
404
- candidates.push_back({declaration.important ? 1 : 0, selector.specificity, rule.order, &declaration});
441
+ pushCandidates(&candidates, scratch, declaration, declaration.important ? 1 : 0,
442
+ selector.specificity, rule.order);
405
443
  }
406
444
  }
407
445
  }
408
446
  const std::string *inlineStyle = node->attr("style");
409
- std::vector<Declaration> inlineDeclarations;
410
447
  if (inlineStyle != nullptr) {
448
+ std::vector<Declaration> &inlineDeclarations = scratch->inline_declarations;
411
449
  inlineDeclarations = xt_css_parse_declarations(*inlineStyle);
412
450
  for (const Declaration &declaration : inlineDeclarations) {
413
- candidates.push_back({declaration.important ? 1 : 0, 1 << 28, INT_MAX, &declaration});
451
+ pushCandidates(&candidates, scratch, declaration, declaration.important ? 1 : 0, 1 << 28,
452
+ INT_MAX);
414
453
  }
415
454
  }
416
455
  applyCandidates(&style, &candidates, nodeContext);
417
456
  finalizeStyle(&style, nodeContext);
457
+ if (!style.transitions.empty()) scratch->has_transitions = true;
418
458
 
459
+ /* `emplace` keeps the address of an existing entry stable, which is what
460
+ * `box->style` pointers rely on. */
419
461
  out[node] = style;
420
462
  childParent = &out[node];
421
463
  const XtStyle &computed = out[node];
422
- computePseudo(node, PseudoElement::Before, computed, sheet, out, context, state, pseudos);
423
- computePseudo(node, PseudoElement::After, computed, sheet, out, context, state, pseudos);
464
+ if (pseudos != nullptr && sheet.has_pseudo_elements) {
465
+ computePseudo(node, PseudoElement::Before, computed, sheet, applies, out, context, state,
466
+ pseudos, scratch);
467
+ computePseudo(node, PseudoElement::After, computed, sheet, applies, out, context, state,
468
+ pseudos, scratch);
469
+ }
470
+ g_perf_frame.restyleNodes += 1;
424
471
  }
425
472
  for (const std::unique_ptr<Node> &child : node->children) {
426
- computeNode(child.get(), childParent, sheet, out, context, state, pseudos);
473
+ computeNode(child.get(), childParent, sheet, applies, out, context, state, pseudos, scratch);
427
474
  }
428
475
  }
429
476
 
@@ -431,16 +478,40 @@ void computeNode(const Node *node, const XtStyle *parent, const StyleSheet &shee
431
478
 
432
479
  void xt_style_compute(const Node *root, const StyleSheet &sheet,
433
480
  std::unordered_map<const Node *, XtStyle> &out, float viewportWidth,
434
- float viewportHeight, const MatchState &state, PseudoNodeSet *pseudos) {
481
+ float viewportHeight, const MatchState &state, PseudoNodeSet *pseudos,
482
+ bool *hasTransitions) {
435
483
  out.clear();
436
- if (pseudos != nullptr && root != nullptr) {
484
+ bool wantsPseudo = pseudos != nullptr && sheet.has_pseudo_elements;
485
+ if (wantsPseudo && root != nullptr) {
437
486
  resetPseudoPointers(root);
438
487
  pseudos->clear();
439
488
  }
440
489
  ApplyContext context;
441
490
  context.viewportWidth = viewportWidth;
442
491
  context.viewportHeight = viewportHeight;
443
- computeNode(root, nullptr, sheet, out, context, state, pseudos);
492
+ /* An `@media` prelude is evaluated once per rule (not once per rule *and*
493
+ * node): the query never depends on the element, so this was the engine's
494
+ * largest per-element cost on a large document. */
495
+ std::vector<char> applies(sheet.rules.size(), 0);
496
+ for (size_t i = 0; i < sheet.rules.size(); i++) {
497
+ const std::string &media = sheet.rules[i].media;
498
+ if (media.empty()) {
499
+ applies[i] = 1;
500
+ continue;
501
+ }
502
+ g_perf_frame.mediaEvaluated += 1;
503
+ applies[i] =
504
+ xt_css_media_matches(media, viewportWidth, viewportHeight) ? (char)1 : (char)0;
505
+ }
506
+ ComputeScratch scratch;
507
+ scratch.candidates.reserve(32);
508
+ if (root != nullptr) {
509
+ computeNode(root, nullptr, sheet, applies, out, context, state, wantsPseudo ? pseudos : nullptr,
510
+ &scratch);
511
+ }
512
+ if (hasTransitions != nullptr) {
513
+ *hasTransitions = *hasTransitions || scratch.has_transitions;
514
+ }
444
515
  }
445
516
 
446
517
  } // namespace xtgui
@@ -60,7 +60,7 @@ struct BackgroundGradientStop {
60
60
  Color color;
61
61
  };
62
62
 
63
- enum class BackgroundKind { Linear, Radial };
63
+ enum class BackgroundKind { Linear, Radial, Image };
64
64
 
65
65
  /** How a radial layer's radii are chosen when it has no explicit lengths. */
66
66
  enum class RadialSize { ClosestSide, FarthestSide, ClosestCorner, FarthestCorner, Explicit };
@@ -80,6 +80,8 @@ struct BackgroundLayer {
80
80
  Length center_y{50.0f, Unit::Percent};
81
81
  Length radius_x{0.0f, Unit::Auto}; // Explicit radii only
82
82
  Length radius_y{0.0f, Unit::Auto};
83
+ /** `BackgroundKind::Image`: the `url(...)` source (a path or data URL). */
84
+ std::string image_src;
83
85
  std::vector<BackgroundGradientStop> stops;
84
86
  };
85
87
 
@@ -262,11 +264,12 @@ class PseudoNodeSet {
262
264
  * `state` supplies `:hover`/`:focus` for dynamic selector matching. When
263
265
  * `pseudos` is non-null, `::before`/`::after` styles are computed too and each
264
266
  * element's transient `pseudo_before`/`pseudo_after` pointers are published for
265
- * layout. */
267
+ * layout. When `hasTransitions` is non-null it is OR-ed with whether any
268
+ * computed style declares a `transition` (inline `style="…"` included). */
266
269
  void xt_style_compute(const Node *root, const StyleSheet &sheet,
267
270
  std::unordered_map<const Node *, XtStyle> &out, float viewportWidth,
268
271
  float viewportHeight, const MatchState &state = MatchState(),
269
- PseudoNodeSet *pseudos = nullptr);
272
+ PseudoNodeSet *pseudos = nullptr, bool *hasTransitions = nullptr);
270
273
 
271
274
  /** Format a computed property for diagnostics/tests (`computedStyle(sel, prop)`). */
272
275
  std::string xt_style_property_to_string(const XtStyle &style, const std::string &property);
@@ -247,7 +247,13 @@ std::string xt_style_property_to_string(const XtStyle &style, const std::string
247
247
  std::string out;
248
248
  for (const BackgroundLayer &layer : style.background_layers) {
249
249
  if (!out.empty()) out += ", ";
250
- out += layer.kind == BackgroundKind::Linear ? "linear-gradient()" : "radial-gradient()";
250
+ if (layer.kind == BackgroundKind::Linear) {
251
+ out += "linear-gradient()";
252
+ } else if (layer.kind == BackgroundKind::Radial) {
253
+ out += "radial-gradient()";
254
+ } else {
255
+ out += "url(" + layer.image_src + ")";
256
+ }
251
257
  }
252
258
  return out;
253
259
  }
@@ -17,6 +17,8 @@
17
17
 
18
18
  #include "text.h"
19
19
 
20
+ #include "perf.h"
21
+
20
22
  #include <ft2build.h>
21
23
  #include FT_FREETYPE_H
22
24
 
@@ -25,6 +27,7 @@
25
27
 
26
28
  #include <cmath>
27
29
  #include <cstddef>
30
+ #include <cstdint>
28
31
  #include <cstdio>
29
32
  #include <cstdlib>
30
33
  #include <cstring>
@@ -125,6 +128,10 @@ struct Engine {
125
128
  std::unordered_map<std::string, std::unique_ptr<Font>> cache; // key -> face (may be null)
126
129
  std::unordered_map<std::string, std::vector<Font *>> fallbacks; // class+size -> faces
127
130
  std::unordered_map<std::string, hb_face_t *> hb_faces; // path -> face
131
+ /** Primary face by (family class, pixel size): `xt_text_resolve` and
132
+ * `xt_text_metrics` are called per text box per layout, and building the
133
+ * 512-byte path key each time showed up in the profile. */
134
+ std::unordered_map<std::string, Font *> primary;
128
135
  };
129
136
 
130
137
  Engine &engine() {
@@ -416,9 +423,15 @@ Font *xt_text_resolve(const FontSpec &spec) {
416
423
  if (!e.ft_ok) return nullptr;
417
424
 
418
425
  bool mono = isMonospaceFamily(spec.family);
426
+ char key[48];
427
+ std::snprintf(key, sizeof(key), "%c|%.4f", mono ? 'm' : 's', (double)spec.pixel_size);
428
+ auto cached = e.primary.find(key);
429
+ if (cached != e.primary.end()) return cached->second;
430
+
419
431
  const std::string &path = mono ? e.mono_path : e.sans_path;
420
- if (path.empty()) return nullptr;
421
- return resolveFacePath(path, spec.pixel_size, mono);
432
+ Font *font = path.empty() ? nullptr : resolveFacePath(path, spec.pixel_size, mono);
433
+ e.primary.emplace(key, font);
434
+ return font;
422
435
  }
423
436
 
424
437
  Font *xt_text_scale(Font *font, float pixel_size) {
@@ -429,12 +442,14 @@ Font *xt_text_scale(Font *font, float pixel_size) {
429
442
  float xt_text_measure_width(const std::string &utf8, const FontSpec &spec) {
430
443
  Font *font = xt_text_resolve(spec);
431
444
  if (font == nullptr) return approximateWidth(utf8, spec.pixel_size, spec.family);
432
- std::vector<ShapedGlyph> glyphs;
433
- return xt_text_shape_run(utf8, font, glyphs);
445
+ if (utf8.empty()) return 0.0f;
446
+ const ShapedRun *run = xt_text_shape_cached(utf8, font);
447
+ return run != nullptr ? run->width : 0.0f;
434
448
  }
435
449
 
436
450
  float xt_text_shape_run(const std::string &utf8, Font *font, std::vector<ShapedGlyph> &out) {
437
451
  if (font == nullptr) return 0;
452
+ g_perf_frame.textShapeCalls += 1;
438
453
  float total = 0;
439
454
  size_t index = 0;
440
455
  size_t segment_start = 0;
@@ -502,6 +517,10 @@ bool xt_text_ready() {
502
517
 
503
518
  void xt_text_shutdown() {
504
519
  Engine &e = engine();
520
+ /* Cached shaped runs point at faces, so drop them before the faces go. */
521
+ xt_text_cache_clear();
522
+ xt_text_glyph_cache_clear();
523
+ e.primary.clear();
505
524
  e.fallbacks.clear();
506
525
  e.cache.clear();
507
526
  for (auto &entry : e.hb_faces) {
@@ -40,6 +40,12 @@ struct ShapedGlyph {
40
40
  Font *font = nullptr;
41
41
  };
42
42
 
43
+ /** One shaped run: its glyphs and total advance. */
44
+ struct ShapedRun {
45
+ float width = 0;
46
+ std::vector<ShapedGlyph> glyphs;
47
+ };
48
+
43
49
  /** An 8-bit grayscale glyph bitmap, as produced by FreeType. */
44
50
  struct GlyphImage {
45
51
  int width = 0;
@@ -59,6 +65,19 @@ Font *xt_text_resolve(const FontSpec &spec);
59
65
  * glyph's `font` says which face it belongs to. */
60
66
  float xt_text_shape_run(const std::string &utf8, Font *font, std::vector<ShapedGlyph> &out);
61
67
 
68
+ /** Shape `utf8` through the process-wide bounded cache (`text_cache.cpp`).
69
+ * Returns a stable pointer for the lifetime of the cache entry (valid until the
70
+ * next cache generation is dropped); callers must not hold it across frames.
71
+ * Layout and paint both go through this, so a word is shaped once instead of
72
+ * once per layout pass and again per repaint. `nullptr` for a null face. */
73
+ const ShapedRun *xt_text_shape_cached(const std::string &utf8, Font *font);
74
+
75
+ /** Current cache occupancy (bytes of keys + glyphs, number of entries). */
76
+ uint64_t xt_text_cache_bytes();
77
+ uint64_t xt_text_cache_entries();
78
+ /** Drop every entry (called by `xt_text_shutdown`, and by tests). */
79
+ void xt_text_cache_clear();
80
+
62
81
  /** The same face as `font` at a different pixel size (used to rasterise glyphs
63
82
  * at the display's scale factor), or null when it cannot be loaded. */
64
83
  Font *xt_text_scale(Font *font, float pixel_size);
@@ -67,6 +86,16 @@ Font *xt_text_scale(Font *font, float pixel_size);
67
86
  * the glyph has no outline (e.g. a space) or cannot be rendered. */
68
87
  bool xt_text_rasterize(Font *font, uint32_t glyph, GlyphImage &out);
69
88
 
89
+ /** `xt_text_rasterize` through a bounded, two-generation cache. A canvas that
90
+ * redraws the same text every frame otherwise runs FreeType for every glyph of
91
+ * every `fillText`. The pointer stays valid for the rest of the frame (until a
92
+ * later call fills a generation and drops the spare one); callers must not hold
93
+ * it across frames. Returns null for a glyph without a bitmap. */
94
+ const GlyphImage *xt_text_glyph_cached(Font *font, uint32_t glyph);
95
+
96
+ /** Drop every cached glyph bitmap (called by `xt_text_shutdown`). */
97
+ void xt_text_glyph_cache_clear(void);
98
+
70
99
  /** Shaped horizontal advance of `utf8` in pixels. Falls back to a deterministic
71
100
  * approximation when no font file can be loaded. */
72
101
  float xt_text_measure_width(const std::string &utf8, const FontSpec &spec);
@@ -0,0 +1,118 @@
1
+ /*
2
+ * xbintsc GUI engine — bounded shaping cache.
3
+ *
4
+ * Layout measures every word of the document on every layout pass, and the
5
+ * renderer shapes every text run again to place its glyphs. Without a cache a
6
+ * document with a few thousand words runs thousands of HarfBuzz shapes per
7
+ * frame, which is what made a large page unusable. This module memoises
8
+ * `(face, text) -> glyphs + advance` behind a two-generation cache: when the
9
+ * budget is exceeded the *spare* generation is dropped and becomes the current
10
+ * one, so no entry-by-entry LRU bookkeeping is needed on the hot path (a
11
+ * document that cycles through many strings keeps at most two budgets).
12
+ *
13
+ * The key is the resolved face pointer plus the text: fallback faces, sizes and
14
+ * `XT_GUI_FONT` overrides each resolve to their own face, and
15
+ * `xt_text_shutdown()` clears the cache before the faces are freed.
16
+ */
17
+
18
+ #include "text.h"
19
+
20
+ #include "perf.h"
21
+
22
+ #include <string>
23
+ #include <unordered_map>
24
+ #include <utility>
25
+ #include <vector>
26
+
27
+ namespace xtgui {
28
+ namespace {
29
+
30
+ /** Glyph + key budget per generation. A shaped glyph is 24 bytes, so 2 MiB is
31
+ * roughly 80k glyphs (more than any single document draws). */
32
+ constexpr uint64_t kMaxBytes = 2ull * 1024 * 1024;
33
+ constexpr uint64_t kMaxEntries = 8192;
34
+
35
+ struct Key {
36
+ Font *font = nullptr;
37
+ std::string text;
38
+
39
+ bool operator==(const Key &other) const {
40
+ return font == other.font && text == other.text;
41
+ }
42
+ };
43
+
44
+ struct KeyHash {
45
+ size_t operator()(const Key &key) const {
46
+ size_t hash = std::hash<const void *>()(key.font);
47
+ // FNV-ish mix of the text, then combine — cheap and stable.
48
+ size_t textHash = std::hash<std::string>()(key.text);
49
+ hash ^= textHash + 0x9e3779b97f4a7c15ull + (hash << 6) + (hash >> 2);
50
+ return hash;
51
+ }
52
+ };
53
+
54
+ struct Generation {
55
+ std::unordered_map<Key, ShapedRun, KeyHash> runs;
56
+ uint64_t bytes = 0;
57
+ };
58
+
59
+ Generation g_generations[2];
60
+ int g_current = 0;
61
+
62
+ uint64_t estimateBytes(const Key &key, const ShapedRun &run) {
63
+ /* Key/Run objects plus the map node's own overhead (a rough, conservative
64
+ * constant: the budget is a guard, not an allocator). */
65
+ return (uint64_t)sizeof(Key) + (uint64_t)key.text.size() + (uint64_t)sizeof(ShapedRun) +
66
+ (uint64_t)run.glyphs.size() * sizeof(ShapedGlyph) + 64;
67
+ }
68
+
69
+ /** Rotate to the spare generation when the current one is full. */
70
+ void respectBudget() {
71
+ Generation &current = g_generations[g_current];
72
+ if (current.runs.size() <= kMaxEntries && current.bytes <= kMaxBytes) return;
73
+ int spare = 1 - g_current;
74
+ g_generations[spare].runs.clear();
75
+ g_generations[spare].bytes = 0;
76
+ g_current = spare;
77
+ }
78
+
79
+ } // namespace
80
+
81
+ const ShapedRun *xt_text_shape_cached(const std::string &utf8, Font *font) {
82
+ if (font == nullptr) return nullptr;
83
+ if (utf8.empty()) return nullptr;
84
+ Key key;
85
+ key.font = font;
86
+ key.text = utf8;
87
+ Generation &generation = g_generations[g_current];
88
+ auto found = generation.runs.find(key);
89
+ if (found != generation.runs.end()) {
90
+ g_perf_frame.textShapeHits += 1;
91
+ return &found->second;
92
+ }
93
+
94
+ auto inserted = generation.runs.emplace(std::move(key), ShapedRun());
95
+ ShapedRun &run = inserted.first->second;
96
+ run.width = xt_text_shape_run(utf8, font, run.glyphs);
97
+ generation.bytes += estimateBytes(inserted.first->first, run);
98
+ respectBudget();
99
+ return &run;
100
+ }
101
+
102
+ uint64_t xt_text_cache_bytes() {
103
+ return g_generations[0].bytes + g_generations[1].bytes;
104
+ }
105
+
106
+ uint64_t xt_text_cache_entries() {
107
+ return (uint64_t)(g_generations[0].runs.size() + g_generations[1].runs.size());
108
+ }
109
+
110
+ void xt_text_cache_clear() {
111
+ g_generations[0].runs.clear();
112
+ g_generations[0].bytes = 0;
113
+ g_generations[1].runs.clear();
114
+ g_generations[1].bytes = 0;
115
+ g_current = 0;
116
+ }
117
+
118
+ } // namespace xtgui
@@ -0,0 +1,100 @@
1
+ /*
2
+ * xbintsc GUI engine — cached glyph bitmaps.
3
+ *
4
+ * `xt_text_rasterize` runs FreeType for one glyph. Canvas `fillText` used to
5
+ * call it for every glyph of every draw, so a redrawn label paid the whole
6
+ * rasterisation cost again on every frame. This unit keeps the bitmaps in two
7
+ * bounded generations, the same policy as the shaped-run cache in
8
+ * `text_cache.cpp`: a hot generation survives, and a page that walks a huge
9
+ * alphabet cannot grow the cache without bound.
10
+ *
11
+ * The cache is process-wide (fonts are), keyed by the face pointer and the
12
+ * glyph id. It is deliberately leaked at shutdown like the other engine
13
+ * caches; `xt_text_glyph_cache_clear` drops its contents when the fonts go.
14
+ */
15
+
16
+ #include "text.h"
17
+
18
+ #include <cstddef>
19
+ #include <cstdint>
20
+ #include <unordered_map>
21
+ #include <utility>
22
+
23
+ namespace xtgui {
24
+
25
+ namespace {
26
+
27
+ /** Per-generation bitmap budget and entry cap. */
28
+ constexpr size_t kGlyphCacheBytes = 2u * 1024u * 1024u;
29
+ constexpr size_t kGlyphCacheEntries = 8192;
30
+
31
+ struct GlyphCache {
32
+ std::unordered_map<uint64_t, GlyphImage> current;
33
+ std::unordered_map<uint64_t, GlyphImage> spare;
34
+ size_t current_bytes = 0;
35
+ size_t spare_bytes = 0;
36
+ };
37
+
38
+ GlyphCache &glyph_cache() {
39
+ /* Deliberately leaked: `Font` objects are destroyed at shutdown after this
40
+ * cache would be, and the key holds their addresses (never dereferenced). */
41
+ static GlyphCache *cache = new GlyphCache();
42
+ return *cache;
43
+ }
44
+
45
+ uint64_t glyph_key(Font *font, uint32_t glyph) {
46
+ return (uint64_t)(uintptr_t)font << 32 | (uint64_t)glyph;
47
+ }
48
+
49
+ /** Drop the old generation and make the current one the spare. */
50
+ void rotate(GlyphCache &cache) {
51
+ cache.current.swap(cache.spare);
52
+ std::swap(cache.current_bytes, cache.spare_bytes);
53
+ cache.spare.clear();
54
+ cache.spare_bytes = 0;
55
+ }
56
+
57
+ } // namespace
58
+
59
+ const GlyphImage *xt_text_glyph_cached(Font *font, uint32_t glyph) {
60
+ if (font == nullptr) return nullptr;
61
+ GlyphCache &cache = glyph_cache();
62
+ const uint64_t key = glyph_key(font, glyph);
63
+ auto found = cache.current.find(key);
64
+ if (found != cache.current.end()) {
65
+ return found->second.pixels.empty() ? nullptr : &found->second;
66
+ }
67
+ GlyphImage image;
68
+ auto spare = cache.spare.find(key);
69
+ if (spare != cache.spare.end()) {
70
+ image = std::move(spare->second);
71
+ cache.spare_bytes -= image.pixels.size();
72
+ cache.spare.erase(spare);
73
+ } else if (!xt_text_rasterize(font, glyph, image)) {
74
+ /* Cache the miss too: a space (or .notdef) would otherwise retry FreeType
75
+ * on every `fillText`. */
76
+ image.width = 0;
77
+ image.height = 0;
78
+ image.left = 0;
79
+ image.top = 0;
80
+ }
81
+ const size_t bytes = image.pixels.size();
82
+ if ((cache.current_bytes + bytes > kGlyphCacheBytes ||
83
+ cache.current.size() >= kGlyphCacheEntries) &&
84
+ !cache.current.empty()) {
85
+ rotate(cache);
86
+ }
87
+ cache.current_bytes += bytes;
88
+ auto inserted = cache.current.emplace(key, std::move(image));
89
+ return inserted.first->second.pixels.empty() ? nullptr : &inserted.first->second;
90
+ }
91
+
92
+ void xt_text_glyph_cache_clear(void) {
93
+ GlyphCache &cache = glyph_cache();
94
+ cache.current.clear();
95
+ cache.spare.clear();
96
+ cache.current_bytes = 0;
97
+ cache.spare_bytes = 0;
98
+ }
99
+
100
+ } // namespace xtgui