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.
- 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-audio.test.js +22 -0
- package/dist/tests/e2e/gui-audio.test.js.map +1 -1
- package/dist/tests/e2e/gui-browser.test.js +3 -0
- package/dist/tests/e2e/gui-browser.test.js.map +1 -1
- package/dist/tests/e2e/gui-effects.test.js +48 -4
- package/dist/tests/e2e/gui-effects.test.js.map +1 -1
- package/dist/tests/e2e/gui-events.test.js +67 -0
- package/dist/tests/e2e/gui-events.test.js.map +1 -1
- package/doc/ai/extensions.md +5 -3
- package/doc/ai/zh-CN/extensions.md +4 -3
- package/doc/gui/api.md +13 -5
- package/doc/gui/input.md +7 -0
- package/doc/gui/known-issues.md +5 -3
- package/doc/gui/rendering.md +12 -8
- package/doc/gui-scripts.md +2 -1
- package/doc/gui.md +7 -4
- package/doc/zh-CN/gui/api.md +12 -5
- package/doc/zh-CN/gui/input.md +5 -0
- package/doc/zh-CN/gui/known-issues.md +3 -2
- package/doc/zh-CN/gui/rendering.md +9 -6
- package/doc/zh-CN/gui-scripts.md +2 -2
- package/doc/zh-CN/gui.md +6 -5
- package/package.json +7 -7
- package/runtime/ext_gui/audio.cpp +18 -0
- package/runtime/ext_gui/audio_device.cpp +12 -2
- package/runtime/ext_gui/browser.cpp +2 -2
- package/runtime/ext_gui/dom_api_events.cpp +2 -1
- package/runtime/ext_gui/gui.cpp +7 -0
- package/runtime/ext_gui/gui_engine.h +9 -0
- package/runtime/ext_gui/gui_events.cpp +52 -10
- package/runtime/ext_gui/gui_keymap.cpp +254 -0
- package/runtime/ext_gui/gui_keymap.h +31 -0
- package/runtime/ext_gui/paint.cpp +16 -1
- package/runtime/ext_gui/paint.h +46 -0
- package/runtime/ext_gui/paint_background.cpp +139 -64
- 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 +22 -0
- package/runtime/ext_gui/window.cpp +10 -1
- package/src/driver/version.ts +1 -1
package/runtime/ext_gui/paint.h
CHANGED
|
@@ -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
|
-
*
|
|
5
|
-
* gradient is emitted as a run of solid bands
|
|
6
|
-
* (64 while repeating)
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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<
|
|
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<
|
|
78
|
+
std::vector<GradientStop> stops;
|
|
36
79
|
for (const BackgroundGradientStop &stop : layer.stops) {
|
|
37
|
-
|
|
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 (
|
|
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<
|
|
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
|
-
/**
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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<
|
|
314
|
+
std::vector<GradientStop> stops = resolveStops(layer, std::max(rx, ry), style, viewportWidth,
|
|
271
315
|
viewportHeight);
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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.
|
|
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)) {
|
package/src/driver/version.ts
CHANGED