xbintsc 0.3.69 → 0.3.79

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 (99) hide show
  1. package/README.md +59 -4
  2. package/README.zh-CN.md +52 -2
  3. package/dist/tests/e2e/gui-advanced.test.d.ts +6 -0
  4. package/dist/tests/e2e/gui-advanced.test.js +261 -0
  5. package/dist/tests/e2e/gui-advanced.test.js.map +1 -0
  6. package/dist/tests/e2e/gui-canvas.test.d.ts +9 -0
  7. package/dist/tests/e2e/gui-canvas.test.js +233 -0
  8. package/dist/tests/e2e/gui-canvas.test.js.map +1 -0
  9. package/dist/tests/e2e/gui-example.test.d.ts +1 -1
  10. package/dist/tests/e2e/gui-example.test.js +2 -2
  11. package/dist/tests/e2e/gui-helpers.d.ts +3 -0
  12. package/dist/tests/e2e/gui-helpers.js +5 -0
  13. package/dist/tests/e2e/gui-helpers.js.map +1 -1
  14. package/dist/tests/e2e/gui-layout.test.js +73 -0
  15. package/dist/tests/e2e/gui-layout.test.js.map +1 -1
  16. package/dist/tests/e2e/gui-media.test.d.ts +8 -0
  17. package/dist/tests/e2e/gui-media.test.js +119 -0
  18. package/dist/tests/e2e/gui-media.test.js.map +1 -0
  19. package/dist/tests/e2e/gui-paint.test.js +40 -0
  20. package/dist/tests/e2e/gui-paint.test.js.map +1 -1
  21. package/doc/ai/extensions.md +16 -3
  22. package/doc/ai/zh-CN/extensions.md +10 -3
  23. package/doc/gui/animation.md +46 -0
  24. package/doc/gui/api.md +92 -0
  25. package/doc/gui/build.md +91 -0
  26. package/doc/gui/canvas.md +98 -0
  27. package/doc/gui/dom.md +123 -0
  28. package/doc/gui/input.md +22 -0
  29. package/doc/gui/known-issues.md +66 -0
  30. package/doc/gui/layout.md +72 -0
  31. package/doc/gui/media.md +87 -0
  32. package/doc/gui/rendering.md +98 -0
  33. package/doc/gui/runtime.md +88 -0
  34. package/doc/gui-scripts.md +7 -3
  35. package/doc/gui.md +80 -612
  36. package/doc/implemented.md +1 -0
  37. package/doc/zh-CN/gui/animation.md +46 -0
  38. package/doc/zh-CN/gui/api.md +88 -0
  39. package/doc/zh-CN/gui/build.md +84 -0
  40. package/doc/zh-CN/gui/canvas.md +80 -0
  41. package/doc/zh-CN/gui/dom.md +114 -0
  42. package/doc/zh-CN/gui/input.md +21 -0
  43. package/doc/zh-CN/gui/known-issues.md +50 -0
  44. package/doc/zh-CN/gui/layout.md +60 -0
  45. package/doc/zh-CN/gui/media.md +71 -0
  46. package/doc/zh-CN/gui/rendering.md +81 -0
  47. package/doc/zh-CN/gui/runtime.md +76 -0
  48. package/doc/zh-CN/gui-scripts.md +6 -2
  49. package/doc/zh-CN/gui.md +64 -532
  50. package/doc/zh-CN/implemented.md +1 -0
  51. package/llms.txt +15 -2
  52. package/package.json +1 -1
  53. package/runtime/ext_gui/canvas.cpp +580 -0
  54. package/runtime/ext_gui/canvas.h +170 -0
  55. package/runtime/ext_gui/canvas_internal.h +59 -0
  56. package/runtime/ext_gui/canvas_raster.cpp +254 -0
  57. package/runtime/ext_gui/document.cpp +24 -1
  58. package/runtime/ext_gui/document.h +11 -0
  59. package/runtime/ext_gui/dom_api.cpp +3 -0
  60. package/runtime/ext_gui/dom_api_canvas.cpp +548 -0
  61. package/runtime/ext_gui/dom_api_internal.h +73 -0
  62. package/runtime/ext_gui/dom_api_media.cpp +274 -0
  63. package/runtime/ext_gui/dom_api_proto.cpp +80 -0
  64. package/runtime/ext_gui/gui.cpp +6 -0
  65. package/runtime/ext_gui/gui_canvas.cpp +95 -0
  66. package/runtime/ext_gui/gui_engine.h +49 -0
  67. package/runtime/ext_gui/gui_events.cpp +21 -5
  68. package/runtime/ext_gui/gui_media.cpp +193 -0
  69. package/runtime/ext_gui/image.cpp +31 -12
  70. package/runtime/ext_gui/image.h +20 -7
  71. package/runtime/ext_gui/layout.cpp +52 -5
  72. package/runtime/ext_gui/layout.h +33 -1
  73. package/runtime/ext_gui/layout_flex.cpp +263 -99
  74. package/runtime/ext_gui/layout_flow.cpp +85 -9
  75. package/runtime/ext_gui/layout_flow_internal.h +84 -5
  76. package/runtime/ext_gui/layout_inline.cpp +54 -37
  77. package/runtime/ext_gui/media.cpp +514 -0
  78. package/runtime/ext_gui/media.h +104 -0
  79. package/runtime/ext_gui/nanosvg.h +3278 -0
  80. package/runtime/ext_gui/nanosvgrast.h +1472 -0
  81. package/runtime/ext_gui/paint.cpp +123 -38
  82. package/runtime/ext_gui/paint.h +36 -6
  83. package/runtime/ext_gui/renderer.cpp +113 -10
  84. package/runtime/ext_gui/renderer.h +49 -9
  85. package/runtime/ext_gui/renderer_image.cpp +203 -77
  86. package/runtime/ext_gui/renderer_internal.h +6 -8
  87. package/runtime/ext_gui/renderer_text.cpp +11 -26
  88. package/runtime/ext_gui/style.h +11 -0
  89. package/runtime/ext_gui/style_print.cpp +11 -0
  90. package/runtime/ext_gui/style_ua.cpp +7 -0
  91. package/runtime/ext_gui/style_values.cpp +40 -1
  92. package/runtime/ext_gui/svg.cpp +182 -0
  93. package/runtime/ext_gui/svg.h +46 -0
  94. package/runtime/ext_gui/text.cpp +156 -25
  95. package/runtime/ext_gui/text.h +11 -2
  96. package/runtime/ext_gui/window.cpp +4 -1
  97. package/scripts/build-gui.ts +148 -6
  98. package/scripts/check-file-length.ts +5 -1
  99. package/scripts/install-gui-deps.sh +28 -0
@@ -9,13 +9,144 @@
9
9
  #include <cstdio>
10
10
  #include <cstring>
11
11
 
12
+ #include "canvas.h"
12
13
  #include "image.h"
14
+ #include "media.h"
13
15
 
14
16
  #include "renderer_shaders.h"
15
17
 
16
18
  namespace xtgui {
17
19
  namespace renderer_detail {
18
20
 
21
+ /** One live `<video>` texture. The texture is created once per pixel size and
22
+ * re-uploaded in place whenever the decoder advances (`generation`). */
23
+ struct MediaTexture {
24
+ SDL_GPUTexture *texture = nullptr;
25
+ int width = 0;
26
+ int height = 0;
27
+ uint64_t generation = ~0ull;
28
+ };
29
+
30
+ static std::unordered_map<uint64_t, MediaTexture> g_media_textures;
31
+
32
+ /** One live `<canvas>` texture, keyed by the canvas id from `canvas.h`. It is
33
+ * re-uploaded in place whenever the canvas bumps its `generation`. */
34
+ static std::unordered_map<uint64_t, MediaTexture> g_canvas_textures;
35
+
36
+ /** Upload RGBA8 `pixels` into `texture` through a staging buffer, submitting
37
+ * its own command buffer. Because command buffers run in submission order and
38
+ * this one is submitted before the frame that samples the texture, re-uploading
39
+ * a texture that the previous frame drew is safe. */
40
+ static bool uploadPixels(SDL_GPUDevice *device, SDL_GPUTexture *texture, int width, int height,
41
+ const void *pixels, uint32_t bytes) {
42
+ if (bytes == 0) return false;
43
+ SDL_GPUTransferBufferCreateInfo transfer_info;
44
+ memset(&transfer_info, 0, sizeof(transfer_info));
45
+ transfer_info.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD;
46
+ transfer_info.size = bytes;
47
+ SDL_GPUTransferBuffer *transfer = SDL_CreateGPUTransferBuffer(device, &transfer_info);
48
+ if (transfer == nullptr) return false;
49
+ void *mapped = SDL_MapGPUTransferBuffer(device, transfer, false);
50
+ std::memcpy(mapped, pixels, bytes);
51
+ SDL_UnmapGPUTransferBuffer(device, transfer);
52
+
53
+ SDL_GPUCommandBuffer *cmd = SDL_AcquireGPUCommandBuffer(device);
54
+ if (cmd == nullptr) {
55
+ SDL_ReleaseGPUTransferBuffer(device, transfer);
56
+ return false;
57
+ }
58
+ SDL_GPUTextureTransferInfo source;
59
+ memset(&source, 0, sizeof(source));
60
+ source.transfer_buffer = transfer;
61
+ source.offset = 0;
62
+ source.pixels_per_row = (Uint32)width;
63
+ source.rows_per_layer = (Uint32)height;
64
+ SDL_GPUTextureRegion destination;
65
+ memset(&destination, 0, sizeof(destination));
66
+ destination.texture = texture;
67
+ destination.mip_level = 0;
68
+ destination.layer = 0;
69
+ destination.w = (Uint32)width;
70
+ destination.h = (Uint32)height;
71
+ destination.d = 1;
72
+ SDL_GPUCopyPass *copy = SDL_BeginGPUCopyPass(cmd);
73
+ SDL_UploadToGPUTexture(copy, &source, &destination, false);
74
+ SDL_EndGPUCopyPass(copy);
75
+ SDL_SubmitGPUCommandBuffer(cmd);
76
+ SDL_ReleaseGPUTransferBuffer(device, transfer);
77
+ return true;
78
+ }
79
+
80
+ /** Ensure the texture for `media_id` exists and holds the decoder's newest
81
+ * frame. Returns `nullptr` before the first frame is decoded. */
82
+ static SDL_GPUTexture *ensureMediaTexture(SDL_GPUDevice *device, uint64_t media_id) {
83
+ xtgui::MediaPlayer *player = xtgui::xt_media_by_id(media_id);
84
+ if (player == nullptr) return nullptr;
85
+ const xtgui::Image *image = player->frame();
86
+ MediaTexture &entry = g_media_textures[media_id];
87
+ if (image == nullptr || image->width <= 0 || image->height <= 0) return entry.texture;
88
+ if (entry.texture == nullptr || entry.width != image->width || entry.height != image->height) {
89
+ if (entry.texture != nullptr) SDL_ReleaseGPUTexture(device, entry.texture);
90
+ SDL_GPUTextureCreateInfo info;
91
+ memset(&info, 0, sizeof(info));
92
+ info.type = SDL_GPU_TEXTURETYPE_2D;
93
+ info.format = SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM;
94
+ info.usage = SDL_GPU_TEXTUREUSAGE_SAMPLER;
95
+ info.width = (Uint32)image->width;
96
+ info.height = (Uint32)image->height;
97
+ info.layer_count_or_depth = 1;
98
+ info.num_levels = 1;
99
+ info.sample_count = SDL_GPU_SAMPLECOUNT_1;
100
+ entry.texture = SDL_CreateGPUTexture(device, &info);
101
+ entry.width = image->width;
102
+ entry.height = image->height;
103
+ entry.generation = ~0ull;
104
+ if (entry.texture == nullptr) return nullptr;
105
+ }
106
+ if (entry.generation != player->frame_generation()) {
107
+ if (uploadPixels(device, entry.texture, entry.width, entry.height, image->pixels.data(),
108
+ (uint32_t)image->pixels.size())) {
109
+ entry.generation = player->frame_generation();
110
+ }
111
+ }
112
+ return entry.texture;
113
+ }
114
+
115
+ /** Ensure the texture for `canvas_id` exists and holds the canvas's newest
116
+ * pixels. Returns `nullptr` for an unknown or empty canvas. */
117
+ static SDL_GPUTexture *ensureCanvasTexture(SDL_GPUDevice *device, uint64_t canvas_id) {
118
+ xtgui::Canvas2D *canvas = xtgui::xt_canvas_by_id(canvas_id);
119
+ if (canvas == nullptr) return nullptr;
120
+ const xtgui::Image &image = canvas->image();
121
+ if (image.width <= 0 || image.height <= 0) return nullptr;
122
+ MediaTexture &entry = g_canvas_textures[canvas_id];
123
+ if (entry.texture == nullptr || entry.width != image.width || entry.height != image.height) {
124
+ if (entry.texture != nullptr) SDL_ReleaseGPUTexture(device, entry.texture);
125
+ SDL_GPUTextureCreateInfo info;
126
+ memset(&info, 0, sizeof(info));
127
+ info.type = SDL_GPU_TEXTURETYPE_2D;
128
+ info.format = SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM;
129
+ info.usage = SDL_GPU_TEXTUREUSAGE_SAMPLER;
130
+ info.width = (Uint32)image.width;
131
+ info.height = (Uint32)image.height;
132
+ info.layer_count_or_depth = 1;
133
+ info.num_levels = 1;
134
+ info.sample_count = SDL_GPU_SAMPLECOUNT_1;
135
+ entry.texture = SDL_CreateGPUTexture(device, &info);
136
+ entry.width = image.width;
137
+ entry.height = image.height;
138
+ entry.generation = ~0ull;
139
+ if (entry.texture == nullptr) return nullptr;
140
+ }
141
+ if (entry.generation != canvas->generation()) {
142
+ if (uploadPixels(device, entry.texture, entry.width, entry.height, image.pixels.data(),
143
+ (uint32_t)image.pixels.size())) {
144
+ entry.generation = canvas->generation();
145
+ }
146
+ }
147
+ return entry.texture;
148
+ }
149
+
19
150
  SDL_GPUGraphicsPipeline *createImagePipeline(SDL_GPUDevice *device, SDL_Window *reference) {
20
151
  const ShaderSource image_vs = selectShader(device, ShaderProgram::Image, false);
21
152
  const ShaderSource image_fs = selectShader(device, ShaderProgram::Image, true);
@@ -96,15 +227,22 @@ SDL_GPUGraphicsPipeline *createImagePipeline(SDL_GPUDevice *device, SDL_Window *
96
227
  return pipeline;
97
228
  }
98
229
 
99
- /** Decode `src` (cached) and upload it as an RGBA texture (cached by path). */
100
- SDL_GPUTexture *ensureImageTexture(SDL_GPUDevice *device, const std::string &src) {
101
- auto found = g_image_textures.find(src);
230
+ /** Decode `src` (cached) and upload it as an RGBA texture. Vector sources are
231
+ * rasterised at the drawn pixel size and cached by that size; bitmaps decode at
232
+ * their intrinsic size and are cached by path. */
233
+ SDL_GPUTexture *ensureImageTexture(SDL_GPUDevice *device, const std::string &src, int pixel_width,
234
+ int pixel_height) {
235
+ bool vector_source = xtgui::xt_image_is_vector(src);
236
+ std::string key =
237
+ vector_source ? src + "@" + std::to_string(pixel_width) + "x" + std::to_string(pixel_height)
238
+ : src;
239
+ auto found = g_image_textures.find(key);
102
240
  if (found != g_image_textures.end()) return found->second;
103
241
 
104
- const xtgui::Image *image = xtgui::xt_image_load(src);
242
+ const xtgui::Image *image = xtgui::xt_image_load(src, pixel_width, pixel_height);
105
243
  if (image == nullptr || image->width <= 0 || image->height <= 0) {
106
244
  fprintf(stderr, "xt_gui: could not decode image '%s'\n", src.c_str());
107
- g_image_textures.emplace(src, nullptr);
245
+ g_image_textures.emplace(key, nullptr);
108
246
  return nullptr;
109
247
  }
110
248
 
@@ -121,56 +259,18 @@ SDL_GPUTexture *ensureImageTexture(SDL_GPUDevice *device, const std::string &src
121
259
  SDL_GPUTexture *texture = SDL_CreateGPUTexture(device, &info);
122
260
  if (texture == nullptr) {
123
261
  fprintf(stderr, "xt_gui: image texture creation failed: %s\n", SDL_GetError());
124
- g_image_textures.emplace(src, nullptr);
262
+ g_image_textures.emplace(key, nullptr);
125
263
  return nullptr;
126
264
  }
127
265
 
128
266
  uint32_t bytes = (uint32_t)(image->pixels.size());
129
- SDL_GPUTransferBufferCreateInfo transfer_info;
130
- memset(&transfer_info, 0, sizeof(transfer_info));
131
- transfer_info.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD;
132
- transfer_info.size = bytes;
133
- SDL_GPUTransferBuffer *transfer = SDL_CreateGPUTransferBuffer(device, &transfer_info);
134
- if (transfer == nullptr) {
267
+ if (!uploadPixels(device, texture, image->width, image->height, image->pixels.data(), bytes)) {
135
268
  SDL_ReleaseGPUTexture(device, texture);
136
- g_image_textures.emplace(src, nullptr);
269
+ g_image_textures.emplace(key, nullptr);
137
270
  return nullptr;
138
271
  }
139
- void *mapped = SDL_MapGPUTransferBuffer(device, transfer, false);
140
- std::memcpy(mapped, image->pixels.data(), bytes);
141
- SDL_UnmapGPUTransferBuffer(device, transfer);
142
272
 
143
- SDL_GPUCommandBuffer *cmd = SDL_AcquireGPUCommandBuffer(device);
144
- if (cmd == nullptr) {
145
- SDL_ReleaseGPUTransferBuffer(device, transfer);
146
- SDL_ReleaseGPUTexture(device, texture);
147
- g_image_textures.emplace(src, nullptr);
148
- return nullptr;
149
- }
150
- SDL_GPUTextureTransferInfo source;
151
- memset(&source, 0, sizeof(source));
152
- source.transfer_buffer = transfer;
153
- source.offset = 0;
154
- source.pixels_per_row = (Uint32)image->width;
155
- source.rows_per_layer = (Uint32)image->height;
156
- SDL_GPUTextureRegion destination;
157
- memset(&destination, 0, sizeof(destination));
158
- destination.texture = texture;
159
- destination.mip_level = 0;
160
- destination.layer = 0;
161
- destination.x = 0;
162
- destination.y = 0;
163
- destination.z = 0;
164
- destination.w = (Uint32)image->width;
165
- destination.h = (Uint32)image->height;
166
- destination.d = 1;
167
- SDL_GPUCopyPass *copy = SDL_BeginGPUCopyPass(cmd);
168
- SDL_UploadToGPUTexture(copy, &source, &destination, false);
169
- SDL_EndGPUCopyPass(copy);
170
- SDL_SubmitGPUCommandBuffer(cmd);
171
- SDL_ReleaseGPUTransferBuffer(device, transfer);
172
-
173
- g_image_textures.emplace(src, texture);
273
+ g_image_textures.emplace(key, texture);
174
274
  return texture;
175
275
  }
176
276
 
@@ -178,13 +278,15 @@ SDL_GPUTexture *ensureImageTexture(SDL_GPUDevice *device, const std::string &src
178
278
  } // namespace xtgui
179
279
 
180
280
  void xt_gui_image_upload(SDL_GPUDevice *device, XtGuiGeometry *geometry,
181
- const xtgui::DisplayList &list) {
281
+ const xtgui::DisplayList &list, float scale) {
182
282
  using namespace xtgui::renderer_detail;
183
- g_image_batches.clear();
283
+ geometry->op_counts.assign(list.images.size(), 0);
284
+ geometry->op_textures.assign(list.images.size(), nullptr);
184
285
  if (g_image_pipeline == nullptr) {
185
286
  geometry->vertex_count = 0;
186
287
  return;
187
288
  }
289
+ if (scale <= 0.0f) scale = 1.0f;
188
290
  std::vector<TextVertex> verts;
189
291
  verts.reserve(list.images.size() * 6);
190
292
  xtgui::Color white{1.0f, 1.0f, 1.0f, 1.0f};
@@ -193,45 +295,69 @@ void xt_gui_image_upload(SDL_GPUDevice *device, XtGuiGeometry *geometry,
193
295
  full.v0 = 0.0f;
194
296
  full.u1 = 1.0f;
195
297
  full.v1 = 1.0f;
196
- for (const xtgui::PaintImage &image : list.images) {
298
+ for (size_t index = 0; index < list.images.size(); index++) {
299
+ const xtgui::PaintImage &image = list.images[index];
197
300
  if (image.width <= 0.0f || image.height <= 0.0f) continue;
198
- SDL_GPUTexture *texture = ensureImageTexture(device, image.src);
301
+ SDL_GPUTexture *texture = nullptr;
302
+ if (image.canvas_id != 0) {
303
+ texture = ensureCanvasTexture(device, image.canvas_id);
304
+ } else if (image.media_id != 0) {
305
+ texture = ensureMediaTexture(device, image.media_id);
306
+ } else {
307
+ /* Draw-time device pixels: an SVG is rasterised at this size so it stays
308
+ * crisp on HiDPI and when scaled. */
309
+ int pixel_width = (int)(image.width * scale + 0.5f);
310
+ int pixel_height = (int)(image.height * scale + 0.5f);
311
+ if (pixel_width < 1) pixel_width = 1;
312
+ if (pixel_height < 1) pixel_height = 1;
313
+ texture = ensureImageTexture(device, image.src, pixel_width, pixel_height);
314
+ }
199
315
  if (texture == nullptr) continue;
200
- uint32_t first = (uint32_t)verts.size();
201
316
  pushGlyph(verts, image.x, image.y, image.width, image.height, full, white);
202
- g_image_batches.push_back(ImageBatch{texture, first, 6});
317
+ geometry->op_counts[index] = 6;
318
+ geometry->op_textures[index] = texture;
203
319
  }
204
320
  uploadVertices(device, geometry, verts.data(), (uint32_t)verts.size(), sizeof(TextVertex));
205
321
  }
206
322
 
207
- void xt_gui_image_draw(SDL_GPUCommandBuffer *cmd, SDL_GPURenderPass *pass, XtGuiGeometry *geometry,
208
- float viewport_width, float viewport_height) {
209
- using namespace xtgui::renderer_detail;
210
- if (g_image_pipeline == nullptr || g_sampler == nullptr) return;
211
- if (geometry->buffer == nullptr || geometry->vertex_count == 0 || g_image_batches.empty()) return;
212
- if (viewport_width <= 0.0f || viewport_height <= 0.0f) return;
213
- SDL_BindGPUGraphicsPipeline(pass, g_image_pipeline);
214
- SDL_GPUBufferBinding binding;
215
- binding.buffer = geometry->buffer;
216
- binding.offset = 0;
217
- SDL_BindGPUVertexBuffers(pass, 0, &binding, 1);
218
- float viewport[2] = {viewport_width, viewport_height};
219
- SDL_PushGPUVertexUniformData(cmd, 0, viewport, sizeof(viewport));
220
- for (const ImageBatch &batch : g_image_batches) {
221
- if (batch.texture == nullptr) continue;
222
- SDL_GPUTextureSamplerBinding texture_binding;
223
- texture_binding.texture = batch.texture;
224
- texture_binding.sampler = g_sampler;
225
- SDL_BindGPUFragmentSamplers(pass, 0, &texture_binding, 1);
226
- SDL_DrawGPUPrimitives(pass, batch.count, 1, batch.first, 0);
227
- }
228
- }
229
-
230
323
  void xt_gui_image_textures_destroy(SDL_GPUDevice *device) {
231
324
  using namespace xtgui::renderer_detail;
232
325
  for (auto &entry : g_image_textures) {
233
326
  if (entry.second != nullptr) SDL_ReleaseGPUTexture(device, entry.second);
234
327
  }
235
328
  g_image_textures.clear();
236
- g_image_batches.clear();
329
+ for (auto &entry : g_media_textures) {
330
+ if (entry.second.texture != nullptr) SDL_ReleaseGPUTexture(device, entry.second.texture);
331
+ }
332
+ g_media_textures.clear();
333
+ for (auto &entry : g_canvas_textures) {
334
+ if (entry.second.texture != nullptr) SDL_ReleaseGPUTexture(device, entry.second.texture);
335
+ }
336
+ g_canvas_textures.clear();
337
+ }
338
+
339
+ void xt_gui_media_upload_frames(SDL_GPUDevice *device) {
340
+ using namespace xtgui::renderer_detail;
341
+ for (auto &entry : g_media_textures) ensureMediaTexture(device, entry.first);
342
+ }
343
+
344
+ void xt_gui_media_texture_release(SDL_GPUDevice *device, uint64_t media_id) {
345
+ using namespace xtgui::renderer_detail;
346
+ auto it = g_media_textures.find(media_id);
347
+ if (it == g_media_textures.end()) return;
348
+ if (it->second.texture != nullptr) SDL_ReleaseGPUTexture(device, it->second.texture);
349
+ g_media_textures.erase(it);
350
+ }
351
+
352
+ void xt_gui_canvas_upload_frames(SDL_GPUDevice *device) {
353
+ using namespace xtgui::renderer_detail;
354
+ for (auto &entry : g_canvas_textures) ensureCanvasTexture(device, entry.first);
355
+ }
356
+
357
+ void xt_gui_canvas_texture_release(SDL_GPUDevice *device, uint64_t canvas_id) {
358
+ using namespace xtgui::renderer_detail;
359
+ auto it = g_canvas_textures.find(canvas_id);
360
+ if (it == g_canvas_textures.end()) return;
361
+ if (it->second.texture != nullptr) SDL_ReleaseGPUTexture(device, it->second.texture);
362
+ g_canvas_textures.erase(it);
237
363
  }
@@ -35,19 +35,12 @@ struct TextVertex {
35
35
  };
36
36
 
37
37
  /* Image quads are batched by texture; textures are cached by source path. */
38
- struct ImageBatch {
39
- SDL_GPUTexture *texture = nullptr;
40
- uint32_t first = 0;
41
- uint32_t count = 0;
42
- };
43
-
44
38
  extern SDL_GPUGraphicsPipeline *g_pipeline;
45
39
  extern SDL_GPUGraphicsPipeline *g_text_pipeline;
46
40
  extern SDL_GPUGraphicsPipeline *g_image_pipeline;
47
41
  extern SDL_GPUSampler *g_sampler;
48
42
  extern int g_renderer_failed;
49
43
  extern bool g_text_ok;
50
- extern std::vector<ImageBatch> g_image_batches;
51
44
  extern std::unordered_map<std::string, SDL_GPUTexture *> g_image_textures;
52
45
 
53
46
  /* -- glyph atlas ---------------------------------------------------------- */
@@ -104,7 +97,12 @@ SDL_GPUSampler *createSampler(SDL_GPUDevice *device);
104
97
  /* -- images --------------------------------------------------------------- */
105
98
 
106
99
  SDL_GPUGraphicsPipeline *createImagePipeline(SDL_GPUDevice *device, SDL_Window *reference);
107
- SDL_GPUTexture *ensureImageTexture(SDL_GPUDevice *device, const std::string &src);
100
+ /** Decode `src` (cached) and upload it as an RGBA texture. `pixel_width` /
101
+ * `pixel_height` are the drawn size in device pixels; vector sources (SVG) are
102
+ * rasterised at that size, raster sources ignore it. The texture cache is keyed
103
+ * by the resulting pixel size, so an SVG drawn at two sizes gets two textures. */
104
+ SDL_GPUTexture *ensureImageTexture(SDL_GPUDevice *device, const std::string &src, int pixel_width,
105
+ int pixel_height);
108
106
 
109
107
  } // namespace renderer_detail
110
108
  } // namespace xtgui
@@ -295,22 +295,26 @@ void xt_gui_text_upload(SDL_GPUDevice *device, XtGuiGeometry *geometry,
295
295
  if (scale <= 0.0f) scale = 1.0f;
296
296
 
297
297
  std::vector<TextVertex> verts;
298
- for (const xtgui::PaintText &run : list.texts) {
298
+ geometry->op_counts.assign(list.texts.size(), 0);
299
+ for (size_t index = 0; index < list.texts.size(); index++) {
300
+ const xtgui::PaintText &run = list.texts[index];
299
301
  if (run.text.empty() || run.color.a <= 0.0f) continue;
300
302
  xtgui::Font *logical = xtgui::xt_text_resolve(run.font);
301
303
  if (logical == nullptr) continue;
302
- xtgui::FontSpec raster_spec = run.font;
303
- raster_spec.pixel_size = run.font.pixel_size * scale;
304
- xtgui::Font *raster = xtgui::xt_text_resolve(raster_spec);
305
- if (raster == nullptr) raster = logical;
306
304
 
307
305
  std::vector<xtgui::ShapedGlyph> glyphs;
308
306
  xtgui::xt_text_shape_run(run.text, logical, glyphs);
309
307
  float pen_x = run.x;
308
+ uint32_t before = (uint32_t)verts.size();
310
309
  for (const xtgui::ShapedGlyph &glyph : glyphs) {
311
310
  if (glyph.glyph != 0) {
311
+ xtgui::Font *face = glyph.font != nullptr ? glyph.font : logical;
312
+ if (scale != 1.0f) {
313
+ xtgui::Font *scaled = xtgui::xt_text_scale(face, run.font.pixel_size * scale);
314
+ if (scaled != nullptr) face = scaled;
315
+ }
312
316
  GlyphEntry entry;
313
- if (ensureGlyph(device, raster, glyph.glyph, entry)) {
317
+ if (ensureGlyph(device, face, glyph.glyph, entry)) {
314
318
  float quad_x = pen_x + glyph.x_offset + (float)entry.left / scale;
315
319
  float quad_y = run.baseline + glyph.y_offset - (float)entry.top / scale;
316
320
  float quad_w = (float)entry.width / scale;
@@ -320,27 +324,8 @@ void xt_gui_text_upload(SDL_GPUDevice *device, XtGuiGeometry *geometry,
320
324
  }
321
325
  pen_x += glyph.x_advance;
322
326
  }
327
+ geometry->op_counts[index] = (uint32_t)verts.size() - before;
323
328
  }
324
329
  uploadVertices(device, geometry, verts.data(), (uint32_t)verts.size(), sizeof(TextVertex));
325
330
  flushAtlas(device);
326
331
  }
327
-
328
- void xt_gui_text_draw(SDL_GPUCommandBuffer *cmd, SDL_GPURenderPass *pass, XtGuiGeometry *geometry,
329
- float viewport_width, float viewport_height) {
330
- using namespace xtgui::renderer_detail;
331
- if (!g_text_ok || geometry->buffer == nullptr || geometry->vertex_count == 0) return;
332
- if (g_atlas_texture == nullptr) return;
333
- if (viewport_width <= 0.0f || viewport_height <= 0.0f) return;
334
- SDL_BindGPUGraphicsPipeline(pass, g_text_pipeline);
335
- SDL_GPUBufferBinding binding;
336
- binding.buffer = geometry->buffer;
337
- binding.offset = 0;
338
- SDL_BindGPUVertexBuffers(pass, 0, &binding, 1);
339
- SDL_GPUTextureSamplerBinding atlas_binding;
340
- atlas_binding.texture = g_atlas_texture;
341
- atlas_binding.sampler = g_sampler;
342
- SDL_BindGPUFragmentSamplers(pass, 0, &atlas_binding, 1);
343
- float viewport[2] = {viewport_width, viewport_height};
344
- SDL_PushGPUVertexUniformData(cmd, 0, viewport, sizeof(viewport));
345
- SDL_DrawGPUPrimitives(pass, geometry->vertex_count, 1, 0, 0);
346
- }
@@ -22,8 +22,12 @@ namespace xtgui {
22
22
  enum class Display { None, Block, Inline, InlineBlock, Flex, ListItem };
23
23
  enum class Position { Static, Relative, Absolute, Fixed };
24
24
  enum class FlexDirection { Row, RowReverse, Column, ColumnReverse };
25
+ enum class FlexWrap { NoWrap, Wrap, WrapReverse };
25
26
  enum class JustifyContent { FlexStart, FlexEnd, Center, SpaceBetween, SpaceAround, SpaceEvenly };
26
27
  enum class AlignItems { Stretch, FlexStart, FlexEnd, Center, Baseline };
28
+ enum class AlignContent { Stretch, FlexStart, FlexEnd, Center, SpaceBetween, SpaceAround };
29
+ enum class FloatSide { None, Left, Right };
30
+ enum class Clear { None, Left, Right, Both };
27
31
  enum class TextAlign { Start, Center, End, Justify };
28
32
  enum class WhiteSpace { Normal, NoWrap, Pre, PreWrap, PreLine };
29
33
  enum class Overflow { Visible, Hidden, Scroll, Auto };
@@ -84,9 +88,13 @@ struct XtStyle {
84
88
  WhiteSpace white_space = WhiteSpace::Normal; // inherited
85
89
 
86
90
  FlexDirection flex_direction = FlexDirection::Row;
91
+ FlexWrap flex_wrap = FlexWrap::NoWrap;
87
92
  JustifyContent justify_content = JustifyContent::FlexStart;
88
93
  AlignItems align_items = AlignItems::Stretch;
94
+ AlignContent align_content = AlignContent::Stretch;
89
95
  Length gap;
96
+ Length row_gap;
97
+ Length column_gap;
90
98
  float flex_grow = 0.0f;
91
99
  float flex_shrink = 1.0f;
92
100
  Length flex_basis;
@@ -96,6 +104,9 @@ struct XtStyle {
96
104
  int z_index = 0;
97
105
  bool has_z_index = false;
98
106
 
107
+ FloatSide float_side = FloatSide::None;
108
+ Clear clear = Clear::None;
109
+
99
110
  /** `transition` specs; not inherited. Applied by `XtDocument`. */
100
111
  std::vector<TransitionSpec> transitions;
101
112
  };
@@ -61,9 +61,14 @@ std::string xt_style_property_to_string(const XtStyle &style, const std::string
61
61
  static const char *const displayNames[] = {"none", "block", "inline", "inline-block", "flex", "list-item"};
62
62
  static const char *const positionNames[] = {"static", "relative", "absolute", "fixed"};
63
63
  static const char *const flexNames[] = {"row", "row-reverse", "column", "column-reverse"};
64
+ static const char *const wrapNames[] = {"nowrap", "wrap", "wrap-reverse"};
64
65
  static const char *const justifyNames[] = {"flex-start", "flex-end", "center", "space-between",
65
66
  "space-around", "space-evenly"};
66
67
  static const char *const alignNames[] = {"stretch", "flex-start", "flex-end", "center", "baseline"};
68
+ static const char *const alignContentNames[] = {"stretch", "flex-start", "flex-end", "center",
69
+ "space-between", "space-around"};
70
+ static const char *const floatNames[] = {"none", "left", "right"};
71
+ static const char *const clearNames[] = {"none", "left", "right", "both"};
67
72
  static const char *const alignTextNames[] = {"start", "center", "end", "justify"};
68
73
 
69
74
  if (property == "display") return displayNames[(int)style.display];
@@ -98,9 +103,15 @@ std::string xt_style_property_to_string(const XtStyle &style, const std::string
98
103
  }
99
104
  if (property == "text-align") return alignTextNames[(int)style.text_align];
100
105
  if (property == "flex-direction") return flexNames[(int)style.flex_direction];
106
+ if (property == "flex-wrap") return wrapNames[(int)style.flex_wrap];
101
107
  if (property == "justify-content") return justifyNames[(int)style.justify_content];
102
108
  if (property == "align-items") return alignNames[(int)style.align_items];
109
+ if (property == "align-content") return alignContentNames[(int)style.align_content];
103
110
  if (property == "gap") return formatLength(style.gap);
111
+ if (property == "row-gap") return formatLength(style.row_gap);
112
+ if (property == "column-gap") return formatLength(style.column_gap);
113
+ if (property == "float") return floatNames[(int)style.float_side];
114
+ if (property == "clear") return clearNames[(int)style.clear];
104
115
  if (property == "flex-grow") return formatNumber(style.flex_grow);
105
116
  if (property == "flex-shrink") return formatNumber(style.flex_shrink);
106
117
  if (property == "flex-basis") return formatLength(style.flex_basis);
@@ -40,6 +40,13 @@ void xt_style_apply_user_agent(const Node *node, XtStyle *style) {
40
40
  style->display = Display::InlineBlock;
41
41
  }
42
42
  if (tag == "img") style->display = Display::InlineBlock;
43
+ /* `<video>` mirrors `<img>` (a replaced inline-block; 300x150 by default from
44
+ * layout), while `<audio>` and its `<source>`/`<track>` children have no
45
+ * rendered box of their own. */
46
+ if (tag == "video") style->display = Display::InlineBlock;
47
+ /* `<canvas>` is also a replaced inline-block (300x150 default in layout). */
48
+ if (tag == "canvas") style->display = Display::InlineBlock;
49
+ if (tag == "audio" || tag == "source" || tag == "track") style->display = Display::None;
43
50
  if (tag == "body") style->margin.set({8.0f, Unit::Px});
44
51
  if (tag == "p") style->margin.top = style->margin.bottom = {16.0f, Unit::Px};
45
52
  if (tag == "blockquote") {
@@ -347,6 +347,23 @@ void xt_style_apply_declaration(XtStyle *style, const std::string &property,
347
347
  if (parseEnum(value, names, 4, &index)) style->flex_direction = (FlexDirection)index;
348
348
  return;
349
349
  }
350
+ if (property == "flex-wrap") {
351
+ static const char *const names[] = {"nowrap", "wrap", "wrap-reverse"};
352
+ int index = 0;
353
+ if (parseEnum(value, names, 3, &index)) style->flex_wrap = (FlexWrap)index;
354
+ return;
355
+ }
356
+ if (property == "flex-flow") {
357
+ /* `flex-flow: <direction> || <wrap>` in either order. */
358
+ for (const std::string &token : xt_css_split_whitespace(value)) {
359
+ static const char *const dirs[] = {"row", "row-reverse", "column", "column-reverse"};
360
+ static const char *const wraps[] = {"nowrap", "wrap", "wrap-reverse"};
361
+ int index = 0;
362
+ if (parseEnum(token, dirs, 4, &index)) style->flex_direction = (FlexDirection)index;
363
+ else if (parseEnum(token, wraps, 3, &index)) style->flex_wrap = (FlexWrap)index;
364
+ }
365
+ return;
366
+ }
350
367
  if (property == "justify-content") {
351
368
  static const char *const names[] = {"flex-start", "flex-end", "center", "space-between",
352
369
  "space-around", "space-evenly"};
@@ -360,10 +377,20 @@ void xt_style_apply_declaration(XtStyle *style, const std::string &property,
360
377
  if (parseEnum(value, names, 5, &index)) style->align_items = (AlignItems)index;
361
378
  return;
362
379
  }
363
- if (property == "gap" || property == "row-gap" || property == "column-gap") {
380
+ if (property == "align-content") {
381
+ static const char *const names[] = {"stretch", "flex-start", "flex-end", "center",
382
+ "space-between", "space-around"};
383
+ int index = 0;
384
+ if (parseEnum(value, names, 6, &index)) style->align_content = (AlignContent)index;
385
+ return;
386
+ }
387
+ if (property == "gap") {
364
388
  assignLength(&style->gap, value);
389
+ style->row_gap = style->column_gap = style->gap;
365
390
  return;
366
391
  }
392
+ if (property == "row-gap") { assignLength(&style->row_gap, value); return; }
393
+ if (property == "column-gap") { assignLength(&style->column_gap, value); return; }
367
394
  if (property == "flex-grow") {
368
395
  float number = 0;
369
396
  if (xt_css_parse_number(value, &number)) style->flex_grow = number;
@@ -433,6 +460,18 @@ void xt_style_apply_declaration(XtStyle *style, const std::string &property,
433
460
  }
434
461
  return;
435
462
  }
463
+ if (property == "float") {
464
+ static const char *const names[] = {"none", "left", "right"};
465
+ int index = 0;
466
+ if (parseEnum(value, names, 3, &index)) style->float_side = (FloatSide)index;
467
+ return;
468
+ }
469
+ if (property == "clear") {
470
+ static const char *const names[] = {"none", "left", "right", "both"};
471
+ int index = 0;
472
+ if (parseEnum(value, names, 4, &index)) style->clear = (Clear)index;
473
+ return;
474
+ }
436
475
  if (property == "transition") {
437
476
  parseTransitionShorthand(value, &style->transitions);
438
477
  return;