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
@@ -166,8 +166,10 @@ canvas 由脚本重绘而不是引擎重绘,因此开销按绘制调用计。
166
166
  9 参数(本引擎为**目标优先**:`dx, dy, dw, dh, sx, sy, sw, sh`)。完整的仿射变换
167
167
  (旋转/斜切/缩放)会被应用:目标矩形变为平行四边形,每个设备像素反查到源矩形。
168
168
  `source` 可以是 `<img>` 元素(经图像模块解码)、另一个 `<canvas>`、
169
- `OffscreenCanvas` 或 `ImageBitmap`。`imageSmoothingEnabled` 为 `true`(默认)时使用
170
- 双线性采样,为 `false` 时使用最近邻。
169
+ `OffscreenCanvas`、`ImageBitmap`,或——本引擎的扩展——直接传字符串路径/`data:` URL,
170
+ 由图像模块解码,因此 SVG 或位图的 data URL 无需 `<img>` 就能绘制。矢量源按目标尺寸
171
+ (`dw`/`dh`)光栅化,放大时 SVG 依然清晰。`imageSmoothingEnabled` 为 `true`(默认)
172
+ 时使用双线性采样,为 `false` 时使用最近邻。
171
173
 
172
174
  测试钩子:`getImageData(x, y, w, h)` 把区域复制为 `{ width, height, data }`,其中
173
175
  `data` 是按行主序的 RGBA8 字节数组。越界像素为透明。
@@ -186,8 +188,8 @@ dirtyWidth, dirtyHeight])` 直接把字节写入位图(不受变换、混合
186
188
  `OffscreenCanvas.convertToBlob()` 解析为同一形状,`transferToImageBitmap()` 对位图
187
189
  做快照。
188
190
 
189
- CSS 背景:`background-image: url(...)` 接受文件路径或 `data:` URL,因此可以先把
190
- canvas 导出再作为 CSS 背景使用
191
+ CSS 背景:`background-image: url(...)` 接受文件路径或 `data:` URL(位图载荷或 SVG
192
+ 文档,base64 或百分号编码均可),因此可以先把 canvas 导出再作为 CSS 背景使用
191
193
  (`element.style.backgroundImage = "url(" + canvas.toDataURL() + ")"`)。图像层会拉伸
192
194
  到元素的边框盒;`background-size`、`background-position` 与平铺尚未建模。
193
195
 
@@ -43,22 +43,25 @@
43
43
  `var()` 自定义属性、`inset` 简写、`box-sizing`、`visibility`/`pointer-events`、
44
44
  `@media` 查询、生成内容(`::before`/`::after`)、线性/径向多层背景、
45
45
  `text-shadow`、`clip-path`(`inset`/`circle`/`ellipse`/`polygon`)以及 `filter`
46
- 的颜色函数。仍有限制:`mix-blend-mode`、`background-clip`、
47
- `background-position`/`size`/`repeat`、重复渐变与 `backdrop-filter` 合成都未实现
48
- (`backdrop-filter` 会解析并参与层叠,但采样背景需要离屏图层,因此不绘制);
46
+ 的颜色函数。仍有限制:`background-clip`、`background-position`/`size`/`repeat`
47
+ 与 `backdrop-filter` 合成都未实现(`backdrop-filter` 会解析并参与层叠,但采样
48
+ 背景需要离屏图层,因此不绘制);`mix-blend-mode` 与重复渐变已实现(见
49
+ [渲染](./rendering.md)),但组 `opacity` 是按图形 alpha 近似而非真正的组合成,
50
+ 且 D3D12/DXIL 后端没有混合合成器,那里会退回普通 source-over 绘制;
49
51
  `filter: blur()` 是按图形做九次偏移的近似,而不是真正的组模糊,重叠的子元素会加深;
50
52
  `drop-shadow` 用盒子轮廓近似;最多只交叉两层嵌套 `clip-path`(更深时保留外侧两层);
51
53
  图像/画布除图像着色器能携带的亮度与透明度染色外不做颜色滤镜。Canvas 纹理使用
52
- 最近邻采样。背景渐变仍是纯色带(线性 18 条、径向 32 个同心椭圆),柔和渐变可能
53
- 出现轻微条带。
54
+ 最近邻采样。背景渐变仍是纯色带(线性 18 条、径向 32 个同心椭圆;重复渐变
55
+ 64 条),柔和渐变可能出现轻微条带。
54
56
  - **SVG 是 NanoSVG 的子集。** SVG *文件*(`<img src="logo.svg">`)与内联
55
57
  `<svg>` 子树共用 vendored 的 NanoSVG 解析/光栅化器(`runtime/ext_gui/svg.cpp`、
56
58
  `inline_svg.cpp`),它会渲染 `path`/`rect`/`circle`/`ellipse`/`line`/
57
59
  `polyline`/`polygon`、组、`defs` 渐变以及基于 `class` 的 `<style>` 规则。
58
60
  `<text>`、`<use>`、滤镜、遮罩与裁剪路径不会渲染,文档层叠的 CSS 也不会进入
59
- 图像内部(这里的 `fill`/`stroke` 是 SVG 表现属性,不是 CSS 属性)。需要这些
60
- 能力的矢量美术必须预先光栅化、用盒子绘制,或限制在受支持的语法内。参见
61
- [媒体](./media.md#图像m6a)。
61
+ 图像内部(这里的 `fill`/`stroke` 是 SVG 表现属性,不是 CSS 属性)。因此只靠滤镜
62
+ 出图的 SVG(`feTurbulence` 噪声、模糊等)会显示未经过滤的形状——常见情况是一个
63
+ 纯色矩形——而不是滤镜结果。需要这些能力的矢量美术必须预先光栅化、用盒子绘制,
64
+ 或限制在受支持的语法内。参见[媒体](./media.md#图像m6a)。
62
65
  - **每次变更仍是整文档重算。** 一次 DOM 变更会重新计算层叠并重新布局整个文档
63
66
  (没有增量子树失效),只影响绘制的变更(例如 hover 颜色)同样会重排。带生成
64
67
  内容(`::before`/`::after`)的文档没有特殊处理。各阶段的实测开销由
@@ -35,6 +35,13 @@ PNG/JPEG/BMP/GIF/TGA 解码为 RGBA8,并带有解码缓存与仅头文件的
35
35
  (最近最少使用),动画文档不会让它无限增长。
36
36
  - 解析结果按路径缓存;解析与光栅化器状态在 `xt_svg_shutdown`
37
37
  (`xt_image_shutdown`)中释放。
38
+ - `data:` URL 在内存中解码,凡接受路径的地方都可使用:`<img src>`、CSS
39
+ `background-image` 与 `canvas.drawImage(url, …)`。既支持 `;base64` 形式,也支持
40
+ 百分号编码(或原样)的载荷,`base64` 标志大小写不敏感。MIME 类型只用于分类:
41
+ `image/svg+xml`(或 MIME 为通用/缺失时按内容嗅探出 SVG)走 NanoSVG——data URL
42
+ 里的 SVG 与 SVG 文件一样按绘制尺寸光栅化;PNG/JPEG/BMP/GIF/TGA 走
43
+ `stb_image`。NanoSVG 的子集限制仍然生效:只靠 `<filter>`(`feTurbulence` 等)
44
+ 出图的 SVG 能解码、能布局,但滤镜本身会被忽略。
38
45
 
39
46
  文档 CSS 只作用于 `<svg>` **盒子**——尺寸、外边距、背景、边框、`transform`、
40
47
  opacity、`display`——与任何其他元素一样。盒子内部的艺术内容保持 SVG 语义:
@@ -45,8 +52,8 @@ opacity、`display`——与任何其他元素一样。盒子内部的艺术内
45
52
  重写艺术内容的程序会为每个变体保留一张纹理;更好的做法是用 `transform`、
46
53
  `opacity` 给盒子做动画,或在一小组文档之间切换。
47
54
 
48
- 图像仍需完成:CSS `background-image: url(...)`、`data:` URI、`object-fit`、
49
- 九宫格边框。
55
+ 图像仍需完成:`object-fit`、九宫格边框。(CSS `background-image: url(...)` 与
56
+ `data:` URL 已支持。)
50
57
 
51
58
  两种 SVG 都受限于 NanoSVG 的子集:`path`、`rect`、`circle`、`ellipse`、
52
59
  `line`、`polyline`、`polygon`、组(`g`)、带线性/径向渐变的 `defs` 以及基于
@@ -15,9 +15,10 @@
15
15
  顺序都能保留到渲染器。`DisplayList::dump()` 为 `paintList()`/`paintCount()`
16
16
  提供数据。
17
17
 
18
- `opacity` 只在两端生效:计算值为 `0` 的盒子(及其整棵子树)不绘制任何内容,
19
- 大于 `0` 的一律按完全不透明绘制。图形只携带其颜色自身的 alpha,因此没有部分
20
- 透明盒子的合成——CSS 解析器接受的 `opacity` 过渡因此也看不到效果。把隐藏态写成
18
+ `opacity` 会乘到该盒子及其后代绘制的每个颜色 alpha 上(沿绘制链逐层相乘),
19
+ 因此小数 opacity 以及 CSS 解析器接受的 `opacity` 过渡都能看到效果。这是按图形
20
+ 逐个近似的,而不是离屏分组:半透明盒子的重叠子元素会各自合成,浏览器则先把整组
21
+ 合成一次再应用 alpha。`opacity: 0` 仍会跳过整棵子树。把隐藏态写成
21
22
  `opacity: 0` 来隐藏/显示一层,正是 `examples/gui/pelican-bike` 游戏结束遮罩的做法。
22
23
 
23
24
  `runtime/ext_gui/renderer.{h,cpp}` 把该列表转换为每个窗口的批处理顶点缓冲
@@ -67,14 +68,20 @@
67
68
  `background-color` 与每一条 `background-image` 层都会绘制。逗号分隔的层列表按
68
69
  从底到上的顺序绘制(声明中的第一层在最上面),两种渐变均受支持:
69
70
 
70
- - **线性**渐变在 18 个位置采样并输出为纯色矩形;支持 `to <side>`、
71
- `deg`/`grad`/`rad`/`turn`、百分比与多色标(色带沿最近轴行走,因此斜角会退化为
71
+ - **线性**渐变在 18 个位置采样(重复渐变 64 个)并输出为纯色矩形;支持
72
+ `to <side>`、`deg`/`grad`/`rad`/`turn`、百分比或长度(`px`/`em`/…)偏移、
73
+ `color <start> <end>` 简写与多色标(色带沿最近轴行走,因此斜角会退化为
72
74
  最近的轴向);
73
- - **径向**渐变是 32 个同心椭圆。每个都是单位半径的圆角矩形,经图形管线的
74
- transform 槽映射,因此圆/椭圆仍保留距离场抗锯齿。支持形状/尺寸关键字
75
- (`circle`/`ellipse`、`closest-side`/`farthest-side`/`closest-corner`/
75
+ - **径向**渐变是 32 个同心椭圆(重复渐变 64 个)。每个都是单位半径的圆角矩形,
76
+ 经图形管线的 transform 槽映射,因此圆/椭圆仍保留距离场抗锯齿。支持形状/尺寸
77
+ 关键字(`circle`/`ellipse`、`closest-side`/`farthest-side`/`closest-corner`/
76
78
  `farthest-corner`、显式半径)与 `at <position>`。
77
79
 
80
+ `repeating-linear-gradient()` 与 `repeating-radial-gradient()` 会在整个盒子上
81
+ 从 0 平铺到最后一个色标的偏移量;色标列表不会被强制结束在 100%,因此
82
+ `red 0%, blue 10%` 每 10% 重复一次。普通渐变的最后一个色标若小于 100%,则剩余
83
+ 区域保持该颜色,与 CSS 一致。
84
+
78
85
  背景色位于所有层下方。若渐变色标带透明度,渐变也会半透明;色带近似可能产生
79
86
  轻微条带,非常透明的多色标渐变会出现 alpha 叠加。
80
87
 
@@ -96,9 +103,21 @@ shader 改动,但边缘是硬边(没有解析抗锯齿)。支持两层嵌
96
103
  **`filter`** 的颜色函数(`brightness`、`contrast`、`grayscale`、`sepia`、
97
104
  `invert`、`saturate`、`hue-rotate`、`opacity`)会折进每个绘制颜色,因此盒子与
98
105
  其后代都会看到滤镜。`blur()` 与 `drop-shadow()` 是显示列表层面的近似:模糊把每个
99
- 图形展开成九个半透明副本,投影用盒子/文字轮廓。这样保持单渲染通道(没有离屏
100
- 渲染目标),代价是组语义:被模糊/滤镜的组内重叠后代可能加深;`backdrop-filter`
101
- 无法采样元素背后的内容,因此会解析并参与层叠,但不绘制。
106
+ 图形展开成九个半透明副本,投影用盒子/文字轮廓。这样普通绘制保持单渲染通道,代价
107
+ 是组语义:被模糊/滤镜的组内重叠后代可能加深;`backdrop-filter` 无法采样元素背后
108
+ 的内容,因此会解析并参与层叠,但不绘制。
109
+
110
+ **`mix-blend-mode`** 是唯一真正需要离屏图层的例外。非 `normal` 的盒子会被
111
+ `LayerBegin`/`LayerEnd` 标记包住(在 `paintList()` 中显示为 `layer blend=…` /
112
+ `end-layer`),渲染器把该组绘制到全视口离屏纹理,再用一个小型 GLSL 混合
113
+ shader 合成到场景上。全部十六种 CSS 模式(`multiply`、`screen`、`overlay`、
114
+ `darken`、`lighten`、`color-dodge`、`color-burn`、`hard-light`、`soft-light`、
115
+ `difference`、`exclusion`、`hue`、`saturation`、`color`、`luminosity`)都按
116
+ 合成规范公式实现。shader 在运行时由内置 glslang 编译(Vulkan 用 SPIR-V),
117
+ 再由 SPIRV-Cross 交叉编译为 MSL(Metal);每个嵌套层级的两张场景纹理跨帧复用,
118
+ 并通过 ping-pong 让背景在写入时仍可采样。既不吃 SPIR-V 也不吃 MSL 的后端
119
+ (这里的 D3D12/DXIL 路径没有 shader 编译器)仍用普通 source-over 混合绘制该组。
120
+ `backdrop-filter` 依旧不绘制。
102
121
 
103
122
  ## 文本技术栈(M4b)
104
123
 
package/doc/zh-CN/gui.md CHANGED
@@ -12,7 +12,7 @@
12
12
  渲染器,因此盒子(以及其中的文本、图像与子元素)可以被平移、缩放与旋转而
13
13
  不触发任何重排——再加上 `transition: transform`,这些姿态可以动画(自绘的示例
14
14
  `examples/gui/pelican-svg` 正是用它让矢量画动起来)。参见
15
- [布局](./gui/layout.md#变换m15)与[动画](./gui/animation.md#css-transformm15)。**M17** 增加了真正的 **WebGL 1.0**,**M18** 增加了 **WebGL 2.0**:`<canvas>.getContext("webgl2")` 接受 `#version 300 es` 着色器、顶点数组对象、实例化绘制与采样器对象,全部由内置 glslang 编译为 SPIR-V(Metal 上再由内置 SPIRV-Cross 转成 MSL)并通过 SDL_GPU 渲染,几何、光照、纹理采样与混合都在 GPU 上完成,页面直接合成颜色纹理而无须读回——见 [WebGL](./gui/webgl.md)。**M19** 补齐 CSS 装饰层:`@media` 查询、生成内容(`::before`/`::after`)、线性/径向多层背景、`text-shadow`、`clip-path` 与 `filter` 颜色/阴影效果。`backdrop-filter` 与真正的组模糊需要离屏合成器,仍为有文档说明的近似或未支持——见[绘制](./gui/rendering.md#背景生成内容与特效m19)。**M20** 让大文档保持可交互:`@media` 每条规则只求值一次,布局与绘制共享一个有界的整形缓存,一帧只做(也只提交)变更所要求的工作——见[性能](./gui/rendering.md#性能m20)。本总览记录了锁定决策、架构、里程碑计划与当前进度;引擎本身按功能拆分在以上各页中。
15
+ [布局](./gui/layout.md#变换m15)与[动画](./gui/animation.md#css-transformm15)。**M17** 增加了真正的 **WebGL 1.0**,**M18** 增加了 **WebGL 2.0**:`<canvas>.getContext("webgl2")` 接受 `#version 300 es` 着色器、顶点数组对象、实例化绘制与采样器对象,全部由内置 glslang 编译为 SPIR-V(Metal 上再由内置 SPIRV-Cross 转成 MSL)并通过 SDL_GPU 渲染,几何、光照、纹理采样与混合都在 GPU 上完成,页面直接合成颜色纹理而无须读回——见 [WebGL](./gui/webgl.md)。**M19** 补齐 CSS 装饰层:`@media` 查询、生成内容(`::before`/`::after`)、线性/径向/重复多层背景、`text-shadow`、`clip-path`、`mix-blend-mode`(通过离屏合成器)与 `filter` 颜色/阴影效果。`backdrop-filter` 仍需要背景快照,真正的组模糊也仍是近似——见[绘制](./gui/rendering.md#背景生成内容与特效m19)。**M20** 让大文档保持可交互:`@media` 每条规则只求值一次,布局与绘制共享一个有界的整形缓存,一帧只做(也只提交)变更所要求的工作——见[性能](./gui/rendering.md#性能m20)。本总览记录了锁定决策、架构、里程碑计划与当前进度;引擎本身按功能拆分在以上各页中。
16
16
 
17
17
  ## 目录
18
18
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xbintsc",
3
- "version": "0.5.41",
3
+ "version": "0.5.42",
4
4
  "description": "xbintsc - a TypeScript binary compiler (TypeScript -> LLVM IR -> native binary)",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -72,12 +72,12 @@
72
72
  "vitest": "^2.1.8"
73
73
  },
74
74
  "optionalDependencies": {
75
- "@xbintsc/linux-x64": "0.5.41",
76
- "@xbintsc/linux-arm64": "0.5.41",
77
- "@xbintsc/darwin-x64": "0.5.41",
78
- "@xbintsc/darwin-arm64": "0.5.41",
79
- "@xbintsc/win32-x64": "0.5.41",
80
- "@xbintsc/win32-arm64": "0.5.41"
75
+ "@xbintsc/linux-x64": "0.5.42",
76
+ "@xbintsc/linux-arm64": "0.5.42",
77
+ "@xbintsc/darwin-x64": "0.5.42",
78
+ "@xbintsc/darwin-arm64": "0.5.42",
79
+ "@xbintsc/win32-x64": "0.5.42",
80
+ "@xbintsc/win32-arm64": "0.5.42"
81
81
  },
82
82
  "engines": {
83
83
  "node": ">=22"
@@ -468,41 +468,59 @@ METHOD(canvas_draw_image) {
468
468
  (void)env;
469
469
  xtgui::Canvas2D *canvas = context_canvas(self);
470
470
  if (canvas == nullptr || argc < 3) return XT_UNDEFINED;
471
+
472
+ const bool has_destination = argc >= 5;
473
+ const float dx = (float)number_arg(argc, argv, 1, 0.0);
474
+ const float dy = (float)number_arg(argc, argv, 2, 0.0);
475
+ const float destination_width =
476
+ has_destination ? (float)number_arg(argc, argv, 3, 0.0) : 0.0f;
477
+ const float destination_height =
478
+ has_destination ? (float)number_arg(argc, argv, 4, 0.0) : 0.0f;
479
+ auto raster_target = [](float value) -> int {
480
+ return value > 0.0f ? (int)(value + 0.5f) : 0;
481
+ };
482
+
471
483
  XtGuiWindow *win = nullptr;
472
484
  xt_value source_value = xt_arg(argc, argv, 0);
473
485
  const xtgui::Image *image = nullptr;
474
- /* An `OffscreenCanvas` (or a transferred one) has no DOM node; its bitmap is
475
- * reachable through the process-wide canvas id. */
476
- if (XT_IS_OBJECT(source_value)) {
477
- xt_value canvas_id = xt_object_get_cstr(source_value, "__xt_gui_canvas");
478
- if (XT_IS_NUMBER(canvas_id)) {
479
- xtgui::Canvas2D *offscreen = xtgui::xt_canvas_by_id((uint64_t)xt_to_number(canvas_id));
480
- if (offscreen != nullptr) {
481
- image = &offscreen->image();
486
+ if (XT_IS_STRING(source_value)) {
487
+ /* Engine extension: the source may be a path or a `data:` URL itself, so a
488
+ * data-URL bitmap/SVG can be drawn without an `<img>` element. */
489
+ const std::string source_url(xt_string_data(source_value));
490
+ image = xtgui::xt_image_load(source_url, raster_target(destination_width),
491
+ raster_target(destination_height));
492
+ } else {
493
+ /* An `OffscreenCanvas` (or a transferred one) has no DOM node; its bitmap
494
+ * is reachable through the process-wide canvas id. */
495
+ if (XT_IS_OBJECT(source_value)) {
496
+ xt_value canvas_id = xt_object_get_cstr(source_value, "__xt_gui_canvas");
497
+ if (XT_IS_NUMBER(canvas_id)) {
498
+ xtgui::Canvas2D *offscreen = xtgui::xt_canvas_by_id((uint64_t)xt_to_number(canvas_id));
499
+ if (offscreen != nullptr) image = &offscreen->image();
500
+ }
501
+ }
502
+ if (image == nullptr) {
503
+ xtgui::Node *source = resolve_node(source_value, &win);
504
+ if (source != nullptr && source->isElement() && source->isTag("img")) {
505
+ const std::string *src = source->attr("src");
506
+ if (src != nullptr) {
507
+ image = xtgui::xt_image_load(*src, raster_target(destination_width),
508
+ raster_target(destination_height));
509
+ }
510
+ } else if (source != nullptr) {
511
+ xtgui::Canvas2D *other = xt_gui_canvas_find(win, source);
512
+ if (other != nullptr) image = &other->image();
482
513
  }
483
514
  }
484
- }
485
- xtgui::Node *source = resolve_node(source_value, &win);
486
- if (source != nullptr && source->isElement() && source->isTag("img")) {
487
- const std::string *src = source->attr("src");
488
- if (src != nullptr) image = xtgui::xt_image_load(*src);
489
- } else if (source != nullptr) {
490
- xtgui::Canvas2D *other = xt_gui_canvas_find(win, source);
491
- if (other != nullptr) image = &other->image();
492
515
  }
493
516
  if (image == nullptr || image->width <= 0 || image->height <= 0) return XT_UNDEFINED;
494
- float dx = (float)number_arg(argc, argv, 1, 0.0);
495
- float dy = (float)number_arg(argc, argv, 2, 0.0);
496
- float dw = (float)image->width;
497
- float dh = (float)image->height;
517
+
518
+ const float dw = has_destination ? destination_width : (float)image->width;
519
+ const float dh = has_destination ? destination_height : (float)image->height;
498
520
  float sx = 0.0f;
499
521
  float sy = 0.0f;
500
522
  float sw = (float)image->width;
501
523
  float sh = (float)image->height;
502
- if (argc >= 5) {
503
- dw = (float)number_arg(argc, argv, 3, (double)dw);
504
- dh = (float)number_arg(argc, argv, 4, (double)dh);
505
- }
506
524
  if (argc >= 9) {
507
525
  sx = (float)number_arg(argc, argv, 5, 0.0);
508
526
  sy = (float)number_arg(argc, argv, 6, 0.0);
@@ -134,67 +134,59 @@ bool parseGradientColor(const std::string &token, Color *color, std::string *res
134
134
  return true;
135
135
  }
136
136
 
137
- /** Parse one `[<percent>]` / `<number>` offset. */
138
- bool parseGradientOffset(const std::string &raw, float *out) {
137
+ /** Parse one gradient stop offset: a percentage, a length (`px`, `em`, …) or a
138
+ * bare number (0.5 = 50%, 50 = 50%). */
139
+ bool parseGradientOffset(const std::string &raw, Length *out) {
139
140
  std::string text = xt_css_trim(raw);
140
141
  if (text.empty()) return false;
141
- bool percent = text.back() == '%';
142
- if (percent) text.pop_back();
142
+ if (text.back() == '%') {
143
+ float value = 0;
144
+ if (!xt_css_parse_number(text.substr(0, text.size() - 1), &value)) return false;
145
+ *out = Length{value, Unit::Percent};
146
+ return true;
147
+ }
143
148
  float value = 0;
144
- if (!xt_css_parse_number(text, &value)) return false;
145
- if (percent) value /= 100.0f;
146
- else if (value > 1.0f) value /= 100.0f;
147
- if (value < 0) value = 0;
148
- if (value > 1) value = 1;
149
- *out = value;
150
- return true;
149
+ if (xt_css_parse_number(text, &value)) {
150
+ /* Unitless offsets are fractions of the gradient line, matching the old
151
+ * parser: `0.5` is 50% and `50` is also 50%. */
152
+ if (value > 1.0f) *out = Length{value, Unit::Percent};
153
+ else *out = Length{value * 100.0f, Unit::Percent};
154
+ return true;
155
+ }
156
+ Length length = xt_css_parse_length(text);
157
+ if (length.unit != Unit::Invalid && length.unit != Unit::Auto &&
158
+ length.unit != Unit::None && length.unit != Unit::Number) {
159
+ *out = length;
160
+ return true;
161
+ }
162
+ return false;
151
163
  }
152
164
 
153
- /** Parse a comma-separated `[color [offset [offset]]]` stop list. */
165
+ /** Parse a comma-separated `[color [offset [offset]]]` stop list. Offsets stay
166
+ * unresolved here: `px` is resolved against the gradient line in paint. */
154
167
  bool parseStops(const std::string &text, std::vector<BackgroundGradientStop> *stops) {
155
168
  std::vector<std::string> parts = splitCommas(text);
156
169
  if (parts.size() < 2) return false;
157
- float last = -1.0f;
158
170
  for (const std::string &part : parts) {
159
171
  Color color;
160
172
  std::string rest;
161
173
  if (!parseGradientColor(part, &color, &rest)) continue;
162
174
  BackgroundGradientStop stop;
163
175
  stop.color = color;
164
- stop.offset = -1.0f;
165
- if (!rest.empty()) {
166
- std::vector<std::string> offsets = splitTokens(rest);
167
- if (!offsets.empty()) {
168
- float first = 0;
169
- if (parseGradientOffset(offsets[0], &first)) {
170
- stop.offset = first;
171
- if (stop.offset < last) stop.offset = last;
172
- last = stop.offset;
173
- }
176
+ std::vector<std::string> offsets = splitTokens(rest);
177
+ if (!offsets.empty() && parseGradientOffset(offsets[0], &stop.offset)) {
178
+ /* `color <start> <end>` is shorthand for the same colour at both offsets
179
+ * (a hard band), so emit the second stop too. */
180
+ stops->push_back(stop);
181
+ if (offsets.size() > 1) {
182
+ BackgroundGradientStop end = stop;
183
+ if (parseGradientOffset(offsets[1], &end.offset)) stops->push_back(end);
174
184
  }
185
+ } else {
186
+ stops->push_back(stop);
175
187
  }
176
- stops->push_back(stop);
177
- }
178
- if (stops->size() < 2) return false;
179
- /* Fill implicit offsets by distributing them between their neighbours. */
180
- size_t i = 0;
181
- while (i < stops->size()) {
182
- if ((*stops)[i].offset >= 0) {
183
- i++;
184
- continue;
185
- }
186
- size_t j = i;
187
- while (j < stops->size() && (*stops)[j].offset < 0) j++;
188
- float before = (i == 0) ? 0.0f : (*stops)[i - 1].offset;
189
- float after = (j >= stops->size()) ? 1.0f : (*stops)[j].offset;
190
- size_t count = j - i;
191
- for (size_t k = 0; k < count; k++)
192
- (*stops)[i + k].offset = before + (after - before) * (float)(k + 1) / (float)(count + 1);
193
- i = j;
194
188
  }
195
- stops->front().offset = 0.0f;
196
- stops->back().offset = 1.0f;
197
- return true;
189
+ return stops->size() >= 2;
198
190
  }
199
191
 
200
192
  /** CSS gradient direction/angle. Returns true when `text` was a direction. */
@@ -395,14 +387,18 @@ bool parseRadialGradient(const std::string &body, BackgroundLayer *layer) {
395
387
  /** Parse one layer, appending it when it is a supported gradient or image. */
396
388
  bool parseLayer(const std::string &part, BackgroundLayer *layer) {
397
389
  std::string body;
398
- if (findCall(part, "linear-gradient", &body) ||
399
- findCall(part, "repeating-linear-gradient", &body)) {
390
+ /* The repeating prefix is checked first: a plain substring search for
391
+ * `linear-gradient(` would also match inside `repeating-linear-gradient(`. */
392
+ if (findCall(part, "repeating-linear-gradient", &body)) {
393
+ layer->repeating = true;
400
394
  return parseLinearGradient(body, layer);
401
395
  }
402
- if (findCall(part, "radial-gradient", &body) ||
403
- findCall(part, "repeating-radial-gradient", &body)) {
396
+ if (findCall(part, "linear-gradient", &body)) return parseLinearGradient(body, layer);
397
+ if (findCall(part, "repeating-radial-gradient", &body)) {
398
+ layer->repeating = true;
404
399
  return parseRadialGradient(body, layer);
405
400
  }
401
+ if (findCall(part, "radial-gradient", &body)) return parseRadialGradient(body, layer);
406
402
  if (findCall(part, "url", &body)) {
407
403
  std::string source = xt_css_trim(body);
408
404
  if (source.size() >= 2 && (source.front() == '"' || source.front() == '\'') &&
@@ -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, pass, &win->geometry, &win->text_geometry,
488
- &win->image_geometry, viewport_width, viewport_height, density);
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 {