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.
Files changed (39) hide show
  1. package/dist/src/cli/help.d.ts +1 -1
  2. package/dist/src/driver/version.d.ts +1 -1
  3. package/dist/src/driver/version.js +1 -1
  4. package/dist/tests/e2e/gui-data-url.test.d.ts +10 -0
  5. package/dist/tests/e2e/gui-data-url.test.js +127 -0
  6. package/dist/tests/e2e/gui-data-url.test.js.map +1 -0
  7. package/dist/tests/e2e/gui-effects.test.js +96 -0
  8. package/dist/tests/e2e/gui-effects.test.js.map +1 -1
  9. package/dist/tests/e2e/gui-paint.test.js +77 -0
  10. package/dist/tests/e2e/gui-paint.test.js.map +1 -1
  11. package/doc/gui/canvas.md +6 -2
  12. package/doc/gui/known-issues.md +15 -9
  13. package/doc/gui/media.md +11 -2
  14. package/doc/gui/rendering.md +43 -18
  15. package/doc/gui.md +5 -4
  16. package/doc/zh-CN/gui/canvas.md +6 -4
  17. package/doc/zh-CN/gui/known-issues.md +11 -8
  18. package/doc/zh-CN/gui/media.md +9 -2
  19. package/doc/zh-CN/gui/rendering.md +30 -11
  20. package/doc/zh-CN/gui.md +1 -1
  21. package/package.json +7 -7
  22. package/runtime/ext_gui/dom_api_canvas.cpp +42 -24
  23. package/runtime/ext_gui/gradient_values.cpp +44 -48
  24. package/runtime/ext_gui/gui_events.cpp +16 -13
  25. package/runtime/ext_gui/image.cpp +180 -25
  26. package/runtime/ext_gui/image.h +6 -4
  27. package/runtime/ext_gui/paint.cpp +53 -3
  28. package/runtime/ext_gui/paint.h +11 -1
  29. package/runtime/ext_gui/paint_background.cpp +92 -11
  30. package/runtime/ext_gui/paint_effects.cpp +2 -1
  31. package/runtime/ext_gui/paint_internal.h +5 -1
  32. package/runtime/ext_gui/renderer.cpp +81 -13
  33. package/runtime/ext_gui/renderer.h +19 -5
  34. package/runtime/ext_gui/renderer_internal.h +26 -0
  35. package/runtime/ext_gui/renderer_layer.cpp +552 -0
  36. package/runtime/ext_gui/style.h +29 -1
  37. package/runtime/ext_gui/style_print.cpp +10 -2
  38. package/runtime/ext_gui/style_values.cpp +10 -0
  39. 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
- /** Decode a `data:` URL's base64 payload (the only encoding the engine emits
68
- * and the common one a page assigns). */
69
- bool decodeDataUrl(const std::string &url, std::vector<unsigned char> *out) {
70
- const size_t comma = url.find(',');
71
- if (comma == std::string::npos) return false;
72
- const std::string header = url.substr(0, comma);
73
- if (header.find(";base64") == std::string::npos) return false;
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((url.size() - comma) * 3 / 4 + 4);
155
+ out->reserve(text.size() * 3 / 4 + 4);
85
156
  uint32_t accumulator = 0;
86
157
  int bits = 0;
87
- for (size_t i = comma + 1; i < url.size(); i++) {
88
- const unsigned char c = (unsigned char)url[i];
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(raw_path);
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
- if (decodeDataUrl(raw_path, &bytes)) {
117
- int width = 0;
118
- int height = 0;
119
- int channels = 0;
120
- unsigned char *data = stbi_load_from_memory(bytes.data(), (int)bytes.size(), &width, &height,
121
- &channels, 4);
122
- if (data != nullptr) {
123
- image.width = width;
124
- image.height = height;
125
- image.pixels.assign(data, data + (size_t)width * (size_t)height * 4);
126
- stbi_image_free(data);
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(raw_path, std::move(image));
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) { return xt_svg_is_svg(normalizePath(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
 
@@ -23,9 +23,11 @@ struct Image {
23
23
  std::vector<unsigned char> pixels;
24
24
  };
25
25
 
26
- /** Decode an image file (PNG/JPEG/BMP/GIF/TGA or SVG) to RGBA8. Returns
27
- * nullptr on failure. Results (including failures) are cached by path. A
28
- * `file://` prefix and URL percent-encoding are handled.
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 (e.g. a future `data:` URI). */
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 paintBox(const LayoutBox *box, DisplayList &out, const PaintClip &clip,
153
- const paint_detail::FilterChain &parentChain, float viewportWidth,
154
- float viewportHeight) {
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);
@@ -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. Radial ellipses use the shape
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
- Color gradientColorAt(const BackgroundLayer &layer, float t) {
23
- const std::vector<BackgroundGradientStop> &stops = layer.stops;
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 BackgroundLayer &layer, const FilterChain &chain,
79
- const PaintClip &clip, DisplayList &out) {
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
- const int strips = 18;
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
- const int bands = 32;
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`. */