xbintsc 0.5.41 → 0.5.42
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 +96 -0
- 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/canvas.md +6 -2
- package/doc/gui/known-issues.md +15 -9
- package/doc/gui/media.md +11 -2
- package/doc/gui/rendering.md +43 -18
- package/doc/gui.md +5 -4
- package/doc/zh-CN/gui/canvas.md +6 -4
- package/doc/zh-CN/gui/known-issues.md +11 -8
- package/doc/zh-CN/gui/media.md +9 -2
- package/doc/zh-CN/gui/rendering.md +30 -11
- package/doc/zh-CN/gui.md +1 -1
- 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 +53 -3
- package/runtime/ext_gui/paint.h +11 -1
- package/runtime/ext_gui/paint_background.cpp +92 -11
- 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_internal.h +26 -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
|
@@ -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);
|
package/runtime/ext_gui/paint.h
CHANGED
|
@@ -102,7 +102,14 @@ struct PaintClip {
|
|
|
102
102
|
int paths[2] = {-1, -1};
|
|
103
103
|
};
|
|
104
104
|
|
|
105
|
-
enum class PaintKind { Rect, Text, Image };
|
|
105
|
+
enum class PaintKind { Rect, Text, Image, LayerBegin, LayerEnd };
|
|
106
|
+
|
|
107
|
+
/** One `mix-blend-mode` compositing group. The ops between its `LayerBegin`
|
|
108
|
+
* and `LayerEnd` markers are rendered to an offscreen texture and composited
|
|
109
|
+
* over the backdrop as a group, so overlapping children blend once. */
|
|
110
|
+
struct PaintLayer {
|
|
111
|
+
MixBlendMode blend = MixBlendMode::Normal;
|
|
112
|
+
};
|
|
106
113
|
|
|
107
114
|
/** One entry in painter order: which kind of shape, its index in the matching
|
|
108
115
|
* payload vector, and the clip in effect. */
|
|
@@ -121,6 +128,8 @@ struct DisplayList {
|
|
|
121
128
|
std::vector<PaintOp> ops;
|
|
122
129
|
/** Device-space clip-path polygons shared by an element's ops. */
|
|
123
130
|
std::vector<ClipPathGeometry> clip_paths;
|
|
131
|
+
/** `mix-blend-mode` groups referenced by `LayerBegin` ops. */
|
|
132
|
+
std::vector<PaintLayer> layers;
|
|
124
133
|
|
|
125
134
|
void clear() {
|
|
126
135
|
rects.clear();
|
|
@@ -128,6 +137,7 @@ struct DisplayList {
|
|
|
128
137
|
images.clear();
|
|
129
138
|
ops.clear();
|
|
130
139
|
clip_paths.clear();
|
|
140
|
+
layers.clear();
|
|
131
141
|
}
|
|
132
142
|
bool empty() const { return rects.empty() && texts.empty() && images.empty(); }
|
|
133
143
|
/** Human-readable dump, one shape per line, in painter order (diagnostics/tests). */
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* The renderer has one rounded-rectangle pipeline and no gradient shader, so a
|
|
5
5
|
* gradient is emitted as a run of solid bands: 18 strips for a linear gradient
|
|
6
|
-
* and 32 concentric ellipses for a radial one
|
|
6
|
+
* (64 while repeating) and 32 concentric ellipses for a radial one (64 while
|
|
7
|
+
* repeating). Radial ellipses use the shape
|
|
7
8
|
* pipeline's transform slot (a unit circle scaled to the ellipse radii), which
|
|
8
9
|
* keeps the distance-field antialiasing intact.
|
|
9
10
|
*/
|
|
@@ -19,9 +20,81 @@ namespace paint_detail {
|
|
|
19
20
|
|
|
20
21
|
namespace {
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
struct ResolvedStop {
|
|
24
|
+
float offset = 0.0f;
|
|
25
|
+
Color color;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/** Resolve the declaration's percentage/length offsets against the gradient
|
|
29
|
+
* line, then distribute the omitted (`Unit::Auto`) ones between their
|
|
30
|
+
* neighbours. The last explicit stop is preserved: for a normal gradient it
|
|
31
|
+
* holds that colour, and for a repeating one it defines the period. */
|
|
32
|
+
std::vector<ResolvedStop> resolveStops(const BackgroundLayer &layer, float line_length,
|
|
33
|
+
const XtStyle &style, float viewportWidth,
|
|
34
|
+
float viewportHeight) {
|
|
35
|
+
std::vector<ResolvedStop> stops;
|
|
36
|
+
for (const BackgroundGradientStop &stop : layer.stops) {
|
|
37
|
+
ResolvedStop resolved;
|
|
38
|
+
resolved.color = stop.color;
|
|
39
|
+
resolved.offset = -1.0f;
|
|
40
|
+
if (stop.offset.unit == Unit::Percent) {
|
|
41
|
+
resolved.offset = stop.offset.value / 100.0f;
|
|
42
|
+
} else if (stop.offset.unit != Unit::Auto && stop.offset.unit != Unit::Invalid &&
|
|
43
|
+
stop.offset.unit != Unit::None) {
|
|
44
|
+
float pixels = resolveLength(stop.offset, 0.0f, style.font_size, viewportWidth,
|
|
45
|
+
viewportHeight);
|
|
46
|
+
resolved.offset = line_length > 0.00001f ? pixels / line_length : 0.0f;
|
|
47
|
+
}
|
|
48
|
+
stops.push_back(resolved);
|
|
49
|
+
}
|
|
50
|
+
if (stops.empty()) return stops;
|
|
51
|
+
|
|
52
|
+
/* Explicit offsets are non-decreasing, as in CSS. */
|
|
53
|
+
float last = -1.0f;
|
|
54
|
+
for (ResolvedStop &stop : stops) {
|
|
55
|
+
if (stop.offset < 0.0f) continue;
|
|
56
|
+
if (stop.offset < last) stop.offset = last;
|
|
57
|
+
last = stop.offset;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* Fill implicit offsets by distributing them between their neighbours. */
|
|
61
|
+
size_t i = 0;
|
|
62
|
+
while (i < stops.size()) {
|
|
63
|
+
if (stops[i].offset >= 0.0f) {
|
|
64
|
+
i++;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
size_t j = i;
|
|
68
|
+
while (j < stops.size() && stops[j].offset < 0.0f) j++;
|
|
69
|
+
float before = (i == 0) ? 0.0f : stops[i - 1].offset;
|
|
70
|
+
float after = (j >= stops.size()) ? 1.0f : stops[j].offset;
|
|
71
|
+
size_t count = j - i;
|
|
72
|
+
for (size_t k = 0; k < count; k++) {
|
|
73
|
+
float offset = 0.0f;
|
|
74
|
+
if (i == 0 && j >= stops.size()) {
|
|
75
|
+
offset = count > 1 ? (float)k / (float)(count - 1) : 0.0f;
|
|
76
|
+
} else if (i == 0) {
|
|
77
|
+
offset = after * (float)k / (float)count;
|
|
78
|
+
} else if (j >= stops.size()) {
|
|
79
|
+
offset = before + (1.0f - before) * (float)(k + 1) / (float)count;
|
|
80
|
+
} else {
|
|
81
|
+
offset = before + (after - before) * (float)(k + 1) / (float)(count + 1);
|
|
82
|
+
}
|
|
83
|
+
stops[i + k].offset = offset;
|
|
84
|
+
}
|
|
85
|
+
i = j;
|
|
86
|
+
}
|
|
87
|
+
return stops;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
Color gradientColorAt(const std::vector<ResolvedStop> &stops, bool repeating, float t) {
|
|
24
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
|
+
}
|
|
25
98
|
if (t <= stops.front().offset) return stops.front().color;
|
|
26
99
|
if (t >= stops.back().offset) return stops.back().color;
|
|
27
100
|
for (size_t i = 1; i < stops.size(); i++) {
|
|
@@ -75,17 +148,23 @@ PaintClip boxClip(const PaintClip &clip, const LayoutBox &box) {
|
|
|
75
148
|
return out;
|
|
76
149
|
}
|
|
77
150
|
|
|
78
|
-
void paintLinear(const LayoutBox &box, const
|
|
79
|
-
const PaintClip &clip,
|
|
151
|
+
void paintLinear(const LayoutBox &box, const XtStyle &style, const BackgroundLayer &layer,
|
|
152
|
+
const FilterChain &chain, const PaintClip &clip, float viewportWidth,
|
|
153
|
+
float viewportHeight, DisplayList &out) {
|
|
80
154
|
if (layer.stops.size() < 2 || box.width <= 0.0f || box.height <= 0.0f) return;
|
|
81
|
-
|
|
155
|
+
/* Repeating patterns need more samples than one gradient ramp: a 10%
|
|
156
|
+
* stripe is only two bands at the old 18-strip resolution. */
|
|
157
|
+
const int strips = layer.repeating ? 64 : 18;
|
|
82
158
|
bool horizontal = (layer.angle > 80.0f && layer.angle < 100.0f) ||
|
|
83
159
|
(layer.angle > 260.0f && layer.angle < 280.0f);
|
|
84
160
|
bool reverse = (layer.angle > 260.0f && layer.angle < 280.0f) || layer.angle < 10.0f ||
|
|
85
161
|
layer.angle > 350.0f;
|
|
162
|
+
const float line_length = horizontal ? box.width : box.height;
|
|
163
|
+
std::vector<ResolvedStop> stops = resolveStops(layer, line_length, style, viewportWidth,
|
|
164
|
+
viewportHeight);
|
|
86
165
|
for (int i = 0; i < strips; i++) {
|
|
87
166
|
float t = ((float)i + 0.5f) / (float)strips;
|
|
88
|
-
Color color = gradientColorAt(layer, reverse ? 1.0f - t : t);
|
|
167
|
+
Color color = gradientColorAt(stops, layer.repeating, reverse ? 1.0f - t : t);
|
|
89
168
|
color = filterColor(chain, color);
|
|
90
169
|
if (color.a <= 0.0f) continue;
|
|
91
170
|
if (horizontal) {
|
|
@@ -188,15 +267,17 @@ void paintRadial(const LayoutBox &box, const XtStyle &style, const BackgroundLay
|
|
|
188
267
|
float ry = 0.0f;
|
|
189
268
|
radialRadii(box, layer, style, viewportWidth, viewportHeight, &cx, &cy, &rx, &ry);
|
|
190
269
|
if (rx <= 0.0f || ry <= 0.0f) return;
|
|
191
|
-
|
|
270
|
+
std::vector<ResolvedStop> stops = resolveStops(layer, std::max(rx, ry), style, viewportWidth,
|
|
271
|
+
viewportHeight);
|
|
272
|
+
const int bands = layer.repeating ? 64 : 32;
|
|
192
273
|
for (int i = bands; i >= 1; i--) {
|
|
193
274
|
float t = (float)i / (float)bands;
|
|
194
|
-
Color color = filterColor(chain, gradientColorAt(layer, t));
|
|
275
|
+
Color color = filterColor(chain, gradientColorAt(stops, layer.repeating, t));
|
|
195
276
|
if (color.a <= 0.0f) continue;
|
|
196
277
|
addEllipse(out, cx, cy, rx * t, ry * t, color, clip, box.world);
|
|
197
278
|
}
|
|
198
279
|
/* Exact centre colour (the innermost band samples just off zero). */
|
|
199
|
-
Color center = filterColor(chain, gradientColorAt(layer, 0.0f));
|
|
280
|
+
Color center = filterColor(chain, gradientColorAt(stops, layer.repeating, 0.0f));
|
|
200
281
|
addEllipse(out, cx, cy, 0.6f, 0.6f, center, clip, box.world);
|
|
201
282
|
}
|
|
202
283
|
|
|
@@ -233,7 +314,7 @@ void paintBackgrounds(DisplayList &out, const LayoutBox &box, const XtStyle &sty
|
|
|
233
314
|
for (size_t i = style.background_layers.size(); i-- > 0;) {
|
|
234
315
|
const BackgroundLayer &layer = style.background_layers[i];
|
|
235
316
|
if (layer.kind == BackgroundKind::Linear) {
|
|
236
|
-
paintLinear(box, layer, chain, layerClip, out);
|
|
317
|
+
paintLinear(box, style, layer, chain, layerClip, viewportWidth, viewportHeight, out);
|
|
237
318
|
} else if (layer.kind == BackgroundKind::Radial) {
|
|
238
319
|
paintRadial(box, style, layer, chain, layerClip, viewportWidth, viewportHeight, out);
|
|
239
320
|
} 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`. */
|