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
|
@@ -471,23 +471,26 @@ void xt_gui_render_window(XtGuiWindow *win) {
|
|
|
471
471
|
}
|
|
472
472
|
win->present_deferred = 0;
|
|
473
473
|
xtgui::g_perf_frame.presents += 1;
|
|
474
|
-
SDL_GPUColorTargetInfo target;
|
|
475
|
-
memset(&target, 0, sizeof(target));
|
|
476
|
-
target.texture = swapchain;
|
|
477
|
-
target.clear_color.r = win->background[0];
|
|
478
|
-
target.clear_color.g = win->background[1];
|
|
479
|
-
target.clear_color.b = win->background[2];
|
|
480
|
-
target.clear_color.a = win->background[3];
|
|
481
|
-
target.load_op = SDL_GPU_LOADOP_CLEAR;
|
|
482
|
-
target.store_op = SDL_GPU_STOREOP_STORE;
|
|
483
|
-
SDL_GPURenderPass *pass = SDL_BeginGPURenderPass(cmd, &target, 1, NULL);
|
|
484
474
|
if (can_paint) {
|
|
485
475
|
float density = SDL_GetWindowPixelDensity(win->window);
|
|
486
476
|
if (density <= 0.0f) density = 1.0f;
|
|
487
|
-
xt_gui_renderer_draw(cmd,
|
|
488
|
-
&win->
|
|
477
|
+
xt_gui_renderer_draw(cmd, swapchain, win->background, &win->geometry,
|
|
478
|
+
&win->text_geometry, &win->image_geometry,
|
|
479
|
+
win->display_list, viewport_width, viewport_height, density);
|
|
480
|
+
} else {
|
|
481
|
+
/* No renderer (no usable shader backend): still clear the window. */
|
|
482
|
+
SDL_GPUColorTargetInfo target;
|
|
483
|
+
memset(&target, 0, sizeof(target));
|
|
484
|
+
target.texture = swapchain;
|
|
485
|
+
target.clear_color.r = win->background[0];
|
|
486
|
+
target.clear_color.g = win->background[1];
|
|
487
|
+
target.clear_color.b = win->background[2];
|
|
488
|
+
target.clear_color.a = win->background[3];
|
|
489
|
+
target.load_op = SDL_GPU_LOADOP_CLEAR;
|
|
490
|
+
target.store_op = SDL_GPU_STOREOP_STORE;
|
|
491
|
+
SDL_GPURenderPass *pass = SDL_BeginGPURenderPass(cmd, &target, 1, NULL);
|
|
492
|
+
if (pass != NULL) SDL_EndGPURenderPass(pass);
|
|
489
493
|
}
|
|
490
|
-
SDL_EndGPURenderPass(pass);
|
|
491
494
|
firstFrame = !win->ready;
|
|
492
495
|
win->ready = 1;
|
|
493
496
|
} else {
|
|
@@ -64,13 +64,84 @@ std::string normalizePath(const std::string &input) {
|
|
|
64
64
|
return out;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
/**
|
|
68
|
-
*
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
67
|
+
/** One classified `data:` URL: whether its payload is SVG and the intrinsic
|
|
68
|
+
* size parsed from it (or from the raster header). Classification happens once
|
|
69
|
+
* per URL and is cached, so the renderer's vector probe does not decode the
|
|
70
|
+
* payload on every frame. */
|
|
71
|
+
struct DataUrlEntry {
|
|
72
|
+
bool svg = false;
|
|
73
|
+
SizeEntry size;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
std::unordered_map<std::string, DataUrlEntry> &dataUrlCache() {
|
|
77
|
+
static std::unordered_map<std::string, DataUrlEntry> cache;
|
|
78
|
+
return cache;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
std::string lowercase(std::string value) {
|
|
82
|
+
for (char &c : value) c = (char)std::tolower((unsigned char)c);
|
|
83
|
+
return value;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
std::string trim(const std::string &text) {
|
|
87
|
+
size_t start = 0;
|
|
88
|
+
while (start < text.size() && std::isspace((unsigned char)text[start])) start++;
|
|
89
|
+
size_t end = text.size();
|
|
90
|
+
while (end > start && std::isspace((unsigned char)text[end - 1])) end--;
|
|
91
|
+
return text.substr(start, end - start);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Decode `%XX` escapes. `+` stays a literal plus: a data URL uses URL
|
|
95
|
+
* percent-encoding, not form encoding. A malformed escape is left verbatim. */
|
|
96
|
+
std::string percentDecode(const std::string &text) {
|
|
97
|
+
std::string out;
|
|
98
|
+
out.reserve(text.size());
|
|
99
|
+
for (size_t i = 0; i < text.size(); i++) {
|
|
100
|
+
if (text[i] == '%' && i + 2 < text.size() && std::isxdigit((unsigned char)text[i + 1]) &&
|
|
101
|
+
std::isxdigit((unsigned char)text[i + 2])) {
|
|
102
|
+
auto hex = [](char c) -> int {
|
|
103
|
+
if (c >= '0' && c <= '9') return c - '0';
|
|
104
|
+
return std::tolower((unsigned char)c) - 'a' + 10;
|
|
105
|
+
};
|
|
106
|
+
out.push_back((char)(hex(text[i + 1]) * 16 + hex(text[i + 2])));
|
|
107
|
+
i += 2;
|
|
108
|
+
} else {
|
|
109
|
+
out.push_back(text[i]);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** True when one of the header's `;`-separated parameters is `base64`
|
|
116
|
+
* (case-insensitive, as the data-URL grammar specifies). */
|
|
117
|
+
bool headerHasBase64(const std::string &header) {
|
|
118
|
+
size_t start = 0;
|
|
119
|
+
while (start <= header.size()) {
|
|
120
|
+
size_t end = header.find(';', start);
|
|
121
|
+
if (end == std::string::npos) end = header.size();
|
|
122
|
+
if (lowercase(trim(header.substr(start, end - start))) == "base64") return true;
|
|
123
|
+
if (end == header.size()) break;
|
|
124
|
+
start = end + 1;
|
|
125
|
+
}
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** The MIME type part of a data URL's header, lowercased. */
|
|
130
|
+
std::string headerMime(const std::string &header) {
|
|
131
|
+
size_t semi = header.find(';');
|
|
132
|
+
return lowercase(trim(header.substr(0, semi)));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** A cheap content sniff for a data URL whose MIME type is generic: look for
|
|
136
|
+
* `<svg` in the first chunk (after an XML declaration, doctype or comment). */
|
|
137
|
+
bool bytesLookLikeSvg(const std::vector<unsigned char> &bytes) {
|
|
138
|
+
if (bytes.empty()) return false;
|
|
139
|
+
const size_t limit = bytes.size() < 1024 ? bytes.size() : 1024;
|
|
140
|
+
const std::string prefix(reinterpret_cast<const char *>(bytes.data()), limit);
|
|
141
|
+
return prefix.find("<svg") != std::string::npos;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
bool base64Decode(const std::string &text, std::vector<unsigned char> *out) {
|
|
74
145
|
static const signed char kDecode[128] = {
|
|
75
146
|
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
|
76
147
|
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
|
@@ -81,12 +152,11 @@ bool decodeDataUrl(const std::string &url, std::vector<unsigned char> *out) {
|
|
|
81
152
|
-1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
|
|
82
153
|
41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1};
|
|
83
154
|
out->clear();
|
|
84
|
-
out->reserve(
|
|
155
|
+
out->reserve(text.size() * 3 / 4 + 4);
|
|
85
156
|
uint32_t accumulator = 0;
|
|
86
157
|
int bits = 0;
|
|
87
|
-
for (
|
|
88
|
-
|
|
89
|
-
if (c == '=' || c == '\n' || c == '\r' || c == ' ') continue;
|
|
158
|
+
for (unsigned char c : text) {
|
|
159
|
+
if (c == '=' || std::isspace(c)) continue;
|
|
90
160
|
if (c >= 128) return false;
|
|
91
161
|
const int value = kDecode[c];
|
|
92
162
|
if (value < 0) return false;
|
|
@@ -100,33 +170,107 @@ bool decodeDataUrl(const std::string &url, std::vector<unsigned char> *out) {
|
|
|
100
170
|
return !out->empty();
|
|
101
171
|
}
|
|
102
172
|
|
|
173
|
+
/** Decode a `data:` URL's payload. Both `;base64` and percent-encoded (or
|
|
174
|
+
* plain) bodies are accepted; in the base64 case the body is percent-decoded
|
|
175
|
+
* first, which is what a browser does. `mime` receives the lowercased header
|
|
176
|
+
* type (empty when the URL omits one). */
|
|
177
|
+
bool decodeDataUrl(const std::string &url, std::vector<unsigned char> *out, std::string *mime) {
|
|
178
|
+
const size_t comma = url.find(',');
|
|
179
|
+
if (comma == std::string::npos || comma < 5) return false;
|
|
180
|
+
const std::string header = url.substr(5, comma - 5);
|
|
181
|
+
if (mime != nullptr) *mime = headerMime(header);
|
|
182
|
+
const std::string body = percentDecode(url.substr(comma + 1));
|
|
183
|
+
if (headerHasBase64(header)) return base64Decode(body, out);
|
|
184
|
+
out->assign(body.begin(), body.end());
|
|
185
|
+
return !out->empty();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Classify (`SVG` vs raster) and measure a `data:` URL once. */
|
|
189
|
+
DataUrlEntry &dataUrlEntry(const std::string &url) {
|
|
190
|
+
auto &cache = dataUrlCache();
|
|
191
|
+
auto found = cache.find(url);
|
|
192
|
+
if (found != cache.end()) return found->second;
|
|
193
|
+
|
|
194
|
+
DataUrlEntry entry;
|
|
195
|
+
std::vector<unsigned char> bytes;
|
|
196
|
+
std::string mime;
|
|
197
|
+
if (decodeDataUrl(url, &bytes, &mime)) {
|
|
198
|
+
/* Trust an explicit raster MIME type; sniff only when the type is absent
|
|
199
|
+
* or generic, so a PNG that happens to contain `<svg` in a text chunk is
|
|
200
|
+
* not mistaken for vector art. */
|
|
201
|
+
const bool generic_mime =
|
|
202
|
+
mime.empty() || mime == "text/plain" || mime == "application/octet-stream";
|
|
203
|
+
entry.svg = mime == "image/svg+xml" || (generic_mime && bytesLookLikeSvg(bytes));
|
|
204
|
+
if (entry.svg) {
|
|
205
|
+
const std::string markup(bytes.begin(), bytes.end());
|
|
206
|
+
int width = 0;
|
|
207
|
+
int height = 0;
|
|
208
|
+
if (xt_svg_markup_size(markup, &width, &height)) {
|
|
209
|
+
entry.size.valid = true;
|
|
210
|
+
entry.size.width = width;
|
|
211
|
+
entry.size.height = height;
|
|
212
|
+
}
|
|
213
|
+
} else {
|
|
214
|
+
int width = 0;
|
|
215
|
+
int height = 0;
|
|
216
|
+
int channels = 0;
|
|
217
|
+
if (stbi_info_from_memory(bytes.data(), (int)bytes.size(), &width, &height, &channels) != 0) {
|
|
218
|
+
entry.size.valid = true;
|
|
219
|
+
entry.size.width = width;
|
|
220
|
+
entry.size.height = height;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return cache.emplace(url, std::move(entry)).first->second;
|
|
225
|
+
}
|
|
226
|
+
|
|
103
227
|
} // namespace
|
|
104
228
|
|
|
105
229
|
const Image *xt_image_load(const std::string &raw_path, int target_width, int target_height) {
|
|
106
230
|
/* A `data:` URL carries its bytes inline (a canvas used as a CSS
|
|
107
231
|
* `background-image` reaches this path through `toDataURL()`). */
|
|
108
232
|
if (raw_path.rfind("data:", 0) == 0) {
|
|
233
|
+
const DataUrlEntry &info = dataUrlEntry(raw_path);
|
|
234
|
+
int width = target_width;
|
|
235
|
+
int height = target_height;
|
|
236
|
+
if (info.svg && width <= 0 && height <= 0 && info.size.valid) {
|
|
237
|
+
width = info.size.width;
|
|
238
|
+
height = info.size.height;
|
|
239
|
+
}
|
|
240
|
+
/* A vector source is rasterised at the drawn size, so that size is part of
|
|
241
|
+
* its cache identity. A raster data URL decodes once at its intrinsic
|
|
242
|
+
* resolution. */
|
|
243
|
+
std::string key = raw_path;
|
|
244
|
+
if (info.svg) {
|
|
245
|
+
key = "data-svg:" + std::to_string(width) + "x" + std::to_string(height) + ":" + raw_path;
|
|
246
|
+
}
|
|
109
247
|
auto &cache = imageCache();
|
|
110
|
-
auto found = cache.find(
|
|
248
|
+
auto found = cache.find(key);
|
|
111
249
|
if (found != cache.end()) {
|
|
112
250
|
return found->second.width > 0 ? &found->second : nullptr;
|
|
113
251
|
}
|
|
114
252
|
Image image;
|
|
115
253
|
std::vector<unsigned char> bytes;
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
254
|
+
std::string mime;
|
|
255
|
+
if (decodeDataUrl(raw_path, &bytes, &mime)) {
|
|
256
|
+
if (info.svg) {
|
|
257
|
+
const std::string markup(bytes.begin(), bytes.end());
|
|
258
|
+
xt_svg_markup_render(markup, width, height, &image);
|
|
259
|
+
} else {
|
|
260
|
+
int decoded_width = 0;
|
|
261
|
+
int decoded_height = 0;
|
|
262
|
+
int channels = 0;
|
|
263
|
+
unsigned char *data = stbi_load_from_memory(bytes.data(), (int)bytes.size(),
|
|
264
|
+
&decoded_width, &decoded_height, &channels, 4);
|
|
265
|
+
if (data != nullptr) {
|
|
266
|
+
image.width = decoded_width;
|
|
267
|
+
image.height = decoded_height;
|
|
268
|
+
image.pixels.assign(data, data + (size_t)decoded_width * (size_t)decoded_height * 4);
|
|
269
|
+
stbi_image_free(data);
|
|
270
|
+
}
|
|
127
271
|
}
|
|
128
272
|
}
|
|
129
|
-
auto inserted = cache.emplace(
|
|
273
|
+
auto inserted = cache.emplace(key, std::move(image));
|
|
130
274
|
return inserted.first->second.width > 0 ? &inserted.first->second : nullptr;
|
|
131
275
|
}
|
|
132
276
|
std::string path = normalizePath(raw_path);
|
|
@@ -165,6 +309,13 @@ const Image *xt_image_load(const std::string &raw_path, int target_width, int ta
|
|
|
165
309
|
|
|
166
310
|
bool xt_image_size(const std::string &raw_path, int *width, int *height) {
|
|
167
311
|
if (raw_path.rfind("data:", 0) == 0) {
|
|
312
|
+
const DataUrlEntry &info = dataUrlEntry(raw_path);
|
|
313
|
+
if (info.size.valid) {
|
|
314
|
+
if (width != nullptr) *width = info.size.width;
|
|
315
|
+
if (height != nullptr) *height = info.size.height;
|
|
316
|
+
return true;
|
|
317
|
+
}
|
|
318
|
+
if (info.svg) return false;
|
|
168
319
|
const Image *image = xt_image_load(raw_path);
|
|
169
320
|
if (image == nullptr) return false;
|
|
170
321
|
if (width != nullptr) *width = image->width;
|
|
@@ -202,11 +353,15 @@ bool xt_image_decode(const unsigned char *data, size_t size, Image *out) {
|
|
|
202
353
|
return true;
|
|
203
354
|
}
|
|
204
355
|
|
|
205
|
-
bool xt_image_is_vector(const std::string &path) {
|
|
356
|
+
bool xt_image_is_vector(const std::string &path) {
|
|
357
|
+
if (path.rfind("data:", 0) == 0) return dataUrlEntry(path).svg;
|
|
358
|
+
return xt_svg_is_svg(normalizePath(path));
|
|
359
|
+
}
|
|
206
360
|
|
|
207
361
|
void xt_image_shutdown() {
|
|
208
362
|
imageCache().clear();
|
|
209
363
|
sizeCache().clear();
|
|
364
|
+
dataUrlCache().clear();
|
|
210
365
|
xt_svg_shutdown();
|
|
211
366
|
}
|
|
212
367
|
|
package/runtime/ext_gui/image.h
CHANGED
|
@@ -23,9 +23,11 @@ struct Image {
|
|
|
23
23
|
std::vector<unsigned char> pixels;
|
|
24
24
|
};
|
|
25
25
|
|
|
26
|
-
/** Decode an image
|
|
27
|
-
* nullptr on failure. Results (including
|
|
28
|
-
* `file://` prefix and URL percent-encoding are
|
|
26
|
+
/** Decode an image source (PNG/JPEG/BMP/GIF/TGA or SVG, from a file path or a
|
|
27
|
+
* `data:` URL) to RGBA8. Returns nullptr on failure. Results (including
|
|
28
|
+
* failures) are cached. A `file://` prefix and URL percent-encoding are
|
|
29
|
+
* handled for paths; a `data:` URL accepts a `;base64` or percent-encoded
|
|
30
|
+
* payload and is classified by MIME type/content (SVG or raster).
|
|
29
31
|
*
|
|
30
32
|
* `target_width`/`target_height` are the pixel size the image will be drawn at.
|
|
31
33
|
* Raster formats ignore them (they decode at their intrinsic resolution), but
|
|
@@ -40,7 +42,7 @@ const Image *xt_image_load(const std::string &path, int target_width = 0, int ta
|
|
|
40
42
|
* viewBox). Returns false when the file cannot be read/parsed. Cached. */
|
|
41
43
|
bool xt_image_size(const std::string &path, int *width, int *height);
|
|
42
44
|
|
|
43
|
-
/** Decode an in-memory buffer to RGBA8 (
|
|
45
|
+
/** Decode an in-memory raster buffer to RGBA8 (used by the app icon). */
|
|
44
46
|
bool xt_image_decode(const unsigned char *data, size_t size, Image *out);
|
|
45
47
|
|
|
46
48
|
/** True when `path` is a vector source (SVG) that is rasterised on demand
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
#include "paint.h"
|
|
10
10
|
|
|
11
11
|
#include <algorithm>
|
|
12
|
+
#include <cstddef>
|
|
12
13
|
#include <cstdio>
|
|
13
14
|
#include <utility>
|
|
14
15
|
|
|
@@ -56,6 +57,9 @@ int zIndexOf(const LayoutBox *box) {
|
|
|
56
57
|
void paintBox(const LayoutBox *box, DisplayList &out, const PaintClip &clip,
|
|
57
58
|
const paint_detail::FilterChain &parentChain, float viewportWidth,
|
|
58
59
|
float viewportHeight);
|
|
60
|
+
void paintBoxInner(const LayoutBox *box, DisplayList &out, const PaintClip &clip,
|
|
61
|
+
const paint_detail::FilterChain &parentChain, float viewportWidth,
|
|
62
|
+
float viewportHeight);
|
|
59
63
|
|
|
60
64
|
/** The clip a box imposes on its content: its padding box, intersected with the
|
|
61
65
|
* clip already in effect. Only `overflow: hidden/scroll/auto` clips. A
|
|
@@ -149,9 +153,9 @@ void addTextRuns(const LayoutBox *box, const XtStyle &style, DisplayList &out,
|
|
|
149
153
|
}
|
|
150
154
|
}
|
|
151
155
|
|
|
152
|
-
void
|
|
153
|
-
|
|
154
|
-
|
|
156
|
+
void paintBoxInner(const LayoutBox *box, DisplayList &out, const PaintClip &clip,
|
|
157
|
+
const paint_detail::FilterChain &parentChain, float viewportWidth,
|
|
158
|
+
float viewportHeight) {
|
|
155
159
|
if (box == nullptr) return;
|
|
156
160
|
const XtStyle *style = box->style;
|
|
157
161
|
|
|
@@ -166,6 +170,11 @@ void paintBox(const LayoutBox *box, DisplayList &out, const PaintClip &clip,
|
|
|
166
170
|
chain = paint_detail::extendFilter(parentChain, style->filters, *style, viewportWidth,
|
|
167
171
|
viewportHeight);
|
|
168
172
|
}
|
|
173
|
+
/* `opacity` is not inherited, but its effect is: every shape painted by the
|
|
174
|
+
* box or its descendants gets its alpha multiplied by the product down the
|
|
175
|
+
* chain. This makes fractional opacity visible instead of treating anything
|
|
176
|
+
* above zero as fully opaque. */
|
|
177
|
+
if (style != nullptr && !box->is_text) chain.opacity *= style->opacity;
|
|
169
178
|
|
|
170
179
|
PaintClip own = clip;
|
|
171
180
|
if (style != nullptr && box->display != Display::None && !box->is_text) {
|
|
@@ -252,6 +261,32 @@ void paintBox(const LayoutBox *box, DisplayList &out, const PaintClip &clip,
|
|
|
252
261
|
paintChildren(box, out, clipForChildren(box, own), childChain, viewportWidth, viewportHeight);
|
|
253
262
|
}
|
|
254
263
|
|
|
264
|
+
/** Paint one box, wrapping it in a `mix-blend-mode` group when it declares one.
|
|
265
|
+
* The group's ops are bounded by `LayerBegin`/`LayerEnd`, so the renderer can
|
|
266
|
+
* draw the subtree to an offscreen texture and blend it over the backdrop as
|
|
267
|
+
* one unit (overlapping children must not blend twice). */
|
|
268
|
+
void paintBox(const LayoutBox *box, DisplayList &out, const PaintClip &clip,
|
|
269
|
+
const paint_detail::FilterChain &parentChain, float viewportWidth,
|
|
270
|
+
float viewportHeight) {
|
|
271
|
+
if (box == nullptr) return;
|
|
272
|
+
const XtStyle *style = box->style;
|
|
273
|
+
const bool group = style != nullptr && !box->is_text && box->display != Display::None &&
|
|
274
|
+
style->mix_blend_mode != MixBlendMode::Normal;
|
|
275
|
+
if (!group) {
|
|
276
|
+
paintBoxInner(box, out, clip, parentChain, viewportWidth, viewportHeight);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
size_t first = out.ops.size();
|
|
280
|
+
paintBoxInner(box, out, clip, parentChain, viewportWidth, viewportHeight);
|
|
281
|
+
if (out.ops.size() == first) return; /* nothing painted (e.g. opacity: 0) */
|
|
282
|
+
PaintLayer layer;
|
|
283
|
+
layer.blend = style->mix_blend_mode;
|
|
284
|
+
uint32_t index = (uint32_t)out.layers.size();
|
|
285
|
+
out.layers.push_back(layer);
|
|
286
|
+
out.ops.insert(out.ops.begin() + (ptrdiff_t)first, PaintOp{PaintKind::LayerBegin, index, clip});
|
|
287
|
+
out.ops.push_back(PaintOp{PaintKind::LayerEnd, 0, clip});
|
|
288
|
+
}
|
|
289
|
+
|
|
255
290
|
} // namespace
|
|
256
291
|
|
|
257
292
|
void xt_paint_build(const LayoutBox *root, DisplayList &out, float viewportWidth,
|
|
@@ -311,6 +346,21 @@ std::string DisplayList::dump() const {
|
|
|
311
346
|
}
|
|
312
347
|
prefix += " ";
|
|
313
348
|
}
|
|
349
|
+
if (op.kind == PaintKind::LayerBegin) {
|
|
350
|
+
static const char *const names[] = {
|
|
351
|
+
"normal", "multiply", "screen", "overlay", "darken",
|
|
352
|
+
"lighten", "color-dodge", "color-burn", "hard-light", "soft-light",
|
|
353
|
+
"difference", "exclusion", "hue", "saturation", "color",
|
|
354
|
+
"luminosity"};
|
|
355
|
+
const char *name = "normal";
|
|
356
|
+
if (op.index < layers.size()) name = names[(int)layers[op.index].blend];
|
|
357
|
+
text += prefix + "layer blend=" + name + "\n";
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
if (op.kind == PaintKind::LayerEnd) {
|
|
361
|
+
text += prefix + "end-layer\n";
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
314
364
|
std::string suffix;
|
|
315
365
|
if (op.kind == PaintKind::Rect) {
|
|
316
366
|
suffix = transformSuffix(rects[op.index].transform);
|
|
@@ -330,7 +380,22 @@ std::string DisplayList::dump() const {
|
|
|
330
380
|
} else {
|
|
331
381
|
const PaintImage &image = images[op.index];
|
|
332
382
|
suffix = transformSuffix(image.transform);
|
|
333
|
-
if (image.
|
|
383
|
+
if (image.gradient != nullptr) {
|
|
384
|
+
const PaintGradient &gradient = *image.gradient;
|
|
385
|
+
std::snprintf(buffer, sizeof(buffer),
|
|
386
|
+
"image x=%.1f y=%.1f w=%.1f h=%.1f gradient=%s center=%.1f,%.1f "
|
|
387
|
+
"radius=%.1f,%.1f stops=",
|
|
388
|
+
image.x, image.y, image.width, image.height,
|
|
389
|
+
gradient.repeating ? "repeating-radial" : "radial", gradient.center_x,
|
|
390
|
+
gradient.center_y, gradient.radius_x, gradient.radius_y);
|
|
391
|
+
text += prefix + buffer;
|
|
392
|
+
for (size_t stop = 0; stop < gradient.stops.size(); stop++) {
|
|
393
|
+
std::snprintf(buffer, sizeof(buffer), "%s%s@%.4f", stop == 0 ? "" : ",",
|
|
394
|
+
formatColor(gradient.stops[stop].color).c_str(),
|
|
395
|
+
gradient.stops[stop].offset);
|
|
396
|
+
text += buffer;
|
|
397
|
+
}
|
|
398
|
+
} else if (image.media_id != 0) {
|
|
334
399
|
std::snprintf(buffer, sizeof(buffer), "image x=%.1f y=%.1f w=%.1f h=%.1f media=%llu",
|
|
335
400
|
image.x, image.y, image.width, image.height,
|
|
336
401
|
(unsigned long long)image.media_id);
|
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
|
|
@@ -102,7 +136,14 @@ struct PaintClip {
|
|
|
102
136
|
int paths[2] = {-1, -1};
|
|
103
137
|
};
|
|
104
138
|
|
|
105
|
-
enum class PaintKind { Rect, Text, Image };
|
|
139
|
+
enum class PaintKind { Rect, Text, Image, LayerBegin, LayerEnd };
|
|
140
|
+
|
|
141
|
+
/** One `mix-blend-mode` compositing group. The ops between its `LayerBegin`
|
|
142
|
+
* and `LayerEnd` markers are rendered to an offscreen texture and composited
|
|
143
|
+
* over the backdrop as a group, so overlapping children blend once. */
|
|
144
|
+
struct PaintLayer {
|
|
145
|
+
MixBlendMode blend = MixBlendMode::Normal;
|
|
146
|
+
};
|
|
106
147
|
|
|
107
148
|
/** One entry in painter order: which kind of shape, its index in the matching
|
|
108
149
|
* payload vector, and the clip in effect. */
|
|
@@ -121,6 +162,8 @@ struct DisplayList {
|
|
|
121
162
|
std::vector<PaintOp> ops;
|
|
122
163
|
/** Device-space clip-path polygons shared by an element's ops. */
|
|
123
164
|
std::vector<ClipPathGeometry> clip_paths;
|
|
165
|
+
/** `mix-blend-mode` groups referenced by `LayerBegin` ops. */
|
|
166
|
+
std::vector<PaintLayer> layers;
|
|
124
167
|
|
|
125
168
|
void clear() {
|
|
126
169
|
rects.clear();
|
|
@@ -128,12 +171,25 @@ struct DisplayList {
|
|
|
128
171
|
images.clear();
|
|
129
172
|
ops.clear();
|
|
130
173
|
clip_paths.clear();
|
|
174
|
+
layers.clear();
|
|
131
175
|
}
|
|
132
176
|
bool empty() const { return rects.empty() && texts.empty() && images.empty(); }
|
|
133
177
|
/** Human-readable dump, one shape per line, in painter order (diagnostics/tests). */
|
|
134
178
|
std::string dump() const;
|
|
135
179
|
};
|
|
136
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
|
+
|
|
137
193
|
/** Build the display list for a laid-out tree and append it to `out`. The
|
|
138
194
|
* viewport size resolves `vw`/`vh` units inside `clip-path` and shadows; pass 0
|
|
139
195
|
* to take it from the root box's containing block. */
|