xbintsc 0.5.42 → 0.5.45

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-audio.test.js +22 -0
  5. package/dist/tests/e2e/gui-audio.test.js.map +1 -1
  6. package/dist/tests/e2e/gui-browser.test.js +3 -0
  7. package/dist/tests/e2e/gui-browser.test.js.map +1 -1
  8. package/dist/tests/e2e/gui-effects.test.js +48 -4
  9. package/dist/tests/e2e/gui-effects.test.js.map +1 -1
  10. package/dist/tests/e2e/gui-events.test.js +67 -0
  11. package/dist/tests/e2e/gui-events.test.js.map +1 -1
  12. package/doc/ai/extensions.md +5 -3
  13. package/doc/ai/zh-CN/extensions.md +4 -3
  14. package/doc/gui/api.md +13 -5
  15. package/doc/gui/input.md +7 -0
  16. package/doc/gui/known-issues.md +5 -3
  17. package/doc/gui/rendering.md +12 -8
  18. package/doc/gui-scripts.md +2 -1
  19. package/doc/gui.md +7 -4
  20. package/doc/zh-CN/gui/api.md +12 -5
  21. package/doc/zh-CN/gui/input.md +5 -0
  22. package/doc/zh-CN/gui/known-issues.md +3 -2
  23. package/doc/zh-CN/gui/rendering.md +9 -6
  24. package/doc/zh-CN/gui-scripts.md +2 -2
  25. package/doc/zh-CN/gui.md +6 -5
  26. package/package.json +7 -7
  27. package/runtime/ext_gui/audio.cpp +18 -0
  28. package/runtime/ext_gui/audio_device.cpp +12 -2
  29. package/runtime/ext_gui/browser.cpp +2 -2
  30. package/runtime/ext_gui/dom_api_events.cpp +2 -1
  31. package/runtime/ext_gui/gui.cpp +7 -0
  32. package/runtime/ext_gui/gui_engine.h +9 -0
  33. package/runtime/ext_gui/gui_events.cpp +52 -10
  34. package/runtime/ext_gui/gui_keymap.cpp +254 -0
  35. package/runtime/ext_gui/gui_keymap.h +31 -0
  36. package/runtime/ext_gui/paint.cpp +16 -1
  37. package/runtime/ext_gui/paint.h +46 -0
  38. package/runtime/ext_gui/paint_background.cpp +139 -64
  39. package/runtime/ext_gui/renderer_gradient.cpp +156 -0
  40. package/runtime/ext_gui/renderer_image.cpp +6 -1
  41. package/runtime/ext_gui/renderer_internal.h +22 -0
  42. package/runtime/ext_gui/window.cpp +10 -1
  43. package/src/driver/version.ts +1 -1
@@ -14,6 +14,7 @@
14
14
  #define XT_GUI_PAINT_H
15
15
 
16
16
  #include <cstdint>
17
+ #include <memory>
17
18
  #include <string>
18
19
  #include <vector>
19
20
 
@@ -24,6 +25,35 @@
24
25
 
25
26
  namespace xtgui {
26
27
 
28
+ /** One resolved colour stop of a gradient. `offset` is a fraction of the
29
+ * gradient line and non-decreasing, as in CSS. */
30
+ struct GradientStop {
31
+ float offset = 0.0f;
32
+ Color color;
33
+ };
34
+
35
+ /** A radial `background-image` layer resolved by the paint pass: the renderer
36
+ * rasterises it into one RGBA texture and draws that as a single image quad.
37
+ *
38
+ * Nesting translucent bands instead composites every band's alpha once per band
39
+ * — each band covers the whole interior — so a vignette whose stops fade to
40
+ * `transparent` turns into an opaque disc that hides the scene behind it.
41
+ * Resolving the ramp per pixel composites it exactly once. */
42
+ struct PaintGradient {
43
+ std::vector<GradientStop> stops;
44
+ /** Centre in box-local logical pixels; the drawn quad spans the whole box. */
45
+ float center_x = 0.0f;
46
+ float center_y = 0.0f;
47
+ float radius_x = 0.0f;
48
+ float radius_y = 0.0f;
49
+ /** `repeating-radial-gradient()`: the stop pattern restarts every `period`
50
+ * (the last stop's offset) instead of clamping at the outer ellipse. */
51
+ bool repeating = false;
52
+ float period = 1.0f;
53
+ /** Identity of the ramp (stops and geometry) for the renderer's texture cache. */
54
+ std::string key;
55
+ };
56
+
27
57
  /** A solid, optionally rounded, rectangle. Coordinates are viewport-relative. */
28
58
  struct PaintRect {
29
59
  float x = 0;
@@ -71,6 +101,10 @@ struct PaintImage {
71
101
  /** Modulating colour for the image shader (white normally; a filter's
72
102
  * multiplicative effect may tint it). */
73
103
  Color tint{1.0f, 1.0f, 1.0f, 1.0f};
104
+ /** A `background-image` radial gradient: the renderer rasterises it into the
105
+ * quad instead of loading `src`/`svg`. Its stops already carry the colour
106
+ * functions of the filter chain; `tint` still carries brightness/opacity. */
107
+ std::shared_ptr<const PaintGradient> gradient;
74
108
  };
75
109
 
76
110
  /** One triangle of a clip path in device-space logical pixels. The paint pass
@@ -144,6 +178,18 @@ struct DisplayList {
144
178
  std::string dump() const;
145
179
  };
146
180
 
181
+ namespace paint_detail {
182
+
183
+ /** Interpolate a gradient's colour at `t` (a fraction of the gradient line; any
184
+ * value when `repeating`). Stops interpolate in premultiplied alpha, as CSS
185
+ * does, so a `transparent` stop does not darken the ramp towards an opaque
186
+ * colour; the result is converted back to straight alpha, which is what both the
187
+ * band and the texture paths blend. */
188
+ Color gradientColorAt(const std::vector<GradientStop> &stops, bool repeating, float period,
189
+ float t);
190
+
191
+ } // namespace paint_detail
192
+
147
193
  /** Build the display list for a laid-out tree and append it to `out`. The
148
194
  * viewport size resolves `vw`/`vh` units inside `clip-path` and shadows; pass 0
149
195
  * to take it from the root box's containing block. */
@@ -1,18 +1,22 @@
1
1
  /*
2
2
  * xbintsc GUI engine — background layer painting.
3
3
  *
4
- * The renderer has one rounded-rectangle pipeline and no gradient shader, so a
5
- * gradient is emitted as a run of solid bands: 18 strips for a linear gradient
6
- * (64 while repeating) and 32 concentric ellipses for a radial one (64 while
7
- * repeating). Radial ellipses use the shape
8
- * pipeline's transform slot (a unit circle scaled to the ellipse radii), which
9
- * keeps the distance-field antialiasing intact.
4
+ * `background-color` is one rounded rectangle and a `background-image` layer is
5
+ * a gradient or an image. A linear gradient is emitted as a run of solid bands
6
+ * (18 strips, 64 while repeating). A radial gradient is emitted as a single
7
+ * `PaintGradient` that the renderer rasterises per pixel
8
+ * (`renderer_gradient.cpp`): stacking translucent radial bands composites their
9
+ * alpha once per band, which turns a vignette whose stops fade to `transparent`
10
+ * into an opaque disc.
10
11
  */
11
12
 
12
13
  #include "paint_internal.h"
13
14
 
14
15
  #include <algorithm>
15
16
  #include <cmath>
17
+ #include <cstdio>
18
+ #include <memory>
19
+ #include <string>
16
20
  #include <vector>
17
21
 
18
22
  namespace xtgui {
@@ -20,21 +24,60 @@ namespace paint_detail {
20
24
 
21
25
  namespace {
22
26
 
23
- struct ResolvedStop {
24
- float offset = 0.0f;
25
- Color color;
26
- };
27
+ /** Blend two stops in premultiplied alpha, as CSS specifies, and convert the
28
+ * result back to the straight alpha both pipelines blend. Without it a
29
+ * `transparent` stop (rgba(0,0,0,0)) drags the ramp towards black, so a
30
+ * `transparent -> white` gradient fades through grey. */
31
+ Color mixGradientStops(const Color &from, const Color &to, float amount) {
32
+ float alpha = from.a + (to.a - from.a) * amount;
33
+ Color out;
34
+ out.a = alpha;
35
+ if (alpha > 0.00001f) {
36
+ out.r = (from.r * from.a + (to.r * to.a - from.r * from.a) * amount) / alpha;
37
+ out.g = (from.g * from.a + (to.g * to.a - from.g * from.a) * amount) / alpha;
38
+ out.b = (from.b * from.a + (to.b * to.a - from.b * from.a) * amount) / alpha;
39
+ } else {
40
+ out.r = from.r;
41
+ out.g = from.g;
42
+ out.b = from.b;
43
+ }
44
+ out.valid = true;
45
+ return out;
46
+ }
47
+
48
+ } // namespace
49
+
50
+ Color gradientColorAt(const std::vector<GradientStop> &stops, bool repeating, float period,
51
+ float t) {
52
+ if (stops.empty()) return Color();
53
+ /* A repeating gradient restarts its stop pattern at the last stop's offset. */
54
+ if (repeating) {
55
+ if (period <= 0.00001f) return stops.back().color;
56
+ t -= std::floor(t / period) * period;
57
+ }
58
+ if (t <= stops.front().offset) return stops.front().color;
59
+ if (t >= stops.back().offset) return stops.back().color;
60
+ for (size_t i = 1; i < stops.size(); i++) {
61
+ if (t > stops[i].offset) continue;
62
+ float span = stops[i].offset - stops[i - 1].offset;
63
+ float u = span > 0.00001f ? (t - stops[i - 1].offset) / span : 0.0f;
64
+ return mixGradientStops(stops[i - 1].color, stops[i].color, u);
65
+ }
66
+ return stops.back().color;
67
+ }
68
+
69
+ namespace {
27
70
 
28
71
  /** Resolve the declaration's percentage/length offsets against the gradient
29
72
  * line, then distribute the omitted (`Unit::Auto`) ones between their
30
73
  * neighbours. The last explicit stop is preserved: for a normal gradient it
31
74
  * holds that colour, and for a repeating one it defines the period. */
32
- std::vector<ResolvedStop> resolveStops(const BackgroundLayer &layer, float line_length,
75
+ std::vector<GradientStop> resolveStops(const BackgroundLayer &layer, float line_length,
33
76
  const XtStyle &style, float viewportWidth,
34
77
  float viewportHeight) {
35
- std::vector<ResolvedStop> stops;
78
+ std::vector<GradientStop> stops;
36
79
  for (const BackgroundGradientStop &stop : layer.stops) {
37
- ResolvedStop resolved;
80
+ GradientStop resolved;
38
81
  resolved.color = stop.color;
39
82
  resolved.offset = -1.0f;
40
83
  if (stop.offset.unit == Unit::Percent) {
@@ -51,7 +94,7 @@ std::vector<ResolvedStop> resolveStops(const BackgroundLayer &layer, float line_
51
94
 
52
95
  /* Explicit offsets are non-decreasing, as in CSS. */
53
96
  float last = -1.0f;
54
- for (ResolvedStop &stop : stops) {
97
+ for (GradientStop &stop : stops) {
55
98
  if (stop.offset < 0.0f) continue;
56
99
  if (stop.offset < last) stop.offset = last;
57
100
  last = stop.offset;
@@ -87,33 +130,6 @@ std::vector<ResolvedStop> resolveStops(const BackgroundLayer &layer, float line_
87
130
  return stops;
88
131
  }
89
132
 
90
- Color gradientColorAt(const std::vector<ResolvedStop> &stops, bool repeating, float t) {
91
- if (stops.empty()) return Color();
92
- /* A repeating gradient restarts its stop pattern at the last stop's offset. */
93
- if (repeating) {
94
- float period = stops.back().offset;
95
- if (period <= 0.00001f) return stops.back().color;
96
- t -= std::floor(t / period) * period;
97
- }
98
- if (t <= stops.front().offset) return stops.front().color;
99
- if (t >= stops.back().offset) return stops.back().color;
100
- for (size_t i = 1; i < stops.size(); i++) {
101
- if (t > stops[i].offset) continue;
102
- float span = stops[i].offset - stops[i - 1].offset;
103
- float u = span > 0.00001f ? (t - stops[i - 1].offset) / span : 0.0f;
104
- Color a = stops[i - 1].color;
105
- Color b = stops[i].color;
106
- Color out;
107
- out.r = a.r + (b.r - a.r) * u;
108
- out.g = a.g + (b.g - a.g) * u;
109
- out.b = a.b + (b.b - a.b) * u;
110
- out.a = a.a + (b.a - a.a) * u;
111
- out.valid = true;
112
- return out;
113
- }
114
- return stops.back().color;
115
- }
116
-
117
133
  /** Intersect `clip` with the box's border box (backgrounds never paint outside
118
134
  * it, even when the box is not an overflow container). */
119
135
  PaintClip boxClip(const PaintClip &clip, const LayoutBox &box) {
@@ -160,11 +176,13 @@ void paintLinear(const LayoutBox &box, const XtStyle &style, const BackgroundLay
160
176
  bool reverse = (layer.angle > 260.0f && layer.angle < 280.0f) || layer.angle < 10.0f ||
161
177
  layer.angle > 350.0f;
162
178
  const float line_length = horizontal ? box.width : box.height;
163
- std::vector<ResolvedStop> stops = resolveStops(layer, line_length, style, viewportWidth,
179
+ std::vector<GradientStop> stops = resolveStops(layer, line_length, style, viewportWidth,
164
180
  viewportHeight);
181
+ if (stops.size() < 2) return;
182
+ const float period = stops.back().offset;
165
183
  for (int i = 0; i < strips; i++) {
166
184
  float t = ((float)i + 0.5f) / (float)strips;
167
- Color color = gradientColorAt(stops, layer.repeating, reverse ? 1.0f - t : t);
185
+ Color color = gradientColorAt(stops, layer.repeating, period, reverse ? 1.0f - t : t);
168
186
  color = filterColor(chain, color);
169
187
  if (color.a <= 0.0f) continue;
170
188
  if (horizontal) {
@@ -177,6 +195,18 @@ void paintLinear(const LayoutBox &box, const XtStyle &style, const BackgroundLay
177
195
  }
178
196
  }
179
197
 
198
+ /** Radii of a keyword-sized ellipse: it keeps the box's aspect ratio and is
199
+ * scaled until it meets the corner at `(px, py)` from the centre, so the 100%
200
+ * stop lands on that corner as it does in CSS. Taking the side distances instead
201
+ * left the corners of a `farthest-corner` gradient unpainted. */
202
+ void cornerEllipseRadii(float width, float height, float px, float py, float *rx, float *ry) {
203
+ float x = 2.0f * px / width;
204
+ float y = 2.0f * py / height;
205
+ float scale = std::sqrt(x * x + y * y);
206
+ *rx = scale * width * 0.5f;
207
+ *ry = scale * height * 0.5f;
208
+ }
209
+
180
210
  /** Default radii for `closest-side` / `farthest-side` / corner keywords. */
181
211
  void radialRadii(const LayoutBox &box, const BackgroundLayer &layer, const XtStyle &style,
182
212
  float viewportWidth, float viewportHeight, float *cx, float *cy, float *rx,
@@ -230,31 +260,45 @@ void radialRadii(const LayoutBox &box, const BackgroundLayer &layer, const XtSty
230
260
  *rx = *ry = radius;
231
261
  } else {
232
262
  switch (layer.size) {
233
- case RadialSize::ClosestCorner:
234
263
  case RadialSize::ClosestSide:
235
264
  *rx = std::min(left, right);
236
265
  *ry = std::min(top, bottom);
237
266
  break;
238
267
  case RadialSize::FarthestSide:
239
- case RadialSize::FarthestCorner:
240
- default:
241
268
  *rx = std::max(left, right);
242
269
  *ry = std::max(top, bottom);
243
270
  break;
271
+ case RadialSize::ClosestCorner:
272
+ cornerEllipseRadii(box.width, box.height, std::min(left, right), std::min(top, bottom), rx,
273
+ ry);
274
+ break;
275
+ case RadialSize::FarthestCorner:
276
+ default:
277
+ cornerEllipseRadii(box.width, box.height, std::max(left, right), std::max(top, bottom), rx,
278
+ ry);
279
+ break;
244
280
  }
245
281
  }
246
282
  *cx = centerX;
247
283
  *cy = centerY;
248
284
  }
249
285
 
250
- /** One filled ellipse: a unit-radius rounded rect mapped by a local scale. */
251
- void addEllipse(DisplayList &out, float cx, float cy, float rx, float ry, const Color &color,
252
- const PaintClip &clip, const Matrix2D &world) {
253
- if (rx <= 0.0f || ry <= 0.0f || color.a <= 0.0f) return;
254
- Matrix2D local = Matrix2D::translate(cx, cy)
255
- .multiply(Matrix2D::scale(rx, ry))
256
- .multiply(Matrix2D::translate(-cx, -cy));
257
- addRect(out, cx - 1.0f, cy - 1.0f, 2.0f, 2.0f, 1.0f, color, clip, world.multiply(local));
286
+ /** Stable identity of a rasterised gradient: the ramp plus its geometry. The
287
+ * renderer keys its texture cache by this string, so an unchanged declaration
288
+ * rasterises once. */
289
+ std::string gradientKey(const PaintGradient &gradient) {
290
+ char buffer[192];
291
+ std::string key;
292
+ for (const GradientStop &stop : gradient.stops) {
293
+ std::snprintf(buffer, sizeof(buffer), "%.4f/%.5f,%.5f,%.5f,%.5f;", stop.offset, stop.color.r,
294
+ stop.color.g, stop.color.b, stop.color.a);
295
+ key += buffer;
296
+ }
297
+ std::snprintf(buffer, sizeof(buffer), "%.3f,%.3f,%.3f,%.3f;%d;%.5f", gradient.center_x,
298
+ gradient.center_y, gradient.radius_x, gradient.radius_y,
299
+ gradient.repeating ? 1 : 0, gradient.period);
300
+ key += buffer;
301
+ return key;
258
302
  }
259
303
 
260
304
  void paintRadial(const LayoutBox &box, const XtStyle &style, const BackgroundLayer &layer,
@@ -267,18 +311,49 @@ void paintRadial(const LayoutBox &box, const XtStyle &style, const BackgroundLay
267
311
  float ry = 0.0f;
268
312
  radialRadii(box, layer, style, viewportWidth, viewportHeight, &cx, &cy, &rx, &ry);
269
313
  if (rx <= 0.0f || ry <= 0.0f) return;
270
- std::vector<ResolvedStop> stops = resolveStops(layer, std::max(rx, ry), style, viewportWidth,
314
+ std::vector<GradientStop> stops = resolveStops(layer, std::max(rx, ry), style, viewportWidth,
271
315
  viewportHeight);
272
- const int bands = layer.repeating ? 64 : 32;
273
- for (int i = bands; i >= 1; i--) {
274
- float t = (float)i / (float)bands;
275
- Color color = filterColor(chain, gradientColorAt(stops, layer.repeating, t));
276
- if (color.a <= 0.0f) continue;
277
- addEllipse(out, cx, cy, rx * t, ry * t, color, clip, box.world);
316
+ if (stops.size() < 2) return;
317
+ /* The colour functions fold into the stops (a per-stop application is exact
318
+ * for the linear ones); `brightness()` and `opacity()` stay out, because the
319
+ * image tint carries them exactly and leaving them out keeps the ramp — and
320
+ * therefore the texture cache key — stable while a fade animates. */
321
+ FilterChain stopChain = chain;
322
+ stopChain.opacity = 1.0f;
323
+ for (size_t i = stopChain.ops.size(); i-- > 0;) {
324
+ FilterKind kind = stopChain.ops[i].kind;
325
+ if (kind == FilterKind::Brightness || kind == FilterKind::Opacity) {
326
+ stopChain.ops.erase(stopChain.ops.begin() + (ptrdiff_t)i);
327
+ }
328
+ }
329
+ auto gradient = std::make_shared<PaintGradient>();
330
+ gradient->stops.reserve(stops.size());
331
+ bool visible = false;
332
+ for (size_t i = 0; i < stops.size(); i++) {
333
+ GradientStop resolved = stops[i];
334
+ resolved.color = filterColor(stopChain, resolved.color);
335
+ visible = visible || resolved.color.a > 0.0f;
336
+ gradient->stops.push_back(resolved);
278
337
  }
279
- /* Exact centre colour (the innermost band samples just off zero). */
280
- Color center = filterColor(chain, gradientColorAt(stops, layer.repeating, 0.0f));
281
- addEllipse(out, cx, cy, 0.6f, 0.6f, center, clip, box.world);
338
+ if (!visible) return;
339
+ gradient->center_x = cx - box.x;
340
+ gradient->center_y = cy - box.y;
341
+ gradient->radius_x = rx;
342
+ gradient->radius_y = ry;
343
+ gradient->repeating = layer.repeating;
344
+ gradient->period = stops.back().offset;
345
+ gradient->key = gradientKey(*gradient);
346
+
347
+ /* One quad for the whole layer: the renderer resolves the ramp per pixel, so
348
+ * the layer composites exactly once instead of once per translucent band. */
349
+ PaintImage image;
350
+ image.x = box.x;
351
+ image.y = box.y;
352
+ image.width = box.width;
353
+ image.height = box.height;
354
+ image.transform = box.world;
355
+ image.gradient = gradient;
356
+ addImage(out, std::move(image), clip, chain);
282
357
  }
283
358
 
284
359
  void paintImage(const LayoutBox &box, const BackgroundLayer &layer, const FilterChain &chain,
@@ -0,0 +1,156 @@
1
+ /*
2
+ * xbintsc GUI engine — radial `background-image` rasterisation.
3
+ *
4
+ * The shape pipeline fills one shape with one flat colour, and the bands a
5
+ * radial gradient used to be drawn as are nested ellipses: every band covers the
6
+ * whole interior, so each band's alpha composites over the previous ones and a
7
+ * vignette whose stops fade to `transparent` ends up opaque. The ramp is
8
+ * therefore resolved per pixel into an RGBA texture, which the image pipeline
9
+ * draws as one quad and composites exactly once.
10
+ */
11
+
12
+ #include "renderer_internal.h"
13
+
14
+ #include <algorithm>
15
+ #include <cmath>
16
+ #include <cstdio>
17
+ #include <string>
18
+ #include <unordered_map>
19
+
20
+ namespace xtgui {
21
+ namespace renderer_detail {
22
+
23
+ namespace {
24
+
25
+ /** A rasterised gradient, keyed by the paint pass's cache key plus the logical
26
+ * box size. The texture is re-rasterised in place when the device pixel size
27
+ * changes (a DPI change), so a resized gradient never leaves a stale texture. */
28
+ struct GradientTexture {
29
+ SDL_GPUTexture *texture = nullptr;
30
+ int width = 0;
31
+ int height = 0;
32
+ /** Set by the upload pass that draws this gradient; see `finishGradientUpload`. */
33
+ bool used = false;
34
+ };
35
+
36
+ std::unordered_map<std::string, GradientTexture> g_gradient_textures;
37
+
38
+ /** How many rasterised gradients that are no longer drawn are kept. A gradient's
39
+ * stops can animate (a `hue-rotate` loop, say), which would otherwise rasterise a
40
+ * fresh texture every frame and never free the old ones. */
41
+ constexpr size_t kMaxStaleGradients = 8;
42
+
43
+ /** Largest texture edge a gradient is rasterised at. A radial ramp is smooth, so
44
+ * a larger box is rasterised smaller and stretched: the sampled colours are
45
+ * unchanged (the stretch is uniform), only the resolution of the ramp drops. */
46
+ constexpr int kMaxGradientEdge = 1024;
47
+
48
+ unsigned char byteOf(float value) {
49
+ if (!(value > 0.0f)) return 0; // NaN and negatives
50
+ if (value > 1.0f) value = 1.0f;
51
+ return (unsigned char)(value * 255.0f + 0.5f);
52
+ }
53
+
54
+ /** Rasterise `gradient` into `out`: the box maps 1:1 onto the texture. The ramp
55
+ * covers the whole box — past the ending shape CSS keeps the last stop's colour
56
+ * rather than fading out — so there is no shape edge to antialias; the texels
57
+ * are sampled with linear filtering. */
58
+ void rasterizeGradient(const PaintGradient &gradient, float logical_width, float logical_height,
59
+ int pixel_width, int pixel_height, Image *out) {
60
+ out->width = pixel_width;
61
+ out->height = pixel_height;
62
+ out->pixels.assign((size_t)pixel_width * (size_t)pixel_height * 4u, 0);
63
+ const float pixel_x = logical_width / (float)pixel_width; // logical px per texel
64
+ const float pixel_y = logical_height / (float)pixel_height;
65
+ const float inv_rx = 1.0f / gradient.radius_x;
66
+ const float inv_ry = 1.0f / gradient.radius_y;
67
+ for (int y = 0; y < pixel_height; y++) {
68
+ const float local_y = ((float)y + 0.5f) * pixel_y;
69
+ unsigned char *row = out->pixels.data() + (size_t)y * (size_t)pixel_width * 4u;
70
+ for (int x = 0; x < pixel_width; x++) {
71
+ const float local_x = ((float)x + 0.5f) * pixel_x;
72
+ const float u = (local_x - gradient.center_x) * inv_rx;
73
+ const float v = (local_y - gradient.center_y) * inv_ry;
74
+ const float t = std::sqrt(u * u + v * v);
75
+ Color color =
76
+ paint_detail::gradientColorAt(gradient.stops, gradient.repeating, gradient.period, t);
77
+ if (color.a <= 0.0f) continue;
78
+ unsigned char *texel = row + (size_t)x * 4u;
79
+ texel[0] = byteOf(color.r);
80
+ texel[1] = byteOf(color.g);
81
+ texel[2] = byteOf(color.b);
82
+ texel[3] = byteOf(color.a);
83
+ }
84
+ }
85
+ }
86
+
87
+ /** The device size a gradient of `pixel_width` x `pixel_height` is rasterised at,
88
+ * preserving its aspect ratio under `kMaxGradientEdge`. */
89
+ void rasterSize(int pixel_width, int pixel_height, int *width, int *height) {
90
+ *width = std::max(1, pixel_width);
91
+ *height = std::max(1, pixel_height);
92
+ if (*width <= kMaxGradientEdge && *height <= kMaxGradientEdge) return;
93
+ float shrink = (float)kMaxGradientEdge / (float)std::max(*width, *height);
94
+ *width = std::max(1, (int)((float)*width * shrink + 0.5f));
95
+ *height = std::max(1, (int)((float)*height * shrink + 0.5f));
96
+ }
97
+
98
+ } // namespace
99
+
100
+ void beginGradientUpload() {
101
+ for (auto &entry : g_gradient_textures) entry.second.used = false;
102
+ }
103
+
104
+ void finishGradientUpload(SDL_GPUDevice *device) {
105
+ if (g_gradient_textures.size() <= kMaxStaleGradients) return;
106
+ /* The display list of the frame being built may hold pointers to the textures
107
+ * it used, so only gradients it did *not* use can go. */
108
+ for (auto it = g_gradient_textures.begin(); it != g_gradient_textures.end();) {
109
+ if (it->second.used) {
110
+ ++it;
111
+ continue;
112
+ }
113
+ if (it->second.texture != nullptr) SDL_ReleaseGPUTexture(device, it->second.texture);
114
+ it = g_gradient_textures.erase(it);
115
+ }
116
+ }
117
+
118
+ SDL_GPUTexture *ensureGradientTexture(SDL_GPUDevice *device, const xtgui::PaintImage &image,
119
+ int pixel_width, int pixel_height, XtGuiUploadBatch *batch) {
120
+ if (image.gradient == nullptr || image.width <= 0.0f || image.height <= 0.0f) return nullptr;
121
+ const xtgui::PaintGradient &gradient = *image.gradient;
122
+ if (gradient.radius_x <= 0.0f || gradient.radius_y <= 0.0f || gradient.stops.size() < 2) {
123
+ return nullptr;
124
+ }
125
+ int width = 0;
126
+ int height = 0;
127
+ rasterSize(pixel_width, pixel_height, &width, &height);
128
+ /* The key carries the ramp, its geometry and the logical box size: two boxes
129
+ * of different sizes can resolve the same px radius but not the same picture. */
130
+ char box[64];
131
+ std::snprintf(box, sizeof(box), "@%.3f,%.3f", image.width, image.height);
132
+ std::string key = gradient.key + box;
133
+ GradientTexture &entry = g_gradient_textures[key];
134
+ entry.used = true;
135
+ if (entry.texture != nullptr && entry.width == width && entry.height == height) {
136
+ return entry.texture;
137
+ }
138
+ Image raster;
139
+ rasterizeGradient(gradient, image.width, image.height, width, height, &raster);
140
+ SDL_GPUTexture *texture = createImageTexture(device, raster, batch);
141
+ if (entry.texture != nullptr) SDL_ReleaseGPUTexture(device, entry.texture);
142
+ entry.texture = texture;
143
+ entry.width = texture != nullptr ? width : 0;
144
+ entry.height = texture != nullptr ? height : 0;
145
+ return texture;
146
+ }
147
+
148
+ void releaseGradientTextures(SDL_GPUDevice *device) {
149
+ for (auto &entry : g_gradient_textures) {
150
+ if (entry.second.texture != nullptr) SDL_ReleaseGPUTexture(device, entry.second.texture);
151
+ }
152
+ g_gradient_textures.clear();
153
+ }
154
+
155
+ } // namespace renderer_detail
156
+ } // namespace xtgui
@@ -379,6 +379,7 @@ void xt_gui_image_upload(SDL_GPUDevice *device, XtGuiGeometry *geometry,
379
379
  full.v0 = 0.0f;
380
380
  full.u1 = 1.0f;
381
381
  full.v1 = 1.0f;
382
+ beginGradientUpload();
382
383
  for (const xtgui::PaintOp &op : list.ops) {
383
384
  if (op.kind != xtgui::PaintKind::Image || op.index >= list.images.size()) continue;
384
385
  const xtgui::PaintImage &image = list.images[op.index];
@@ -390,7 +391,9 @@ void xt_gui_image_upload(SDL_GPUDevice *device, XtGuiGeometry *geometry,
390
391
  if (pixel_width < 1) pixel_width = 1;
391
392
  if (pixel_height < 1) pixel_height = 1;
392
393
  SDL_GPUTexture *texture = nullptr;
393
- if (image.canvas_id != 0) {
394
+ if (image.gradient != nullptr) {
395
+ texture = ensureGradientTexture(device, image, pixel_width, pixel_height, batch);
396
+ } else if (image.canvas_id != 0) {
394
397
  texture = ensureCanvasTexture(device, image.canvas_id, batch);
395
398
  } else if (image.media_id != 0) {
396
399
  texture = ensureMediaTexture(device, image.media_id, batch);
@@ -405,11 +408,13 @@ void xt_gui_image_upload(SDL_GPUDevice *device, XtGuiGeometry *geometry,
405
408
  geometry->op_counts[op.index] = count;
406
409
  geometry->op_textures[op.index] = texture;
407
410
  }
411
+ finishGradientUpload(device);
408
412
  uploadVertices(device, geometry, verts.data(), (uint32_t)verts.size(), sizeof(TextVertex), batch);
409
413
  }
410
414
 
411
415
  void xt_gui_image_textures_destroy(SDL_GPUDevice *device) {
412
416
  using namespace xtgui::renderer_detail;
417
+ releaseGradientTextures(device);
413
418
  for (auto &entry : g_image_textures) {
414
419
  if (entry.second != nullptr) SDL_ReleaseGPUTexture(device, entry.second);
415
420
  }
@@ -14,6 +14,7 @@
14
14
  #include <unordered_map>
15
15
  #include <vector>
16
16
 
17
+ #include "image.h"
17
18
  #include "paint.h"
18
19
  #include "renderer.h"
19
20
 
@@ -147,6 +148,10 @@ void layerRendererDestroy(SDL_GPUDevice *device);
147
148
  /* -- images --------------------------------------------------------------- */
148
149
 
149
150
  SDL_GPUGraphicsPipeline *createImagePipeline(SDL_GPUDevice *device, SDL_Window *reference);
151
+ /** Create an RGBA texture holding `image` and upload its pixels. Returns null
152
+ * (after reporting the SDL error) when either step fails. */
153
+ SDL_GPUTexture *createImageTexture(SDL_GPUDevice *device, const xtgui::Image &image,
154
+ XtGuiUploadBatch *batch);
150
155
  /** Decode `src` (cached) and upload it as an RGBA texture. `pixel_width` /
151
156
  * `pixel_height` are the drawn size in device pixels; vector sources (SVG) are
152
157
  * rasterised at that size, raster sources ignore it. The texture cache is keyed
@@ -154,6 +159,23 @@ SDL_GPUGraphicsPipeline *createImagePipeline(SDL_GPUDevice *device, SDL_Window *
154
159
  SDL_GPUTexture *ensureImageTexture(SDL_GPUDevice *device, const std::string &src, int pixel_width,
155
160
  int pixel_height, XtGuiUploadBatch *batch);
156
161
 
162
+ /* -- background gradients ------------------------------------------------- */
163
+
164
+ /** Rasterise a `PaintImage` radial gradient into an RGBA texture and cache it by
165
+ * the paint pass's key. The ramp is resolved per device pixel, so a translucent
166
+ * gradient composites once instead of once per band. Bracket a frame's uploads
167
+ * with `beginGradientUpload`/`finishGradientUpload` so textures the frame no
168
+ * longer draws can be reclaimed; returns null when it cannot be created. */
169
+ SDL_GPUTexture *ensureGradientTexture(SDL_GPUDevice *device, const xtgui::PaintImage &image,
170
+ int pixel_width, int pixel_height, XtGuiUploadBatch *batch);
171
+ /** Mark every cached gradient unused; `ensureGradientTexture` marks the ones the
172
+ * upload pass draws. */
173
+ void beginGradientUpload();
174
+ /** Release cached gradients the pass did not draw, keeping the cache bounded. */
175
+ void finishGradientUpload(SDL_GPUDevice *device);
176
+ /** Release every rasterised gradient texture. */
177
+ void releaseGradientTextures(SDL_GPUDevice *device);
178
+
157
179
  } // namespace renderer_detail
158
180
  } // namespace xtgui
159
181
 
@@ -413,7 +413,8 @@ static xt_value win_hit_test(xt_value self, xt_value env, int32_t argc, xt_value
413
413
  /** Test/debug hook: synthesise an input event and deliver it to handlers.
414
414
  * `win.sendEvent("click", { x, y, button, clicks })`,
415
415
  * `win.sendEvent("wheel", { x, y, deltaX, deltaY })` or
416
- * `win.sendEvent("keydown", { key, code })`. */
416
+ * `win.sendEvent("keydown", { key, code })`. A key event may instead name the
417
+ * SDL `scancode`/`keycode` plus modifiers to exercise a real-keyboard path. */
417
418
  static xt_value win_send_event(xt_value self, xt_value env, int32_t argc, xt_value *argv) {
418
419
  (void)env;
419
420
  XtGuiWindow *win = xt_gui_window_from_this(self);
@@ -439,6 +440,14 @@ static xt_value win_send_event(xt_value self, xt_value env, int32_t argc, xt_val
439
440
  return XT_TRUE;
440
441
  }
441
442
  if (name == "keydown" || name == "keyup") {
443
+ if (XT_IS_OBJECT(options) && XT_IS_NUMBER(xt_object_get_cstr(options, "scancode"))) {
444
+ int scancode = (int)xt_gui_number_or(xt_object_get_cstr(options, "scancode"), 0);
445
+ int keycode = (int)xt_gui_number_or(xt_object_get_cstr(options, "keycode"), 0);
446
+ auto flag = [options](const char *key) { return xt_truthy(xt_object_get_cstr(options, key)); };
447
+ xt_gui_dispatch_physical_key(win, type, scancode, keycode, flag("repeat"), flag("ctrl"),
448
+ flag("shift"), flag("alt"), flag("meta"), flag("caps"));
449
+ return XT_TRUE;
450
+ }
442
451
  const char *key = "";
443
452
  const char *code = "";
444
453
  if (XT_IS_OBJECT(options)) {
@@ -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.42";
12
+ export const COMPILER_VERSION = "0.5.45";