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.
- package/dist/src/cli/help.d.ts +1 -1
- package/dist/src/driver/version.d.ts +1 -1
- package/dist/src/driver/version.js +1 -1
- package/dist/tests/e2e/gui-data-url.test.d.ts +10 -0
- package/dist/tests/e2e/gui-data-url.test.js +127 -0
- package/dist/tests/e2e/gui-data-url.test.js.map +1 -0
- package/dist/tests/e2e/gui-effects.test.js +144 -4
- package/dist/tests/e2e/gui-effects.test.js.map +1 -1
- package/dist/tests/e2e/gui-paint.test.js +77 -0
- package/dist/tests/e2e/gui-paint.test.js.map +1 -1
- package/doc/gui/api.md +1 -1
- package/doc/gui/canvas.md +6 -2
- package/doc/gui/known-issues.md +18 -10
- package/doc/gui/media.md +11 -2
- package/doc/gui/rendering.md +48 -19
- package/doc/gui.md +12 -8
- package/doc/zh-CN/gui/api.md +1 -1
- package/doc/zh-CN/gui/canvas.md +6 -4
- package/doc/zh-CN/gui/known-issues.md +12 -8
- package/doc/zh-CN/gui/media.md +9 -2
- package/doc/zh-CN/gui/rendering.md +37 -15
- package/doc/zh-CN/gui.md +7 -6
- package/package.json +7 -7
- package/runtime/ext_gui/dom_api_canvas.cpp +42 -24
- package/runtime/ext_gui/gradient_values.cpp +44 -48
- package/runtime/ext_gui/gui_events.cpp +16 -13
- package/runtime/ext_gui/image.cpp +180 -25
- package/runtime/ext_gui/image.h +6 -4
- package/runtime/ext_gui/paint.cpp +69 -4
- package/runtime/ext_gui/paint.h +57 -1
- package/runtime/ext_gui/paint_background.cpp +197 -41
- package/runtime/ext_gui/paint_effects.cpp +2 -1
- package/runtime/ext_gui/paint_internal.h +5 -1
- package/runtime/ext_gui/renderer.cpp +81 -13
- package/runtime/ext_gui/renderer.h +19 -5
- package/runtime/ext_gui/renderer_gradient.cpp +156 -0
- package/runtime/ext_gui/renderer_image.cpp +6 -1
- package/runtime/ext_gui/renderer_internal.h +48 -0
- package/runtime/ext_gui/renderer_layer.cpp +552 -0
- package/runtime/ext_gui/style.h +29 -1
- package/runtime/ext_gui/style_print.cpp +10 -2
- package/runtime/ext_gui/style_values.cpp +10 -0
- package/src/driver/version.ts +1 -1
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* xbintsc GUI engine — background layer painting.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* gradient is emitted as a run of solid bands
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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.
|
|
9
11
|
*/
|
|
10
12
|
|
|
11
13
|
#include "paint_internal.h"
|
|
12
14
|
|
|
13
15
|
#include <algorithm>
|
|
14
16
|
#include <cmath>
|
|
17
|
+
#include <cstdio>
|
|
18
|
+
#include <memory>
|
|
19
|
+
#include <string>
|
|
15
20
|
#include <vector>
|
|
16
21
|
|
|
17
22
|
namespace xtgui {
|
|
@@ -19,28 +24,112 @@ namespace paint_detail {
|
|
|
19
24
|
|
|
20
25
|
namespace {
|
|
21
26
|
|
|
22
|
-
|
|
23
|
-
|
|
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) {
|
|
24
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
|
+
}
|
|
25
58
|
if (t <= stops.front().offset) return stops.front().color;
|
|
26
59
|
if (t >= stops.back().offset) return stops.back().color;
|
|
27
60
|
for (size_t i = 1; i < stops.size(); i++) {
|
|
28
61
|
if (t > stops[i].offset) continue;
|
|
29
62
|
float span = stops[i].offset - stops[i - 1].offset;
|
|
30
63
|
float u = span > 0.00001f ? (t - stops[i - 1].offset) / span : 0.0f;
|
|
31
|
-
|
|
32
|
-
Color b = stops[i].color;
|
|
33
|
-
Color out;
|
|
34
|
-
out.r = a.r + (b.r - a.r) * u;
|
|
35
|
-
out.g = a.g + (b.g - a.g) * u;
|
|
36
|
-
out.b = a.b + (b.b - a.b) * u;
|
|
37
|
-
out.a = a.a + (b.a - a.a) * u;
|
|
38
|
-
out.valid = true;
|
|
39
|
-
return out;
|
|
64
|
+
return mixGradientStops(stops[i - 1].color, stops[i].color, u);
|
|
40
65
|
}
|
|
41
66
|
return stops.back().color;
|
|
42
67
|
}
|
|
43
68
|
|
|
69
|
+
namespace {
|
|
70
|
+
|
|
71
|
+
/** Resolve the declaration's percentage/length offsets against the gradient
|
|
72
|
+
* line, then distribute the omitted (`Unit::Auto`) ones between their
|
|
73
|
+
* neighbours. The last explicit stop is preserved: for a normal gradient it
|
|
74
|
+
* holds that colour, and for a repeating one it defines the period. */
|
|
75
|
+
std::vector<GradientStop> resolveStops(const BackgroundLayer &layer, float line_length,
|
|
76
|
+
const XtStyle &style, float viewportWidth,
|
|
77
|
+
float viewportHeight) {
|
|
78
|
+
std::vector<GradientStop> stops;
|
|
79
|
+
for (const BackgroundGradientStop &stop : layer.stops) {
|
|
80
|
+
GradientStop resolved;
|
|
81
|
+
resolved.color = stop.color;
|
|
82
|
+
resolved.offset = -1.0f;
|
|
83
|
+
if (stop.offset.unit == Unit::Percent) {
|
|
84
|
+
resolved.offset = stop.offset.value / 100.0f;
|
|
85
|
+
} else if (stop.offset.unit != Unit::Auto && stop.offset.unit != Unit::Invalid &&
|
|
86
|
+
stop.offset.unit != Unit::None) {
|
|
87
|
+
float pixels = resolveLength(stop.offset, 0.0f, style.font_size, viewportWidth,
|
|
88
|
+
viewportHeight);
|
|
89
|
+
resolved.offset = line_length > 0.00001f ? pixels / line_length : 0.0f;
|
|
90
|
+
}
|
|
91
|
+
stops.push_back(resolved);
|
|
92
|
+
}
|
|
93
|
+
if (stops.empty()) return stops;
|
|
94
|
+
|
|
95
|
+
/* Explicit offsets are non-decreasing, as in CSS. */
|
|
96
|
+
float last = -1.0f;
|
|
97
|
+
for (GradientStop &stop : stops) {
|
|
98
|
+
if (stop.offset < 0.0f) continue;
|
|
99
|
+
if (stop.offset < last) stop.offset = last;
|
|
100
|
+
last = stop.offset;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/* Fill implicit offsets by distributing them between their neighbours. */
|
|
104
|
+
size_t i = 0;
|
|
105
|
+
while (i < stops.size()) {
|
|
106
|
+
if (stops[i].offset >= 0.0f) {
|
|
107
|
+
i++;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
size_t j = i;
|
|
111
|
+
while (j < stops.size() && stops[j].offset < 0.0f) j++;
|
|
112
|
+
float before = (i == 0) ? 0.0f : stops[i - 1].offset;
|
|
113
|
+
float after = (j >= stops.size()) ? 1.0f : stops[j].offset;
|
|
114
|
+
size_t count = j - i;
|
|
115
|
+
for (size_t k = 0; k < count; k++) {
|
|
116
|
+
float offset = 0.0f;
|
|
117
|
+
if (i == 0 && j >= stops.size()) {
|
|
118
|
+
offset = count > 1 ? (float)k / (float)(count - 1) : 0.0f;
|
|
119
|
+
} else if (i == 0) {
|
|
120
|
+
offset = after * (float)k / (float)count;
|
|
121
|
+
} else if (j >= stops.size()) {
|
|
122
|
+
offset = before + (1.0f - before) * (float)(k + 1) / (float)count;
|
|
123
|
+
} else {
|
|
124
|
+
offset = before + (after - before) * (float)(k + 1) / (float)(count + 1);
|
|
125
|
+
}
|
|
126
|
+
stops[i + k].offset = offset;
|
|
127
|
+
}
|
|
128
|
+
i = j;
|
|
129
|
+
}
|
|
130
|
+
return stops;
|
|
131
|
+
}
|
|
132
|
+
|
|
44
133
|
/** Intersect `clip` with the box's border box (backgrounds never paint outside
|
|
45
134
|
* it, even when the box is not an overflow container). */
|
|
46
135
|
PaintClip boxClip(const PaintClip &clip, const LayoutBox &box) {
|
|
@@ -75,17 +164,25 @@ PaintClip boxClip(const PaintClip &clip, const LayoutBox &box) {
|
|
|
75
164
|
return out;
|
|
76
165
|
}
|
|
77
166
|
|
|
78
|
-
void paintLinear(const LayoutBox &box, const
|
|
79
|
-
const PaintClip &clip,
|
|
167
|
+
void paintLinear(const LayoutBox &box, const XtStyle &style, const BackgroundLayer &layer,
|
|
168
|
+
const FilterChain &chain, const PaintClip &clip, float viewportWidth,
|
|
169
|
+
float viewportHeight, DisplayList &out) {
|
|
80
170
|
if (layer.stops.size() < 2 || box.width <= 0.0f || box.height <= 0.0f) return;
|
|
81
|
-
|
|
171
|
+
/* Repeating patterns need more samples than one gradient ramp: a 10%
|
|
172
|
+
* stripe is only two bands at the old 18-strip resolution. */
|
|
173
|
+
const int strips = layer.repeating ? 64 : 18;
|
|
82
174
|
bool horizontal = (layer.angle > 80.0f && layer.angle < 100.0f) ||
|
|
83
175
|
(layer.angle > 260.0f && layer.angle < 280.0f);
|
|
84
176
|
bool reverse = (layer.angle > 260.0f && layer.angle < 280.0f) || layer.angle < 10.0f ||
|
|
85
177
|
layer.angle > 350.0f;
|
|
178
|
+
const float line_length = horizontal ? box.width : box.height;
|
|
179
|
+
std::vector<GradientStop> stops = resolveStops(layer, line_length, style, viewportWidth,
|
|
180
|
+
viewportHeight);
|
|
181
|
+
if (stops.size() < 2) return;
|
|
182
|
+
const float period = stops.back().offset;
|
|
86
183
|
for (int i = 0; i < strips; i++) {
|
|
87
184
|
float t = ((float)i + 0.5f) / (float)strips;
|
|
88
|
-
Color color = gradientColorAt(layer, reverse ? 1.0f - t : t);
|
|
185
|
+
Color color = gradientColorAt(stops, layer.repeating, period, reverse ? 1.0f - t : t);
|
|
89
186
|
color = filterColor(chain, color);
|
|
90
187
|
if (color.a <= 0.0f) continue;
|
|
91
188
|
if (horizontal) {
|
|
@@ -98,6 +195,18 @@ void paintLinear(const LayoutBox &box, const BackgroundLayer &layer, const Filte
|
|
|
98
195
|
}
|
|
99
196
|
}
|
|
100
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
|
+
|
|
101
210
|
/** Default radii for `closest-side` / `farthest-side` / corner keywords. */
|
|
102
211
|
void radialRadii(const LayoutBox &box, const BackgroundLayer &layer, const XtStyle &style,
|
|
103
212
|
float viewportWidth, float viewportHeight, float *cx, float *cy, float *rx,
|
|
@@ -151,31 +260,45 @@ void radialRadii(const LayoutBox &box, const BackgroundLayer &layer, const XtSty
|
|
|
151
260
|
*rx = *ry = radius;
|
|
152
261
|
} else {
|
|
153
262
|
switch (layer.size) {
|
|
154
|
-
case RadialSize::ClosestCorner:
|
|
155
263
|
case RadialSize::ClosestSide:
|
|
156
264
|
*rx = std::min(left, right);
|
|
157
265
|
*ry = std::min(top, bottom);
|
|
158
266
|
break;
|
|
159
267
|
case RadialSize::FarthestSide:
|
|
160
|
-
case RadialSize::FarthestCorner:
|
|
161
|
-
default:
|
|
162
268
|
*rx = std::max(left, right);
|
|
163
269
|
*ry = std::max(top, bottom);
|
|
164
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;
|
|
165
280
|
}
|
|
166
281
|
}
|
|
167
282
|
*cx = centerX;
|
|
168
283
|
*cy = centerY;
|
|
169
284
|
}
|
|
170
285
|
|
|
171
|
-
/**
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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;
|
|
179
302
|
}
|
|
180
303
|
|
|
181
304
|
void paintRadial(const LayoutBox &box, const XtStyle &style, const BackgroundLayer &layer,
|
|
@@ -188,16 +311,49 @@ void paintRadial(const LayoutBox &box, const XtStyle &style, const BackgroundLay
|
|
|
188
311
|
float ry = 0.0f;
|
|
189
312
|
radialRadii(box, layer, style, viewportWidth, viewportHeight, &cx, &cy, &rx, &ry);
|
|
190
313
|
if (rx <= 0.0f || ry <= 0.0f) return;
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
314
|
+
std::vector<GradientStop> stops = resolveStops(layer, std::max(rx, ry), style, viewportWidth,
|
|
315
|
+
viewportHeight);
|
|
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
|
+
}
|
|
197
328
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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);
|
|
337
|
+
}
|
|
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);
|
|
201
357
|
}
|
|
202
358
|
|
|
203
359
|
void paintImage(const LayoutBox &box, const BackgroundLayer &layer, const FilterChain &chain,
|
|
@@ -233,7 +389,7 @@ void paintBackgrounds(DisplayList &out, const LayoutBox &box, const XtStyle &sty
|
|
|
233
389
|
for (size_t i = style.background_layers.size(); i-- > 0;) {
|
|
234
390
|
const BackgroundLayer &layer = style.background_layers[i];
|
|
235
391
|
if (layer.kind == BackgroundKind::Linear) {
|
|
236
|
-
paintLinear(box, layer, chain, layerClip, out);
|
|
392
|
+
paintLinear(box, style, layer, chain, layerClip, viewportWidth, viewportHeight, out);
|
|
237
393
|
} else if (layer.kind == BackgroundKind::Radial) {
|
|
238
394
|
paintRadial(box, style, layer, chain, layerClip, viewportWidth, viewportHeight, out);
|
|
239
395
|
} else {
|
|
@@ -169,6 +169,7 @@ Color filterColor(const FilterChain &chain, Color color) {
|
|
|
169
169
|
color.b = clamp01(color.b);
|
|
170
170
|
color.a = clamp01(color.a);
|
|
171
171
|
}
|
|
172
|
+
color.a = clamp01(color.a * chain.opacity);
|
|
172
173
|
return color;
|
|
173
174
|
}
|
|
174
175
|
|
|
@@ -190,7 +191,7 @@ Color imageTint(const FilterChain &chain) {
|
|
|
190
191
|
tint.r = clamp01(tint.r);
|
|
191
192
|
tint.g = clamp01(tint.g);
|
|
192
193
|
tint.b = clamp01(tint.b);
|
|
193
|
-
tint.a = clamp01(tint.a);
|
|
194
|
+
tint.a = clamp01(tint.a * chain.opacity);
|
|
194
195
|
return tint;
|
|
195
196
|
}
|
|
196
197
|
|
|
@@ -25,6 +25,10 @@ namespace paint_detail {
|
|
|
25
25
|
struct FilterChain {
|
|
26
26
|
std::vector<FilterOp> ops; // colour functions (and any unknown no-ops)
|
|
27
27
|
float blur = 0.0f; // resolved px
|
|
28
|
+
/** Product of the CSS `opacity` values on the ancestor chain (including the
|
|
29
|
+
* element itself). It multiplies every painted colour's alpha, which is the
|
|
30
|
+
* per-shape approximation of group opacity. */
|
|
31
|
+
float opacity = 1.0f;
|
|
28
32
|
struct Shadow {
|
|
29
33
|
float dx = 0.0f;
|
|
30
34
|
float dy = 0.0f;
|
|
@@ -33,7 +37,7 @@ struct FilterChain {
|
|
|
33
37
|
};
|
|
34
38
|
std::vector<Shadow> shadows;
|
|
35
39
|
|
|
36
|
-
bool empty() const { return ops.empty() && blur <= 0.0f && shadows.empty(); }
|
|
40
|
+
bool empty() const { return ops.empty() && blur <= 0.0f && shadows.empty() && opacity >= 1.0f; }
|
|
37
41
|
};
|
|
38
42
|
|
|
39
43
|
/** Append `filters` (declared on a box with `style`) to `parent`. */
|
|
@@ -27,6 +27,8 @@ SDL_GPUGraphicsPipeline *g_pipeline = nullptr;
|
|
|
27
27
|
SDL_GPUGraphicsPipeline *g_text_pipeline = nullptr;
|
|
28
28
|
SDL_GPUGraphicsPipeline *g_image_pipeline = nullptr;
|
|
29
29
|
SDL_GPUSampler *g_sampler = nullptr;
|
|
30
|
+
SDL_GPUDevice *g_device = nullptr;
|
|
31
|
+
SDL_GPUTextureFormat g_swapchain_format = SDL_GPU_TEXTUREFORMAT_INVALID;
|
|
30
32
|
int g_renderer_failed = 0;
|
|
31
33
|
bool g_text_ok = false;
|
|
32
34
|
std::unordered_map<std::string, SDL_GPUTexture *> g_image_textures;
|
|
@@ -146,6 +148,8 @@ bool xt_gui_renderer_ensure(SDL_GPUDevice *device, SDL_Window *reference) {
|
|
|
146
148
|
using namespace xtgui::renderer_detail;
|
|
147
149
|
if (g_pipeline != nullptr) return true;
|
|
148
150
|
if (g_renderer_failed) return false;
|
|
151
|
+
g_device = device;
|
|
152
|
+
g_swapchain_format = SDL_GetGPUSwapchainTextureFormat(device, reference);
|
|
149
153
|
const ShaderSource fill_vs = selectShader(device, ShaderProgram::Fill, false);
|
|
150
154
|
const ShaderSource fill_fs = selectShader(device, ShaderProgram::Fill, true);
|
|
151
155
|
if (!fill_vs.valid() || !fill_fs.valid()) {
|
|
@@ -242,6 +246,7 @@ bool xt_gui_renderer_ensure(SDL_GPUDevice *device, SDL_Window *reference) {
|
|
|
242
246
|
|
|
243
247
|
void xt_gui_renderer_destroy(SDL_GPUDevice *device) {
|
|
244
248
|
using namespace xtgui::renderer_detail;
|
|
249
|
+
xtgui::renderer_detail::layerRendererDestroy(device);
|
|
245
250
|
if (g_pipeline != nullptr) {
|
|
246
251
|
SDL_ReleaseGPUGraphicsPipeline(device, g_pipeline);
|
|
247
252
|
g_pipeline = nullptr;
|
|
@@ -275,6 +280,8 @@ void xt_gui_renderer_destroy(SDL_GPUDevice *device) {
|
|
|
275
280
|
g_atlas_full_logged = false;
|
|
276
281
|
g_text_ok = false;
|
|
277
282
|
g_renderer_failed = 0;
|
|
283
|
+
g_device = nullptr;
|
|
284
|
+
g_swapchain_format = SDL_GPU_TEXTUREFORMAT_INVALID;
|
|
278
285
|
}
|
|
279
286
|
|
|
280
287
|
void xt_gui_geometry_destroy(SDL_GPUDevice *device, XtGuiGeometry *geometry) {
|
|
@@ -286,6 +293,13 @@ void xt_gui_geometry_destroy(SDL_GPUDevice *device, XtGuiGeometry *geometry) {
|
|
|
286
293
|
SDL_ReleaseGPUTransferBuffer(device, geometry->staging);
|
|
287
294
|
geometry->staging = nullptr;
|
|
288
295
|
}
|
|
296
|
+
for (SDL_GPUTexture *texture : geometry->scene_textures) {
|
|
297
|
+
if (texture != nullptr) SDL_ReleaseGPUTexture(device, texture);
|
|
298
|
+
}
|
|
299
|
+
geometry->scene_textures.clear();
|
|
300
|
+
geometry->scene_width = 0;
|
|
301
|
+
geometry->scene_height = 0;
|
|
302
|
+
geometry->scene_format = SDL_GPU_TEXTUREFORMAT_INVALID;
|
|
289
303
|
geometry->staging_capacity = 0;
|
|
290
304
|
geometry->capacity_bytes = 0;
|
|
291
305
|
geometry->vertex_count = 0;
|
|
@@ -314,6 +328,15 @@ void xt_gui_build_runs(XtGuiGeometry *shapes, XtGuiGeometry *texts, XtGuiGeometr
|
|
|
314
328
|
shapes->runs.clear();
|
|
315
329
|
uint32_t cursor[3] = {0, 0, 0};
|
|
316
330
|
for (const xtgui::PaintOp &op : list.ops) {
|
|
331
|
+
if (op.kind == xtgui::PaintKind::LayerBegin ||
|
|
332
|
+
op.kind == xtgui::PaintKind::LayerEnd) {
|
|
333
|
+
XtGuiDrawRun marker;
|
|
334
|
+
marker.pipeline = op.kind == xtgui::PaintKind::LayerBegin ? XtGuiDrawRun::LayerBegin
|
|
335
|
+
: XtGuiDrawRun::LayerEnd;
|
|
336
|
+
marker.layer = op.kind == xtgui::PaintKind::LayerBegin ? op.index : 0;
|
|
337
|
+
shapes->runs.push_back(marker);
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
317
340
|
int pipeline = (int)op.kind;
|
|
318
341
|
XtGuiGeometry *geometry = pipeline == 0 ? shapes : (pipeline == 1 ? texts : images);
|
|
319
342
|
if (op.index >= geometry->op_counts.size()) continue;
|
|
@@ -349,17 +372,13 @@ void xt_gui_build_runs(XtGuiGeometry *shapes, XtGuiGeometry *texts, XtGuiGeometr
|
|
|
349
372
|
}
|
|
350
373
|
}
|
|
351
374
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
float viewport_width, float viewport_height, float pixel_density) {
|
|
355
|
-
using namespace xtgui::renderer_detail;
|
|
356
|
-
if (viewport_width <= 0.0f || viewport_height <= 0.0f) return;
|
|
357
|
-
if (pixel_density <= 0.0f) pixel_density = 1.0f;
|
|
358
|
-
float viewport[2] = {viewport_width, viewport_height};
|
|
359
|
-
SDL_PushGPUVertexUniformData(cmd, 0, viewport, sizeof(viewport));
|
|
375
|
+
namespace xtgui {
|
|
376
|
+
namespace renderer_detail {
|
|
360
377
|
|
|
361
|
-
|
|
362
|
-
|
|
378
|
+
void drawRuns(SDL_GPURenderPass *pass, const std::vector<XtGuiDrawRun> &runs, size_t begin,
|
|
379
|
+
size_t end, XtGuiGeometry *shapes, XtGuiGeometry *texts, XtGuiGeometry *images,
|
|
380
|
+
int target_width, int target_height, float pixel_density) {
|
|
381
|
+
if (pass == nullptr) return;
|
|
363
382
|
/* A run list alternates pipelines (background, text, background, …), so the
|
|
364
383
|
* same scissor rectangle and the same pipeline were re-bound for every run.
|
|
365
384
|
* Track what is bound and only issue the state change when it actually
|
|
@@ -389,7 +408,11 @@ void xt_gui_renderer_draw(SDL_GPUCommandBuffer *cmd, SDL_GPURenderPass *pass,
|
|
|
389
408
|
SDL_BindGPUFragmentSamplers(pass, 0, &texture_binding, 1);
|
|
390
409
|
bound_texture = texture;
|
|
391
410
|
};
|
|
392
|
-
for (
|
|
411
|
+
for (size_t i = begin; i < end && i < runs.size(); i++) {
|
|
412
|
+
const XtGuiDrawRun &run = runs[i];
|
|
413
|
+
if (run.pipeline == XtGuiDrawRun::LayerBegin || run.pipeline == XtGuiDrawRun::LayerEnd)
|
|
414
|
+
continue;
|
|
415
|
+
if (run.count == 0) continue;
|
|
393
416
|
SDL_Rect scissor;
|
|
394
417
|
scissor.x = 0;
|
|
395
418
|
scissor.y = 0;
|
|
@@ -426,7 +449,7 @@ void xt_gui_renderer_draw(SDL_GPUCommandBuffer *cmd, SDL_GPURenderPass *pass,
|
|
|
426
449
|
bindTexture(g_atlas_texture);
|
|
427
450
|
SDL_DrawGPUPrimitives(pass, run.count, 1, run.first, 0);
|
|
428
451
|
xtgui::g_perf_frame.drawCalls += 1;
|
|
429
|
-
} else {
|
|
452
|
+
} else if (run.pipeline == XtGuiDrawRun::Image) {
|
|
430
453
|
if (g_image_pipeline == nullptr || g_sampler == nullptr || images->buffer == nullptr ||
|
|
431
454
|
run.texture == nullptr)
|
|
432
455
|
continue;
|
|
@@ -437,7 +460,7 @@ void xt_gui_renderer_draw(SDL_GPUCommandBuffer *cmd, SDL_GPURenderPass *pass,
|
|
|
437
460
|
}
|
|
438
461
|
}
|
|
439
462
|
|
|
440
|
-
/* Restore the full-target scissor so the next
|
|
463
|
+
/* Restore the full-target scissor so the next pass starts clean. */
|
|
441
464
|
SDL_Rect full;
|
|
442
465
|
full.x = 0;
|
|
443
466
|
full.y = 0;
|
|
@@ -445,3 +468,48 @@ void xt_gui_renderer_draw(SDL_GPUCommandBuffer *cmd, SDL_GPURenderPass *pass,
|
|
|
445
468
|
full.h = target_height;
|
|
446
469
|
SDL_SetGPUScissor(pass, &full);
|
|
447
470
|
}
|
|
471
|
+
|
|
472
|
+
} // namespace renderer_detail
|
|
473
|
+
} // namespace xtgui
|
|
474
|
+
|
|
475
|
+
void xt_gui_renderer_draw(SDL_GPUCommandBuffer *cmd, SDL_GPUTexture *swapchain,
|
|
476
|
+
const float clear_color[4], XtGuiGeometry *shapes,
|
|
477
|
+
XtGuiGeometry *texts, XtGuiGeometry *images,
|
|
478
|
+
const xtgui::DisplayList &list, float viewport_width,
|
|
479
|
+
float viewport_height, float pixel_density) {
|
|
480
|
+
using namespace xtgui::renderer_detail;
|
|
481
|
+
if (cmd == nullptr || swapchain == nullptr) return;
|
|
482
|
+
if (viewport_width <= 0.0f || viewport_height <= 0.0f) return;
|
|
483
|
+
if (pixel_density <= 0.0f) pixel_density = 1.0f;
|
|
484
|
+
float viewport[2] = {viewport_width, viewport_height};
|
|
485
|
+
SDL_PushGPUVertexUniformData(cmd, 0, viewport, sizeof(viewport));
|
|
486
|
+
|
|
487
|
+
/* `mix-blend-mode` needs the backdrop as a texture; the blend pipeline is
|
|
488
|
+
* compiled at run time on the SPIR-V/MSL backends. Everywhere else (or if
|
|
489
|
+
* that compilation failed) the ordinary single pass still paints the
|
|
490
|
+
* unblended content rather than dropping it. */
|
|
491
|
+
if (!list.layers.empty() && layerRendererReady(g_device)) {
|
|
492
|
+
if (drawLayeredScene(cmd, swapchain, clear_color, shapes, texts, images, list, viewport_width,
|
|
493
|
+
viewport_height, pixel_density))
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
SDL_GPUColorTargetInfo target;
|
|
498
|
+
memset(&target, 0, sizeof(target));
|
|
499
|
+
target.texture = swapchain;
|
|
500
|
+
target.clear_color.r = clear_color != nullptr ? clear_color[0] : 0.0f;
|
|
501
|
+
target.clear_color.g = clear_color != nullptr ? clear_color[1] : 0.0f;
|
|
502
|
+
target.clear_color.b = clear_color != nullptr ? clear_color[2] : 0.0f;
|
|
503
|
+
target.clear_color.a = clear_color != nullptr ? clear_color[3] : 1.0f;
|
|
504
|
+
target.load_op = SDL_GPU_LOADOP_CLEAR;
|
|
505
|
+
target.store_op = SDL_GPU_STOREOP_STORE;
|
|
506
|
+
SDL_GPURenderPass *pass = SDL_BeginGPURenderPass(cmd, &target, 1, nullptr);
|
|
507
|
+
if (pass == nullptr) return;
|
|
508
|
+
int target_width = (int)(viewport_width * pixel_density + 0.5f);
|
|
509
|
+
int target_height = (int)(viewport_height * pixel_density + 0.5f);
|
|
510
|
+
if (shapes != nullptr && texts != nullptr && images != nullptr) {
|
|
511
|
+
drawRuns(pass, shapes->runs, 0, shapes->runs.size(), shapes, texts, images, target_width,
|
|
512
|
+
target_height, pixel_density);
|
|
513
|
+
}
|
|
514
|
+
SDL_EndGPURenderPass(pass);
|
|
515
|
+
}
|
|
@@ -33,7 +33,9 @@ struct DisplayList;
|
|
|
33
33
|
* `clip_width <= 0` means the whole viewport. For image runs, `texture` is the
|
|
34
34
|
* bound texture. */
|
|
35
35
|
struct XtGuiDrawRun {
|
|
36
|
-
|
|
36
|
+
/** `LayerBegin`/`LayerEnd` are zero-count markers around a
|
|
37
|
+
* `mix-blend-mode` group; `layer` indexes `DisplayList::layers`. */
|
|
38
|
+
enum Pipeline { Shape = 0, Text = 1, Image = 2, LayerBegin = 3, LayerEnd = 4 };
|
|
37
39
|
int pipeline = Shape;
|
|
38
40
|
uint32_t first = 0;
|
|
39
41
|
uint32_t count = 0;
|
|
@@ -42,6 +44,7 @@ struct XtGuiDrawRun {
|
|
|
42
44
|
float clip_width = 0;
|
|
43
45
|
float clip_height = 0;
|
|
44
46
|
SDL_GPUTexture *texture = nullptr;
|
|
47
|
+
uint32_t layer = 0;
|
|
45
48
|
};
|
|
46
49
|
|
|
47
50
|
/** Per-window GPU geometry (vertex buffer + draw range). */
|
|
@@ -63,6 +66,12 @@ struct XtGuiGeometry {
|
|
|
63
66
|
/** Painter-order draw runs, built from the display list by
|
|
64
67
|
* `xt_gui_build_runs` and consumed by `xt_gui_renderer_draw`. */
|
|
65
68
|
std::vector<XtGuiDrawRun> runs;
|
|
69
|
+
/** Offscreen colour targets used by `mix-blend-mode` groups (two per nesting
|
|
70
|
+
* level). Reused across frames and tied to the swapchain size/format. */
|
|
71
|
+
std::vector<SDL_GPUTexture *> scene_textures;
|
|
72
|
+
uint32_t scene_width = 0;
|
|
73
|
+
uint32_t scene_height = 0;
|
|
74
|
+
SDL_GPUTextureFormat scene_format = SDL_GPU_TEXTUREFORMAT_INVALID;
|
|
66
75
|
};
|
|
67
76
|
|
|
68
77
|
/** One GPU command buffer shared by every upload a frame performs (shape, text,
|
|
@@ -91,10 +100,15 @@ void xt_gui_geometry_destroy(SDL_GPUDevice *device, XtGuiGeometry *geometry);
|
|
|
91
100
|
* immediately. */
|
|
92
101
|
void xt_gui_geometry_upload(SDL_GPUDevice *device, XtGuiGeometry *geometry,
|
|
93
102
|
const xtgui::DisplayList &list, XtGuiUploadBatch *batch = nullptr);
|
|
94
|
-
/** Draw `
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
103
|
+
/** Draw the display list into `swapchain`, beginning and ending its own render
|
|
104
|
+
* passes. With `mix-blend-mode` groups the renderer draws them to offscreen
|
|
105
|
+
* textures and composites them with a runtime-compiled blend shader; without
|
|
106
|
+
* groups this is the original single clear-and-draw pass. */
|
|
107
|
+
void xt_gui_renderer_draw(SDL_GPUCommandBuffer *cmd, SDL_GPUTexture *swapchain,
|
|
108
|
+
const float clear_color[4], XtGuiGeometry *shapes,
|
|
109
|
+
XtGuiGeometry *texts, XtGuiGeometry *images,
|
|
110
|
+
const xtgui::DisplayList &list, float viewport_width,
|
|
111
|
+
float viewport_height, float pixel_density);
|
|
98
112
|
/** Shape `list.texts`, ensure their glyphs are in the atlas, and upload the
|
|
99
113
|
* text geometry. `scale` is the device pixel density (1.0 = logical pixels). */
|
|
100
114
|
void xt_gui_text_upload(SDL_GPUDevice *device, XtGuiGeometry *geometry,
|