xbintsc 0.5.41 → 0.5.43

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 (43) hide show
  1. package/dist/src/cli/help.d.ts +1 -1
  2. package/dist/src/driver/version.d.ts +1 -1
  3. package/dist/src/driver/version.js +1 -1
  4. package/dist/tests/e2e/gui-data-url.test.d.ts +10 -0
  5. package/dist/tests/e2e/gui-data-url.test.js +127 -0
  6. package/dist/tests/e2e/gui-data-url.test.js.map +1 -0
  7. package/dist/tests/e2e/gui-effects.test.js +144 -4
  8. package/dist/tests/e2e/gui-effects.test.js.map +1 -1
  9. package/dist/tests/e2e/gui-paint.test.js +77 -0
  10. package/dist/tests/e2e/gui-paint.test.js.map +1 -1
  11. package/doc/gui/api.md +1 -1
  12. package/doc/gui/canvas.md +6 -2
  13. package/doc/gui/known-issues.md +18 -10
  14. package/doc/gui/media.md +11 -2
  15. package/doc/gui/rendering.md +48 -19
  16. package/doc/gui.md +12 -8
  17. package/doc/zh-CN/gui/api.md +1 -1
  18. package/doc/zh-CN/gui/canvas.md +6 -4
  19. package/doc/zh-CN/gui/known-issues.md +12 -8
  20. package/doc/zh-CN/gui/media.md +9 -2
  21. package/doc/zh-CN/gui/rendering.md +37 -15
  22. package/doc/zh-CN/gui.md +7 -6
  23. package/package.json +7 -7
  24. package/runtime/ext_gui/dom_api_canvas.cpp +42 -24
  25. package/runtime/ext_gui/gradient_values.cpp +44 -48
  26. package/runtime/ext_gui/gui_events.cpp +16 -13
  27. package/runtime/ext_gui/image.cpp +180 -25
  28. package/runtime/ext_gui/image.h +6 -4
  29. package/runtime/ext_gui/paint.cpp +69 -4
  30. package/runtime/ext_gui/paint.h +57 -1
  31. package/runtime/ext_gui/paint_background.cpp +197 -41
  32. package/runtime/ext_gui/paint_effects.cpp +2 -1
  33. package/runtime/ext_gui/paint_internal.h +5 -1
  34. package/runtime/ext_gui/renderer.cpp +81 -13
  35. package/runtime/ext_gui/renderer.h +19 -5
  36. package/runtime/ext_gui/renderer_gradient.cpp +156 -0
  37. package/runtime/ext_gui/renderer_image.cpp +6 -1
  38. package/runtime/ext_gui/renderer_internal.h +48 -0
  39. package/runtime/ext_gui/renderer_layer.cpp +552 -0
  40. package/runtime/ext_gui/style.h +29 -1
  41. package/runtime/ext_gui/style_print.cpp +10 -2
  42. package/runtime/ext_gui/style_values.cpp +10 -0
  43. package/src/driver/version.ts +1 -1
@@ -0,0 +1,552 @@
1
+ /*
2
+ * xbintsc GUI engine — offscreen `mix-blend-mode` compositing.
3
+ *
4
+ * The ordinary painter is single-pass: every shape blends directly with the
5
+ * swapchain, which is fine for source-over but cannot sample the backdrop that
6
+ * a CSS blend mode needs. A `mix-blend-mode` element is therefore painted into
7
+ * a display-list group (see `paint.cpp`), rendered to an offscreen texture and
8
+ * composited over the scene with a small GLSL shader. The shader is compiled at
9
+ * run time by the bundled glslang and, on Metal, cross-compiled to MSL by
10
+ * SPIRV-Cross — the same path WebGL uses. Backends that consume neither format
11
+ * (D3D12/DXIL) fall back to the ordinary painter.
12
+ *
13
+ * Two full-viewport textures per nesting level are reused across frames; one
14
+ * holds the current scene, the other is the target of the next composite. That
15
+ * ping-pong keeps the backdrop sampleable while it is being written.
16
+ */
17
+
18
+ #include "renderer_internal.h"
19
+
20
+ #include <cstdio>
21
+ #include <cstring>
22
+ #include <string>
23
+ #include <vector>
24
+
25
+ #include "perf.h"
26
+ #include "webgl_glsl.h"
27
+ #include "webgl_msl.h"
28
+
29
+ namespace xtgui {
30
+ namespace renderer_detail {
31
+
32
+ namespace {
33
+
34
+ const char *const kBlendVS = R"GLSL(#version 300 es
35
+ precision highp float;
36
+
37
+ out vec2 v_uv;
38
+
39
+ void main() {
40
+ vec2 corners[6] = vec2[6](vec2(-1.0, -1.0), vec2(1.0, -1.0), vec2(-1.0, 1.0),
41
+ vec2(1.0, -1.0), vec2(1.0, 1.0), vec2(-1.0, 1.0));
42
+ vec2 p = corners[gl_VertexIndex];
43
+ v_uv = vec2(p.x * 0.5 + 0.5, 0.5 - p.y * 0.5);
44
+ gl_Position = vec4(p, 0.0, 1.0);
45
+ }
46
+ )GLSL";
47
+
48
+ /* `u_mode` is the MixBlendMode ordinal, or -1 to copy the backdrop (the final
49
+ * blit to the swapchain). Colours are un-premultiplied for the blend formula
50
+ * and re-premultiplied for the source-over composite, matching the CSS
51
+ * compositing spec. */
52
+ const char *const kBlendFS = R"GLSL(#version 300 es
53
+ precision highp float;
54
+
55
+ in vec2 v_uv;
56
+ uniform sampler2D u_backdrop;
57
+ uniform sampler2D u_layer;
58
+ uniform int u_mode;
59
+ out vec4 fragColor;
60
+
61
+ float separable(int mode, float b, float s) {
62
+ if (mode == 1) return b * s;
63
+ if (mode == 2) return b + s - b * s;
64
+ if (mode == 3) return b <= 0.5 ? 2.0 * b * s : 1.0 - 2.0 * (1.0 - b) * (1.0 - s);
65
+ if (mode == 4) return min(b, s);
66
+ if (mode == 5) return max(b, s);
67
+ if (mode == 6) return b <= 0.0 ? 0.0 : (s >= 1.0 ? 1.0 : min(1.0, b / (1.0 - s)));
68
+ if (mode == 7) return b >= 1.0 ? 1.0 : (s <= 0.0 ? 0.0 : 1.0 - min(1.0, (1.0 - b) / s));
69
+ if (mode == 8) return s <= 0.5 ? 2.0 * b * s : 1.0 - 2.0 * (1.0 - b) * (1.0 - s);
70
+ if (mode == 9) {
71
+ float d = b <= 0.25 ? ((16.0 * b - 12.0) * b + 4.0) * b : sqrt(b);
72
+ return s <= 0.5 ? b - (1.0 - 2.0 * s) * b * (1.0 - b) : b + (2.0 * s - 1.0) * (d - b);
73
+ }
74
+ if (mode == 10) return abs(b - s);
75
+ if (mode == 11) return b + s - 2.0 * b * s;
76
+ return s;
77
+ }
78
+
79
+ float lum(vec3 c) { return dot(c, vec3(0.3, 0.59, 0.11)); }
80
+
81
+ vec3 clipColor(vec3 c) {
82
+ float l = lum(c);
83
+ float n = min(min(c.r, c.g), c.b);
84
+ float x = max(max(c.r, c.g), c.b);
85
+ if (n < 0.0) c = l + (c - l) * l / (l - n);
86
+ if (x > 1.0) c = l + (c - l) * (1.0 - l) / (x - l);
87
+ return c;
88
+ }
89
+
90
+ vec3 setLum(vec3 c, float l) { return clipColor(c + (l - lum(c))); }
91
+
92
+ float sat(vec3 c) { return max(max(c.r, c.g), c.b) - min(min(c.r, c.g), c.b); }
93
+
94
+ vec3 setSat(vec3 c, float s) {
95
+ float mn = min(min(c.r, c.g), c.b);
96
+ float mx = max(max(c.r, c.g), c.b);
97
+ if (mx <= mn) return vec3(0.0);
98
+ return (c - mn) * s / (mx - mn);
99
+ }
100
+
101
+ vec3 nonSeparable(int mode, vec3 b, vec3 s) {
102
+ if (mode == 12) return setLum(setSat(s, sat(b)), lum(b));
103
+ if (mode == 13) return setLum(setSat(b, sat(s)), lum(b));
104
+ if (mode == 14) return setLum(s, lum(b));
105
+ if (mode == 15) return setLum(b, lum(s));
106
+ return s;
107
+ }
108
+
109
+ void main() {
110
+ vec4 b = texture(u_backdrop, v_uv);
111
+ if (u_mode < 0) {
112
+ fragColor = b;
113
+ return;
114
+ }
115
+ vec4 s = texture(u_layer, v_uv);
116
+ float ab = clamp(b.a, 0.0, 1.0);
117
+ float as = clamp(s.a, 0.0, 1.0);
118
+ vec3 cb = ab > 0.00001 ? b.rgb / ab : vec3(0.0);
119
+ vec3 cs = as > 0.00001 ? s.rgb / as : vec3(0.0);
120
+ vec3 blended;
121
+ if (u_mode <= 11) {
122
+ blended = vec3(separable(u_mode, cb.r, cs.r), separable(u_mode, cb.g, cs.g),
123
+ separable(u_mode, cb.b, cs.b));
124
+ } else {
125
+ blended = nonSeparable(u_mode, cb, cs);
126
+ }
127
+ float ao = as + ab * (1.0 - as);
128
+ vec3 premul = as * (1.0 - ab) * cs + as * ab * blended + (1.0 - as) * ab * cb;
129
+ vec3 co = ao > 0.00001 ? premul / ao : vec3(0.0);
130
+ fragColor = vec4(clamp(co, 0.0, 1.0), ao);
131
+ }
132
+ )GLSL";
133
+
134
+ SDL_GPUGraphicsPipeline *g_layer_pipeline = nullptr;
135
+ SDL_GPUSampler *g_layer_sampler = nullptr;
136
+ SDL_GPUTextureFormat g_layer_format = SDL_GPU_TEXTUREFORMAT_INVALID;
137
+ bool g_layer_failed = false;
138
+
139
+ struct BlendParams {
140
+ int mode;
141
+ int padding[3];
142
+ };
143
+
144
+ void pushMode(SDL_GPUCommandBuffer *cmd, int mode) {
145
+ BlendParams params;
146
+ std::memset(&params, 0, sizeof(params));
147
+ params.mode = mode;
148
+ SDL_PushGPUFragmentUniformData(cmd, 0, &params, sizeof(params));
149
+ }
150
+
151
+ /** Compile `source` into the shader format the device consumes. The returned
152
+ * `code` either points at `spirv` or at `text`, so both must outlive
153
+ * `SDL_CreateGPUShader`. */
154
+ bool prepareStage(const std::string &source, bool fragment, SDL_GPUShaderFormat formats,
155
+ std::vector<uint32_t> *spirv, std::string *text, const Uint8 **code,
156
+ size_t *code_size, std::string *entry, std::string *log) {
157
+ GlslShaderInfo info;
158
+ scan_glsl(source, fragment, &info);
159
+ if (!info.unsupported.empty()) {
160
+ *log = info.unsupported;
161
+ return false;
162
+ }
163
+ GlslLinkLayout layout;
164
+ layout.varyings["v_uv"] = 0;
165
+ if (fragment) {
166
+ layout.samplers["u_backdrop"] = 0;
167
+ layout.samplers["u_layer"] = 1;
168
+ layout.block_members = info.uniforms;
169
+ }
170
+ const std::string rewritten = rewrite_glsl(source, fragment, layout, info);
171
+ if (!compile_glsl(rewritten, fragment, spirv, log)) return false;
172
+
173
+ if ((formats & SDL_GPU_SHADERFORMAT_MSL) != 0) {
174
+ if (!cross_compile_msl(*spirv, fragment, text, entry, log)) return false;
175
+ *code = (const Uint8 *)text->data();
176
+ *code_size = text->size();
177
+ return true;
178
+ }
179
+ if ((formats & SDL_GPU_SHADERFORMAT_SPIRV) != 0) {
180
+ *code = (const Uint8 *)spirv->data();
181
+ *code_size = spirv->size() * sizeof(uint32_t);
182
+ *entry = "main";
183
+ return true;
184
+ }
185
+ *log = "this GPU backend consumes neither Metal nor Vulkan shaders";
186
+ return false;
187
+ }
188
+
189
+ bool ensureLayerPipeline(SDL_GPUDevice *device) {
190
+ if (g_layer_pipeline != nullptr) return true;
191
+ if (g_layer_failed) return false;
192
+ if (device == nullptr || g_swapchain_format == SDL_GPU_TEXTUREFORMAT_INVALID) return false;
193
+ const SDL_GPUTextureUsageFlags usage =
194
+ SDL_GPU_TEXTUREUSAGE_SAMPLER | SDL_GPU_TEXTUREUSAGE_COLOR_TARGET;
195
+ if (!SDL_GPUTextureSupportsFormat(device, g_swapchain_format, SDL_GPU_TEXTURETYPE_2D, usage)) {
196
+ g_layer_failed = true;
197
+ return false;
198
+ }
199
+ const SDL_GPUShaderFormat formats = SDL_GetGPUShaderFormats(device);
200
+ if ((formats & (SDL_GPU_SHADERFORMAT_MSL | SDL_GPU_SHADERFORMAT_SPIRV)) == 0) {
201
+ g_layer_failed = true;
202
+ return false;
203
+ }
204
+
205
+ std::string log;
206
+ std::vector<uint32_t> vs_spirv;
207
+ std::vector<uint32_t> fs_spirv;
208
+ std::string vs_text;
209
+ std::string fs_text;
210
+ const Uint8 *vs_code = nullptr;
211
+ const Uint8 *fs_code = nullptr;
212
+ size_t vs_size = 0;
213
+ size_t fs_size = 0;
214
+ std::string vs_entry;
215
+ std::string fs_entry;
216
+ if (!prepareStage(kBlendVS, false, formats, &vs_spirv, &vs_text, &vs_code, &vs_size, &vs_entry,
217
+ &log) ||
218
+ !prepareStage(kBlendFS, true, formats, &fs_spirv, &fs_text, &fs_code, &fs_size, &fs_entry,
219
+ &log)) {
220
+ fprintf(stderr, "xt_gui: mix-blend-mode shader unavailable: %s\n", log.c_str());
221
+ g_layer_failed = true;
222
+ return false;
223
+ }
224
+
225
+ const SDL_GPUShaderFormat shader_format =
226
+ (formats & SDL_GPU_SHADERFORMAT_MSL) != 0 ? SDL_GPU_SHADERFORMAT_MSL
227
+ : SDL_GPU_SHADERFORMAT_SPIRV;
228
+ SDL_GPUShaderCreateInfo vs_info;
229
+ std::memset(&vs_info, 0, sizeof(vs_info));
230
+ vs_info.code_size = vs_size;
231
+ vs_info.code = vs_code;
232
+ vs_info.entrypoint = vs_entry.c_str();
233
+ vs_info.format = shader_format;
234
+ vs_info.stage = SDL_GPU_SHADERSTAGE_VERTEX;
235
+ SDL_GPUShader *vs = SDL_CreateGPUShader(device, &vs_info);
236
+
237
+ SDL_GPUShaderCreateInfo fs_info;
238
+ std::memset(&fs_info, 0, sizeof(fs_info));
239
+ fs_info.code_size = fs_size;
240
+ fs_info.code = fs_code;
241
+ fs_info.entrypoint = fs_entry.c_str();
242
+ fs_info.format = shader_format;
243
+ fs_info.stage = SDL_GPU_SHADERSTAGE_FRAGMENT;
244
+ fs_info.num_uniform_buffers = 1;
245
+ fs_info.num_samplers = 2;
246
+ SDL_GPUShader *fs = SDL_CreateGPUShader(device, &fs_info);
247
+
248
+ if (vs == nullptr || fs == nullptr) {
249
+ fprintf(stderr, "xt_gui: mix-blend-mode shader creation failed: %s\n", SDL_GetError());
250
+ if (vs != nullptr) SDL_ReleaseGPUShader(device, vs);
251
+ if (fs != nullptr) SDL_ReleaseGPUShader(device, fs);
252
+ g_layer_failed = true;
253
+ return false;
254
+ }
255
+
256
+ SDL_GPUColorTargetDescription target;
257
+ std::memset(&target, 0, sizeof(target));
258
+ target.format = g_swapchain_format;
259
+ target.blend_state.enable_blend = false;
260
+
261
+ SDL_GPUGraphicsPipelineCreateInfo info;
262
+ std::memset(&info, 0, sizeof(info));
263
+ info.vertex_shader = vs;
264
+ info.fragment_shader = fs;
265
+ info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST;
266
+ info.rasterizer_state.fill_mode = SDL_GPU_FILLMODE_FILL;
267
+ info.rasterizer_state.cull_mode = SDL_GPU_CULLMODE_NONE;
268
+ info.rasterizer_state.front_face = SDL_GPU_FRONTFACE_COUNTER_CLOCKWISE;
269
+ info.multisample_state.sample_count = SDL_GPU_SAMPLECOUNT_1;
270
+ info.target_info.color_target_descriptions = &target;
271
+ info.target_info.num_color_targets = 1;
272
+ g_layer_pipeline = SDL_CreateGPUGraphicsPipeline(device, &info);
273
+ SDL_ReleaseGPUShader(device, vs);
274
+ SDL_ReleaseGPUShader(device, fs);
275
+ if (g_layer_pipeline == nullptr) {
276
+ fprintf(stderr, "xt_gui: mix-blend-mode pipeline creation failed: %s\n", SDL_GetError());
277
+ g_layer_failed = true;
278
+ return false;
279
+ }
280
+
281
+ SDL_GPUSamplerCreateInfo sampler_info;
282
+ std::memset(&sampler_info, 0, sizeof(sampler_info));
283
+ sampler_info.min_filter = SDL_GPU_FILTER_NEAREST;
284
+ sampler_info.mag_filter = SDL_GPU_FILTER_NEAREST;
285
+ sampler_info.mipmap_mode = SDL_GPU_SAMPLERMIPMAPMODE_NEAREST;
286
+ sampler_info.address_mode_u = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
287
+ sampler_info.address_mode_v = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
288
+ sampler_info.address_mode_w = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
289
+ g_layer_sampler = SDL_CreateGPUSampler(device, &sampler_info);
290
+ if (g_layer_sampler == nullptr) {
291
+ fprintf(stderr, "xt_gui: mix-blend-mode sampler creation failed: %s\n", SDL_GetError());
292
+ SDL_ReleaseGPUGraphicsPipeline(device, g_layer_pipeline);
293
+ g_layer_pipeline = nullptr;
294
+ g_layer_failed = true;
295
+ return false;
296
+ }
297
+ g_layer_format = g_swapchain_format;
298
+ return true;
299
+ }
300
+
301
+ void releaseSceneTextures(SDL_GPUDevice *device, XtGuiGeometry *geometry) {
302
+ for (SDL_GPUTexture *texture : geometry->scene_textures) {
303
+ if (texture != nullptr) SDL_ReleaseGPUTexture(device, texture);
304
+ }
305
+ geometry->scene_textures.clear();
306
+ geometry->scene_width = 0;
307
+ geometry->scene_height = 0;
308
+ geometry->scene_format = SDL_GPU_TEXTUREFORMAT_INVALID;
309
+ }
310
+
311
+ bool prepareSceneTextures(SDL_GPUDevice *device, XtGuiGeometry *geometry, int pairs, int width,
312
+ int height, SDL_GPUTextureFormat format) {
313
+ if (geometry->scene_format != format || geometry->scene_width != (uint32_t)width ||
314
+ geometry->scene_height != (uint32_t)height) {
315
+ releaseSceneTextures(device, geometry);
316
+ }
317
+ if (geometry->scene_format == SDL_GPU_TEXTUREFORMAT_INVALID) {
318
+ geometry->scene_format = format;
319
+ geometry->scene_width = (uint32_t)width;
320
+ geometry->scene_height = (uint32_t)height;
321
+ }
322
+ while ((int)geometry->scene_textures.size() < pairs * 2) {
323
+ SDL_GPUTextureCreateInfo info;
324
+ std::memset(&info, 0, sizeof(info));
325
+ info.type = SDL_GPU_TEXTURETYPE_2D;
326
+ info.format = format;
327
+ info.usage = SDL_GPU_TEXTUREUSAGE_SAMPLER | SDL_GPU_TEXTUREUSAGE_COLOR_TARGET;
328
+ info.width = (Uint32)width;
329
+ info.height = (Uint32)height;
330
+ info.layer_count_or_depth = 1;
331
+ info.num_levels = 1;
332
+ info.sample_count = SDL_GPU_SAMPLECOUNT_1;
333
+ SDL_GPUTexture *texture = SDL_CreateGPUTexture(device, &info);
334
+ if (texture == nullptr) {
335
+ fprintf(stderr, "xt_gui: mix-blend-mode texture creation failed: %s\n", SDL_GetError());
336
+ releaseSceneTextures(device, geometry);
337
+ g_layer_failed = true;
338
+ return false;
339
+ }
340
+ geometry->scene_textures.push_back(texture);
341
+ }
342
+ return true;
343
+ }
344
+
345
+ SDL_GPUTexture *sceneTexture(XtGuiGeometry *geometry, int depth, int parity) {
346
+ size_t index = (size_t)depth * 2 + (size_t)(parity & 1);
347
+ if (index >= geometry->scene_textures.size()) return nullptr;
348
+ return geometry->scene_textures[index];
349
+ }
350
+
351
+ struct SceneContext {
352
+ SDL_GPUCommandBuffer *cmd = nullptr;
353
+ XtGuiGeometry *shapes = nullptr;
354
+ XtGuiGeometry *texts = nullptr;
355
+ XtGuiGeometry *images = nullptr;
356
+ const std::vector<XtGuiDrawRun> *runs = nullptr;
357
+ const DisplayList *list = nullptr;
358
+ int target_width = 0;
359
+ int target_height = 0;
360
+ float pixel_density = 1.0f;
361
+ int max_depth = 0;
362
+ std::vector<int> parity;
363
+ };
364
+
365
+ size_t matchingLayerEnd(const std::vector<XtGuiDrawRun> &runs, size_t begin, size_t end) {
366
+ int depth = 1;
367
+ for (size_t i = begin + 1; i < end; i++) {
368
+ if (runs[i].pipeline == XtGuiDrawRun::LayerBegin) depth++;
369
+ else if (runs[i].pipeline == XtGuiDrawRun::LayerEnd) {
370
+ depth--;
371
+ if (depth == 0) return i;
372
+ }
373
+ }
374
+ return end;
375
+ }
376
+
377
+ void beginPass(SDL_GPUCommandBuffer *cmd, SDL_GPUTexture *texture, bool clear,
378
+ const float color[4], SDL_GPURenderPass **pass) {
379
+ SDL_GPUColorTargetInfo target;
380
+ std::memset(&target, 0, sizeof(target));
381
+ target.texture = texture;
382
+ target.clear_color.r = color != nullptr ? color[0] : 0.0f;
383
+ target.clear_color.g = color != nullptr ? color[1] : 0.0f;
384
+ target.clear_color.b = color != nullptr ? color[2] : 0.0f;
385
+ target.clear_color.a = color != nullptr ? color[3] : 0.0f;
386
+ target.load_op = clear ? SDL_GPU_LOADOP_CLEAR : SDL_GPU_LOADOP_LOAD;
387
+ target.store_op = SDL_GPU_STOREOP_STORE;
388
+ *pass = SDL_BeginGPURenderPass(cmd, &target, 1, nullptr);
389
+ }
390
+
391
+ void compositePass(SceneContext &context, SDL_GPUTexture *target, SDL_GPUTexture *backdrop,
392
+ SDL_GPUTexture *layer, int mode) {
393
+ SDL_GPURenderPass *pass = nullptr;
394
+ beginPass(context.cmd, target, true, nullptr, &pass);
395
+ if (pass == nullptr) return;
396
+ SDL_BindGPUGraphicsPipeline(pass, g_layer_pipeline);
397
+ SDL_GPUTextureSamplerBinding bindings[2];
398
+ bindings[0].texture = backdrop;
399
+ bindings[0].sampler = g_layer_sampler;
400
+ bindings[1].texture = layer;
401
+ bindings[1].sampler = g_layer_sampler;
402
+ SDL_BindGPUFragmentSamplers(pass, 0, bindings, 2);
403
+ pushMode(context.cmd, mode);
404
+ SDL_Rect full;
405
+ full.x = 0;
406
+ full.y = 0;
407
+ full.w = context.target_width;
408
+ full.h = context.target_height;
409
+ SDL_SetGPUScissor(pass, &full);
410
+ SDL_DrawGPUPrimitives(pass, 6, 1, 0, 0);
411
+ SDL_EndGPURenderPass(pass);
412
+ xtgui::g_perf_frame.drawCalls += 1;
413
+ }
414
+
415
+ void renderSequence(SceneContext &context, size_t begin, size_t end, int depth, bool clear,
416
+ const float color[4]) {
417
+ const std::vector<XtGuiDrawRun> &runs = *context.runs;
418
+ SDL_GPUTexture *texture = sceneTexture(context.shapes, depth, context.parity[depth]);
419
+ if (texture == nullptr) return;
420
+ SDL_GPURenderPass *pass = nullptr;
421
+ beginPass(context.cmd, texture, clear, color, &pass);
422
+ if (pass == nullptr) return;
423
+
424
+ for (size_t i = begin; i < end; i++) {
425
+ const XtGuiDrawRun &run = runs[i];
426
+ if (run.pipeline == XtGuiDrawRun::LayerBegin) {
427
+ SDL_EndGPURenderPass(pass);
428
+ size_t layer_end = matchingLayerEnd(runs, i, end);
429
+ if (depth + 1 < (int)context.parity.size()) {
430
+ /* The child renders to its own pair, then is composited over this
431
+ * scene. `depth` is the parent, `depth + 1` the child. */
432
+ renderSequence(context, i + 1, layer_end, depth + 1, true, nullptr);
433
+ SDL_GPUTexture *child = sceneTexture(context.shapes, depth + 1, context.parity[depth + 1]);
434
+ SDL_GPUTexture *backdrop = texture;
435
+ SDL_GPUTexture *target = sceneTexture(context.shapes, depth,
436
+ context.parity[depth] ^ 1);
437
+ int mode = 0;
438
+ if (context.list != nullptr && run.layer < context.list->layers.size())
439
+ mode = (int)context.list->layers[run.layer].blend;
440
+ if (child != nullptr && backdrop != nullptr && target != nullptr) {
441
+ compositePass(context, target, backdrop, child, mode);
442
+ context.parity[depth] ^= 1;
443
+ }
444
+ }
445
+ /* Continue painting later siblings over the composited scene. */
446
+ texture = sceneTexture(context.shapes, depth, context.parity[depth]);
447
+ beginPass(context.cmd, texture, false, nullptr, &pass);
448
+ if (pass == nullptr) return;
449
+ i = layer_end;
450
+ continue;
451
+ }
452
+ if (run.pipeline == XtGuiDrawRun::LayerEnd) continue;
453
+
454
+ size_t segment_end = i;
455
+ while (segment_end < end && runs[segment_end].pipeline != XtGuiDrawRun::LayerBegin &&
456
+ runs[segment_end].pipeline != XtGuiDrawRun::LayerEnd) {
457
+ segment_end++;
458
+ }
459
+ drawRuns(pass, runs, i, segment_end, context.shapes, context.texts, context.images,
460
+ context.target_width, context.target_height, context.pixel_density);
461
+ i = segment_end - 1;
462
+ }
463
+ SDL_EndGPURenderPass(pass);
464
+ }
465
+
466
+ } // namespace
467
+
468
+ bool layerRendererReady(SDL_GPUDevice *device) { return ensureLayerPipeline(device); }
469
+
470
+ bool drawLayeredScene(SDL_GPUCommandBuffer *cmd, SDL_GPUTexture *swapchain,
471
+ const float clear_color[4], XtGuiGeometry *shapes, XtGuiGeometry *texts,
472
+ XtGuiGeometry *images, const DisplayList &list, float viewport_width,
473
+ float viewport_height, float pixel_density) {
474
+ if (cmd == nullptr || swapchain == nullptr || shapes == nullptr || g_layer_pipeline == nullptr)
475
+ return false;
476
+ int target_width = (int)(viewport_width * pixel_density + 0.5f);
477
+ int target_height = (int)(viewport_height * pixel_density + 0.5f);
478
+ if (target_width <= 0 || target_height <= 0) return false;
479
+
480
+ int max_depth = 0;
481
+ int depth = 0;
482
+ for (const XtGuiDrawRun &run : shapes->runs) {
483
+ if (run.pipeline == XtGuiDrawRun::LayerBegin) {
484
+ depth++;
485
+ if (depth > max_depth) max_depth = depth;
486
+ } else if (run.pipeline == XtGuiDrawRun::LayerEnd) {
487
+ if (depth > 0) depth--;
488
+ }
489
+ }
490
+ if (!prepareSceneTextures(g_device, shapes, max_depth + 1, target_width, target_height,
491
+ g_layer_format))
492
+ return false;
493
+
494
+ SceneContext context;
495
+ context.cmd = cmd;
496
+ context.shapes = shapes;
497
+ context.texts = texts;
498
+ context.images = images;
499
+ context.runs = &shapes->runs;
500
+ context.list = &list;
501
+ context.target_width = target_width;
502
+ context.target_height = target_height;
503
+ context.pixel_density = pixel_density;
504
+ context.max_depth = max_depth;
505
+ context.parity.assign((size_t)max_depth + 1, 0);
506
+
507
+ static const float kTransparent[4] = {0.0f, 0.0f, 0.0f, 0.0f};
508
+ renderSequence(context, 0, shapes->runs.size(), 0, true,
509
+ clear_color != nullptr ? clear_color : kTransparent);
510
+
511
+ SDL_GPUTexture *scene = sceneTexture(shapes, 0, context.parity[0]);
512
+ if (scene == nullptr) return true;
513
+ /* Copy the finished scene to the swapchain. Mode -1 samples only the
514
+ * backdrop, so the same texture is bound twice. */
515
+ SDL_GPURenderPass *pass = nullptr;
516
+ beginPass(cmd, swapchain, true, clear_color, &pass);
517
+ if (pass == nullptr) return true;
518
+ SDL_BindGPUGraphicsPipeline(pass, g_layer_pipeline);
519
+ SDL_GPUTextureSamplerBinding bindings[2];
520
+ bindings[0].texture = scene;
521
+ bindings[0].sampler = g_layer_sampler;
522
+ bindings[1].texture = scene;
523
+ bindings[1].sampler = g_layer_sampler;
524
+ SDL_BindGPUFragmentSamplers(pass, 0, bindings, 2);
525
+ pushMode(cmd, -1);
526
+ SDL_Rect full;
527
+ full.x = 0;
528
+ full.y = 0;
529
+ full.w = target_width;
530
+ full.h = target_height;
531
+ SDL_SetGPUScissor(pass, &full);
532
+ SDL_DrawGPUPrimitives(pass, 6, 1, 0, 0);
533
+ SDL_EndGPURenderPass(pass);
534
+ xtgui::g_perf_frame.drawCalls += 1;
535
+ return true;
536
+ }
537
+
538
+ void layerRendererDestroy(SDL_GPUDevice *device) {
539
+ if (g_layer_pipeline != nullptr) {
540
+ SDL_ReleaseGPUGraphicsPipeline(device, g_layer_pipeline);
541
+ g_layer_pipeline = nullptr;
542
+ }
543
+ if (g_layer_sampler != nullptr) {
544
+ SDL_ReleaseGPUSampler(device, g_layer_sampler);
545
+ g_layer_sampler = nullptr;
546
+ }
547
+ g_layer_format = SDL_GPU_TEXTUREFORMAT_INVALID;
548
+ g_layer_failed = false;
549
+ }
550
+
551
+ } // namespace renderer_detail
552
+ } // namespace xtgui
@@ -38,6 +38,27 @@ enum class BorderStyle { None, Solid, Dashed, Dotted };
38
38
  enum class FontStyle { Normal, Italic };
39
39
  enum class TimingFunction { Linear, Ease, EaseIn, EaseOut, EaseInOut };
40
40
 
41
+ /** CSS `mix-blend-mode`. The order matches the composite shader's `u_mode`
42
+ * enum and the keyword table in `style_values.cpp`. */
43
+ enum class MixBlendMode {
44
+ Normal,
45
+ Multiply,
46
+ Screen,
47
+ Overlay,
48
+ Darken,
49
+ Lighten,
50
+ ColorDodge,
51
+ ColorBurn,
52
+ HardLight,
53
+ SoftLight,
54
+ Difference,
55
+ Exclusion,
56
+ Hue,
57
+ Saturation,
58
+ Color,
59
+ Luminosity
60
+ };
61
+
41
62
  /** One `transition` entry: which property, how long, how it eases. */
42
63
  struct TransitionSpec {
43
64
  std::string property = "all";
@@ -56,7 +77,9 @@ struct GridTrack {
56
77
  };
57
78
 
58
79
  struct BackgroundGradientStop {
59
- float offset = 0.0f;
80
+ /** `Unit::Auto` means the offset was omitted; `Percent` and length units are
81
+ * resolved against the gradient line when the layer is painted. */
82
+ Length offset;
60
83
  Color color;
61
84
  };
62
85
 
@@ -72,6 +95,9 @@ struct BackgroundLayer {
72
95
  BackgroundKind kind = BackgroundKind::Linear;
73
96
  /* linear */
74
97
  float angle = 180.0f; // CSS degrees: 0 = to top, 90 = to right, 180 = to bottom
98
+ /** `repeating-linear-gradient` / `repeating-radial-gradient`: the stop
99
+ * pattern is tiled beyond its last stop instead of holding the final colour. */
100
+ bool repeating = false;
75
101
  /* radial */
76
102
  bool circle = false;
77
103
  bool has_shape = false;
@@ -225,6 +251,8 @@ struct XtStyle {
225
251
  Length flex_basis;
226
252
 
227
253
  float opacity = 1.0f;
254
+ /** `mix-blend-mode` (not inherited: it creates a compositing group). */
255
+ MixBlendMode mix_blend_mode = MixBlendMode::Normal;
228
256
  Overflow overflow = Overflow::Visible;
229
257
  int z_index = 0;
230
258
  bool has_z_index = false;
@@ -236,6 +236,14 @@ std::string xt_style_property_to_string(const XtStyle &style, const std::string
236
236
  if (property == "flex-basis") return formatLength(style.flex_basis);
237
237
  if (property == "border-radius") return formatNumber(style.border_radius) + "px";
238
238
  if (property == "opacity") return formatNumber(style.opacity);
239
+ if (property == "mix-blend-mode") {
240
+ static const char *const names[] = {
241
+ "normal", "multiply", "screen", "overlay", "darken",
242
+ "lighten", "color-dodge", "color-burn", "hard-light", "soft-light",
243
+ "difference", "exclusion", "hue", "saturation", "color",
244
+ "luminosity"};
245
+ return names[(int)style.mix_blend_mode];
246
+ }
239
247
  if (property == "z-index") return style.has_z_index ? formatNumber((float)style.z_index) : "auto";
240
248
  if (property == "content") return style.has_content ? quoteText(style.content) : "none";
241
249
  if (property == "clip-path") return formatClipPath(style.clip_path);
@@ -248,9 +256,9 @@ std::string xt_style_property_to_string(const XtStyle &style, const std::string
248
256
  for (const BackgroundLayer &layer : style.background_layers) {
249
257
  if (!out.empty()) out += ", ";
250
258
  if (layer.kind == BackgroundKind::Linear) {
251
- out += "linear-gradient()";
259
+ out += layer.repeating ? "repeating-linear-gradient()" : "linear-gradient()";
252
260
  } else if (layer.kind == BackgroundKind::Radial) {
253
- out += "radial-gradient()";
261
+ out += layer.repeating ? "repeating-radial-gradient()" : "radial-gradient()";
254
262
  } else {
255
263
  out += "url(" + layer.image_src + ")";
256
264
  }
@@ -543,6 +543,16 @@ void xt_style_apply_declaration(XtStyle *style, const std::string &property,
543
543
  if (xt_css_parse_number(value, &number)) style->opacity = number < 0 ? 0 : (number > 1 ? 1 : number);
544
544
  return;
545
545
  }
546
+ if (property == "mix-blend-mode") {
547
+ static const char *const names[] = {
548
+ "normal", "multiply", "screen", "overlay", "darken",
549
+ "lighten", "color-dodge", "color-burn", "hard-light", "soft-light",
550
+ "difference", "exclusion", "hue", "saturation", "color",
551
+ "luminosity"};
552
+ int index = 0;
553
+ if (parseEnum(value, names, 16, &index)) style->mix_blend_mode = (MixBlendMode)index;
554
+ return;
555
+ }
546
556
  if (property == "overflow" || property == "overflow-x" || property == "overflow-y") {
547
557
  static const char *const names[] = {"visible", "hidden", "scroll", "auto"};
548
558
  int index = 0;
@@ -9,4 +9,4 @@
9
9
  * `main`/`master` only (see `.githooks/pre-commit`), so feature branches never
10
10
  * change this line and merging two of them cannot conflict here.
11
11
  */
12
- export const COMPILER_VERSION = "0.5.41";
12
+ export const COMPILER_VERSION = "0.5.43";