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
@@ -30,6 +30,27 @@ bool same_point(const CanvasPoint &a, const CanvasPoint &b) {
30
30
  return std::fabs(a.x - b.x) < 1e-6f && std::fabs(a.y - b.y) < 1e-6f;
31
31
  }
32
32
 
33
+ /** Twice the signed area of a closed polygon (shoelace). */
34
+ float signed_area(const CanvasSubpath &loop) {
35
+ float sum = 0.0f;
36
+ const size_t count = loop.size();
37
+ for (size_t i = 0; i < count; i++) {
38
+ const CanvasPoint &a = loop[i];
39
+ const CanvasPoint &b = loop[(i + 1) % count];
40
+ sum += a.x * b.y - b.x * a.y;
41
+ }
42
+ return sum;
43
+ }
44
+
45
+ /** Append one stroke piece to the batch, normalising its winding: a stroke is
46
+ * filled as one nonzero path, so a join that happened to be wound the other
47
+ * way would cancel the quads it overlaps instead of covering them. */
48
+ void append_piece(std::vector<CanvasSubpath> *loops, CanvasSubpath piece) {
49
+ if (piece.size() < 3) return;
50
+ if (signed_area(piece) < 0.0f) std::reverse(piece.begin(), piece.end());
51
+ loops->push_back(std::move(piece));
52
+ }
53
+
33
54
  } // namespace
34
55
 
35
56
  void Canvas2D::stroke_subpath(const CanvasSubpath &points, bool closed, const CanvasPaint &paint) {
@@ -54,10 +75,12 @@ void Canvas2D::stroke_polyline(const CanvasSubpath &points, bool closed, float h
54
75
  const size_t n = pts.size();
55
76
  if (n < 2) return;
56
77
 
57
- auto fill_loop = [&](const CanvasSubpath &loop) {
58
- canvas_detail::fill_loop(image_.pixels, image_.width, image_.height, loop, paint,
59
- state_.global_alpha);
60
- };
78
+ /* Every segment, join and cap becomes one polygon; the whole polyline is
79
+ * filled in a single coverage pass instead of one pass per piece (which was
80
+ * the dominant cost of a many-segment stroke). */
81
+ std::vector<CanvasSubpath> loops;
82
+ loops.reserve(n * 2 + 2);
83
+
61
84
  auto fill_disc = [&](const CanvasPoint &p) {
62
85
  CanvasSubpath disc;
63
86
  const int segments = 24;
@@ -66,7 +89,7 @@ void Canvas2D::stroke_polyline(const CanvasSubpath &points, bool closed, float h
66
89
  float angle = 2.0f * kPi * ((float)i / (float)segments);
67
90
  disc.push_back(CanvasPoint{p.x + half * std::cos(angle), p.y + half * std::sin(angle)});
68
91
  }
69
- fill_loop(disc);
92
+ append_piece(&loops, std::move(disc));
70
93
  };
71
94
 
72
95
  /* Segments. A square cap extends the first/last quad along the line by half
@@ -92,7 +115,7 @@ void Canvas2D::stroke_polyline(const CanvasSubpath &points, bool closed, float h
92
115
  }
93
116
  CanvasSubpath quad{CanvasPoint{p.x + nx, p.y + ny}, CanvasPoint{q.x + nx, q.y + ny},
94
117
  CanvasPoint{q.x - nx, q.y - ny}, CanvasPoint{p.x - nx, p.y - ny}};
95
- fill_loop(quad);
118
+ append_piece(&loops, std::move(quad));
96
119
  }
97
120
 
98
121
  /* Joins: round adds a disc, bevel a triangle, miter the intersect of the two
@@ -120,7 +143,7 @@ void Canvas2D::stroke_polyline(const CanvasSubpath &points, bool closed, float h
120
143
  CanvasPoint a{p.x - v1y * half * side, p.y + v1x * half * side};
121
144
  CanvasPoint b{p.x - v2y * half * side, p.y + v2x * half * side};
122
145
  if (state_.line_join == 2) {
123
- fill_loop(CanvasSubpath{a, b, p});
146
+ append_piece(&loops, CanvasSubpath{a, b, p});
124
147
  return;
125
148
  }
126
149
  float t = ((b.x - a.x) * v2y - (b.y - a.y) * v2x) / cross;
@@ -128,9 +151,9 @@ void Canvas2D::stroke_polyline(const CanvasSubpath &points, bool closed, float h
128
151
  float miterLength = std::sqrt((miter.x - p.x) * (miter.x - p.x) +
129
152
  (miter.y - p.y) * (miter.y - p.y));
130
153
  if (std::isfinite(miterLength) && miterLength <= half * state_.miter_limit) {
131
- fill_loop(CanvasSubpath{a, miter, b, p});
154
+ append_piece(&loops, CanvasSubpath{a, miter, b, p});
132
155
  } else {
133
- fill_loop(CanvasSubpath{a, b, p});
156
+ append_piece(&loops, CanvasSubpath{a, b, p});
134
157
  }
135
158
  };
136
159
 
@@ -148,6 +171,11 @@ void Canvas2D::stroke_polyline(const CanvasSubpath &points, bool closed, float h
148
171
  fill_disc(pts.back());
149
172
  }
150
173
  }
174
+
175
+ if (!loops.empty()) {
176
+ canvas_detail::fill_loops(image_.pixels, image_.width, image_.height, loops, paint,
177
+ state_.global_alpha, false, clip_mask());
178
+ }
151
179
  }
152
180
 
153
181
  void Canvas2D::dash_polyline(const CanvasSubpath &points, bool closed,
@@ -144,6 +144,65 @@ void Canvas2D::set_global_alpha(float alpha) {
144
144
  state_.global_alpha = alpha;
145
145
  }
146
146
 
147
+ void Canvas2D::set_global_composite_operation(const std::string &value) {
148
+ std::string lower = lower_ascii(value);
149
+ struct Entry {
150
+ const char *name;
151
+ int value;
152
+ };
153
+ static const Entry entries[] = {
154
+ {"source-over", kCanvasCompositeSourceOver},
155
+ {"source-in", kCanvasCompositeSourceIn},
156
+ {"source-out", kCanvasCompositeSourceOut},
157
+ {"source-atop", kCanvasCompositeSourceAtop},
158
+ {"destination-over", kCanvasCompositeDestinationOver},
159
+ {"destination-in", kCanvasCompositeDestinationIn},
160
+ {"destination-out", kCanvasCompositeDestinationOut},
161
+ {"destination-atop", kCanvasCompositeDestinationAtop},
162
+ {"lighter", kCanvasCompositeLighter},
163
+ {"copy", kCanvasCompositeCopy},
164
+ {"xor", kCanvasCompositeXor},
165
+ {"multiply", kCanvasCompositeMultiply},
166
+ {"screen", kCanvasCompositeScreen},
167
+ {"overlay", kCanvasCompositeOverlay},
168
+ {"darken", kCanvasCompositeDarken},
169
+ {"lighten", kCanvasCompositeLighten},
170
+ {"color-dodge", kCanvasCompositeColorDodge},
171
+ {"color-burn", kCanvasCompositeColorBurn},
172
+ {"hard-light", kCanvasCompositeHardLight},
173
+ {"soft-light", kCanvasCompositeSoftLight},
174
+ {"difference", kCanvasCompositeDifference},
175
+ {"exclusion", kCanvasCompositeExclusion},
176
+ {"hue", kCanvasCompositeHue},
177
+ {"saturation", kCanvasCompositeSaturation},
178
+ {"color", kCanvasCompositeColor},
179
+ {"luminosity", kCanvasCompositeLuminosity},
180
+ };
181
+ for (const Entry &entry : entries) {
182
+ if (lower == entry.name) {
183
+ state_.global_composite = entry.value;
184
+ state_.global_composite_string = entry.name;
185
+ return;
186
+ }
187
+ }
188
+ }
189
+
190
+ void Canvas2D::set_shadow_color(const std::string &css) {
191
+ parse_color_into(css, state_.shadow_color, &state_.shadow_color_string);
192
+ }
193
+
194
+ void Canvas2D::set_shadow_blur(float blur) {
195
+ if (!positive_finite(blur)) return;
196
+ state_.shadow_blur = blur;
197
+ }
198
+
199
+ void Canvas2D::set_image_smoothing_quality(const std::string &value) {
200
+ std::string lower = lower_ascii(value);
201
+ if (lower == "low" || lower == "medium" || lower == "high") {
202
+ state_.image_smoothing_quality = lower;
203
+ }
204
+ }
205
+
147
206
  void Canvas2D::set_font(const std::string &font) {
148
207
  state_.font = parse_font_string(font);
149
208
  state_.font_string = font;
@@ -0,0 +1,140 @@
1
+ /*
2
+ * xbintsc GUI engine — Canvas2D text drawing.
3
+ *
4
+ * `fillText`/`strokeText` shape a run with HarfBuzz and blit FreeType glyph
5
+ * coverage through the rasteriser. The state and path live in `canvas.h`; this
6
+ * unit only owns the text entry points and the optional `maxWidth` condense.
7
+ */
8
+
9
+ #include "canvas.h"
10
+
11
+ #include <cmath>
12
+ #include <string>
13
+ #include <vector>
14
+
15
+ #include "canvas_internal.h"
16
+ #include "perf.h"
17
+
18
+ namespace xtgui {
19
+
20
+ float Canvas2D::measure_text(const std::string &text) const {
21
+ return xt_text_measure_width(text, state_.font);
22
+ }
23
+
24
+ void Canvas2D::paint_text(const std::string &text, float x, float y, const CanvasPaint &paint,
25
+ bool stroke, float max_width) {
26
+ Font *font = xt_text_resolve(state_.font);
27
+ if (font == nullptr) return;
28
+ /* Layout and paint share the bounded shaping cache: a label redrawn every
29
+ * frame is shaped once, not once per frame. `maxWidth` only condenses the
30
+ * drawn run, so it does not affect the shaped result. */
31
+ const ShapedRun *run = xt_text_shape_cached(text, font);
32
+ if (run == nullptr) return;
33
+ const std::vector<ShapedGlyph> &glyphs = run->glyphs;
34
+ float width = run->width;
35
+ float ascent = 0.0f, descent = 0.0f, lineHeight = 0.0f;
36
+ xt_text_metrics(state_.font, &ascent, &descent, &lineHeight);
37
+
38
+ /* The optional `maxWidth` condenses the run horizontally instead of clipping
39
+ * it: advances and glyph bitmaps are squeezed by the same factor. */
40
+ float horizontal = 1.0f;
41
+ if (max_width > 0.0f && width > max_width && width > 0.0001f) {
42
+ horizontal = max_width / width;
43
+ }
44
+
45
+ float originX = x;
46
+ if (state_.text_align == 1) {
47
+ originX -= width * horizontal * 0.5f;
48
+ } else if (state_.text_align == 2) {
49
+ originX -= width * horizontal;
50
+ }
51
+ float originY = y;
52
+ switch (state_.text_baseline) {
53
+ case 1:
54
+ originY += ascent;
55
+ break;
56
+ case 2:
57
+ originY += (ascent - descent) * 0.5f;
58
+ break;
59
+ case 3:
60
+ originY -= descent;
61
+ break;
62
+ case 4:
63
+ originY += ascent * 0.8f;
64
+ break;
65
+ case 5:
66
+ originY -= descent;
67
+ break;
68
+ default:
69
+ break;
70
+ }
71
+
72
+ float scale = state_.transform.scale();
73
+ if (scale <= 0.0001f) scale = 1.0f;
74
+ bool scaled = std::fabs(scale - 1.0f) > 0.01f;
75
+
76
+ /* Scaling a face is a cached lookup, but it was still one path key + hash
77
+ * per glyph. There are at most a handful of faces in a run, so remember the
78
+ * sized face for the ones this call touches. */
79
+ Font *scaled_from[8] = {nullptr};
80
+ Font *scaled_to[8] = {nullptr};
81
+ int scaled_count = 0;
82
+ auto sized_face = [&](Font *face) -> Font * {
83
+ for (int i = 0; i < scaled_count; i++) {
84
+ if (scaled_from[i] == face) return scaled_to[i];
85
+ }
86
+ Font *sized = xt_text_scale(face, state_.font.pixel_size * scale);
87
+ if (sized == nullptr) sized = face;
88
+ if (scaled_count < 8) {
89
+ scaled_from[scaled_count] = face;
90
+ scaled_to[scaled_count] = sized;
91
+ scaled_count += 1;
92
+ }
93
+ return sized;
94
+ };
95
+
96
+ float penX = 0.0f;
97
+ for (const ShapedGlyph &glyph : glyphs) {
98
+ float userX = originX + (penX + glyph.x_offset) * horizontal;
99
+ float userY = originY + glyph.y_offset;
100
+ float deviceX = 0.0f, deviceY = 0.0f;
101
+ to_device(userX, userY, &deviceX, &deviceY);
102
+ Font *face = glyph.font != nullptr ? glyph.font : font;
103
+ if (scaled) face = sized_face(face);
104
+ const GlyphImage *image = xt_text_glyph_cached(face, glyph.glyph);
105
+ if (image != nullptr && image->width > 0 && image->height > 0) {
106
+ const float left = deviceX + (float)image->left * horizontal;
107
+ const float top = deviceY - (float)image->top;
108
+ mark_dirty(left, top, left + (float)image->width * horizontal,
109
+ top + (float)image->height);
110
+ canvas_detail::blit_glyph(image_.pixels, image_.width, image_.height, image->pixels,
111
+ image->width, image->height, image->left, image->top, deviceX,
112
+ deviceY, horizontal, paint, state_.global_alpha, clip_mask());
113
+ }
114
+ penX += glyph.x_advance;
115
+ }
116
+ (void)stroke;
117
+ generation_++;
118
+ }
119
+
120
+ void Canvas2D::fill_text(const std::string &text, float x, float y, float max_width) {
121
+ if (begin_shadow_layer()) {
122
+ fill_text(text, x, y, max_width);
123
+ end_shadow_layer();
124
+ return;
125
+ }
126
+ g_perf_frame.canvasDraws += 1;
127
+ paint_text(text, x, y, fill_paint(), false, max_width);
128
+ }
129
+
130
+ void Canvas2D::stroke_text(const std::string &text, float x, float y, float max_width) {
131
+ if (begin_shadow_layer()) {
132
+ stroke_text(text, x, y, max_width);
133
+ end_shadow_layer();
134
+ return;
135
+ }
136
+ g_perf_frame.canvasDraws += 1;
137
+ paint_text(text, x, y, stroke_paint(), true, max_width);
138
+ }
139
+
140
+ } // namespace xtgui
@@ -81,6 +81,13 @@ struct Rule {
81
81
 
82
82
  struct StyleSheet {
83
83
  std::vector<Rule> rules;
84
+ /* Facts the cascade can act on once per sheet instead of once per element (a
85
+ * `@media` block, a `::before` rule or a `transition` declaration used to be
86
+ * discovered by scanning every rule for every node). */
87
+ bool has_pseudo_elements = false;
88
+ bool has_hover = false;
89
+ bool has_focus = false;
90
+ bool has_transitions = false;
84
91
  };
85
92
 
86
93
  /** Dynamic element state consulted by `:hover` / `:focus` while matching. */
@@ -140,6 +140,27 @@ void parseRules(const std::string &text, const std::string &media, int *order,
140
140
  }
141
141
  }
142
142
 
143
+ /** Record the sheet-wide facts the cascade consults per run (see `StyleSheet`).
144
+ * `:hover`/`:focus` are matched by substring so a nested `:not(:hover)` rule is
145
+ * still recognised — a false positive only costs the restyle that is already
146
+ * being skipped. */
147
+ void computeSheetFlags(StyleSheet *sheet) {
148
+ for (const Rule &rule : sheet->rules) {
149
+ for (const ComplexSelector &selector : rule.selectors) {
150
+ for (const CompoundSelector &compound : selector.compounds) {
151
+ if (!compound.pseudo_element.empty()) sheet->has_pseudo_elements = true;
152
+ for (const std::string &pseudo : compound.pseudoClasses) {
153
+ if (pseudo.find("hover") != std::string::npos) sheet->has_hover = true;
154
+ if (pseudo.find("focus") != std::string::npos) sheet->has_focus = true;
155
+ }
156
+ }
157
+ }
158
+ for (const Declaration &declaration : rule.declarations) {
159
+ if (declaration.property.compare(0, 10, "transition") == 0) sheet->has_transitions = true;
160
+ }
161
+ }
162
+ }
163
+
143
164
  } // namespace
144
165
 
145
166
  std::vector<Declaration> xt_css_parse_declarations(const std::string &text) {
@@ -167,6 +188,7 @@ StyleSheet xt_css_parse_stylesheet(const std::string &text) {
167
188
  std::string clean = stripComments(text);
168
189
  int order = 0;
169
190
  parseRules(clean, "", &order, &sheet);
191
+ computeSheetFlags(&sheet);
170
192
  return sheet;
171
193
  }
172
194
 
@@ -4,6 +4,8 @@
4
4
 
5
5
  #include "document.h"
6
6
 
7
+ #include "perf.h"
8
+
7
9
  namespace xtgui {
8
10
 
9
11
  void XtDocument::collectStyleText(const Node *node, std::string *out) const {
@@ -43,11 +45,15 @@ void XtDocument::load(const std::string &html, float viewportWidth, float viewpo
43
45
  void XtDocument::restyle(float viewportWidth, float viewportHeight) {
44
46
  viewportWidth_ = viewportWidth;
45
47
  viewportHeight_ = viewportHeight;
48
+ g_perf_frame.restyles += 1;
46
49
  if (root_ != nullptr) {
47
50
  MatchState state;
48
51
  state.hover = hover_;
49
52
  state.focus = focus_;
50
- xt_style_compute(root_.get(), sheet_, styles_, viewportWidth, viewportHeight, state, &pseudos_);
53
+ bool hasTransitions = sheet_.has_transitions;
54
+ xt_style_compute(root_.get(), sheet_, styles_, viewportWidth, viewportHeight, state, &pseudos_,
55
+ &hasTransitions);
56
+ has_transitions_ = hasTransitions;
51
57
  startTransitions();
52
58
  updateDisplay();
53
59
  }
@@ -119,6 +125,10 @@ void applyNumber(XtStyle *style, const std::string &property, float value) {
119
125
 
120
126
  void XtDocument::startTransitions() {
121
127
  if (display_.empty()) return; // first style computation: nothing to animate from
128
+ /* Without a `transition` declaration anywhere in the sheet (or in an inline
129
+ * style) no transition can start, so skip the O(nodes) scan `restyle` would
130
+ * otherwise do every time. */
131
+ if (!has_transitions_) return;
122
132
  static const char *const kProperties[] = {"background-color", "color", "border-color",
123
133
  "border-radius", "transform"};
124
134
  for (auto &entry : styles_) {
@@ -186,6 +196,17 @@ void XtDocument::startTransitions() {
186
196
  }
187
197
 
188
198
  void XtDocument::updateDisplay() {
199
+ /* Without a `transition` declaration in the sheet (or an inline style) no
200
+ * transition can ever start, so the displayed style *is* the cascade result:
201
+ * skip the per-node deep copy of the whole style map (and keep `display_`
202
+ * empty, which `styleOf` already falls back from). */
203
+ if (!has_transitions_) {
204
+ if (!transitions_.empty()) transitions_.clear();
205
+ if (!display_.empty()) display_.clear();
206
+ layout_.compute(root_.get(), styles_, viewportWidth_, viewportHeight_, &mediaSizes_,
207
+ &canvasInfo_);
208
+ return;
209
+ }
189
210
  display_ = styles_;
190
211
  for (size_t i = 0; i < transitions_.size();) {
191
212
  ActiveTransition &transition = transitions_[i];
@@ -232,6 +253,8 @@ bool XtDocument::advance(double delta_ms) {
232
253
  bool XtDocument::setHover(const Node *node) {
233
254
  if (hover_ == node) return false;
234
255
  hover_ = node;
256
+ /* No `:hover` rule anywhere: the state changed but the paint cannot. */
257
+ if (!sheet_.has_hover) return false;
235
258
  restyle(viewportWidth_, viewportHeight_);
236
259
  return true;
237
260
  }
@@ -239,6 +262,7 @@ bool XtDocument::setHover(const Node *node) {
239
262
  bool XtDocument::setFocus(const Node *node) {
240
263
  if (focus_ == node) return false;
241
264
  focus_ = node;
265
+ if (!sheet_.has_focus) return false;
242
266
  restyle(viewportWidth_, viewportHeight_);
243
267
  return true;
244
268
  }
@@ -93,6 +93,10 @@ class XtDocument {
93
93
 
94
94
  /** Mark the document as needing a restyle/relayout before the next paint. */
95
95
  void invalidate() { dirty_ = true; }
96
+ /** Whether a restyle is pending (peek; `takeDirty` consumes it). The frame
97
+ * loop uses this to skip reconciling media/canvas tables when nothing in the
98
+ * document changed. */
99
+ bool dirty() const { return dirty_; }
96
100
  bool takeDirty() {
97
101
  bool was = dirty_;
98
102
  dirty_ = false;
@@ -127,6 +131,10 @@ class XtDocument {
127
131
  MediaSizeMap mediaSizes_; // `<video>` intrinsic sizes
128
132
  CanvasInfoMap canvasInfo_; // `<canvas>` sizes + ids
129
133
  std::vector<ActiveTransition> transitions_;
134
+ /** Whether any computed style declares a `transition` (from the sheet or an
135
+ * inline `style="…"`): when false the document skips the transition snapshot
136
+ * and `display_` stays empty. */
137
+ bool has_transitions_ = false;
130
138
  double clock_ms_ = 0.0;
131
139
  LayoutTree layout_;
132
140
  const Node *hover_ = nullptr;
@@ -80,6 +80,9 @@ std::string canvas_string_arg(int32_t argc, xt_value *argv, int32_t index) {
80
80
  METHOD(node_get_context) {
81
81
  (void)env;
82
82
  std::string mode = string_arg(argc, argv, 0);
83
+ /* After `transferControlToOffscreen` the element has no usable context; the
84
+ * OffscreenCanvas owns it. */
85
+ if (value_truthy(xt_object_get_cstr(self, "__xt_gui_offscreen"))) return XT_NULL;
83
86
  XtGuiWindow *win = nullptr;
84
87
  xtgui::Node *node = resolve_node(self, &win);
85
88
  if (mode == "webgl" || mode == "experimental-webgl" || mode == "webgl2") {
@@ -466,8 +469,20 @@ METHOD(canvas_draw_image) {
466
469
  xtgui::Canvas2D *canvas = context_canvas(self);
467
470
  if (canvas == nullptr || argc < 3) return XT_UNDEFINED;
468
471
  XtGuiWindow *win = nullptr;
469
- xtgui::Node *source = resolve_node(xt_arg(argc, argv, 0), &win);
472
+ xt_value source_value = xt_arg(argc, argv, 0);
470
473
  const xtgui::Image *image = nullptr;
474
+ /* An `OffscreenCanvas` (or a transferred one) has no DOM node; its bitmap is
475
+ * reachable through the process-wide canvas id. */
476
+ if (XT_IS_OBJECT(source_value)) {
477
+ xt_value canvas_id = xt_object_get_cstr(source_value, "__xt_gui_canvas");
478
+ if (XT_IS_NUMBER(canvas_id)) {
479
+ xtgui::Canvas2D *offscreen = xtgui::xt_canvas_by_id((uint64_t)xt_to_number(canvas_id));
480
+ if (offscreen != nullptr) {
481
+ image = &offscreen->image();
482
+ }
483
+ }
484
+ }
485
+ xtgui::Node *source = resolve_node(source_value, &win);
471
486
  if (source != nullptr && source->isElement() && source->isTag("img")) {
472
487
  const std::string *src = source->attr("src");
473
488
  if (src != nullptr) image = xtgui::xt_image_load(*src);
@@ -54,6 +54,15 @@ xt_value make_image_data(int width, int height, const std::vector<unsigned char>
54
54
 
55
55
  /** Decode a `createPattern` source: an `<img>` or another `<canvas>`. */
56
56
  bool source_image(XtGuiWindow *win, xt_value value, xtgui::Image *out) {
57
+ if (XT_IS_OBJECT(value)) {
58
+ xt_value canvas_id = xt_object_get_cstr(value, "__xt_gui_canvas");
59
+ if (XT_IS_NUMBER(canvas_id)) {
60
+ xtgui::Canvas2D *offscreen = xtgui::xt_canvas_by_id((uint64_t)xt_to_number(canvas_id));
61
+ if (offscreen == nullptr) return false;
62
+ *out = offscreen->image();
63
+ return out->width > 0 && out->height > 0;
64
+ }
65
+ }
57
66
  xtgui::Node *node = resolve_node(value, &win);
58
67
  if (node == nullptr || !node->isElement()) return false;
59
68
  if (node->isTag("img")) {