@react-native-gpui/svg 0.0.0-stage → 15.15.0-alpha.1
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/CHANGELOG.md +21 -0
- package/LICENSE +21 -0
- package/README.md +32 -2
- package/desktop/CMakeLists.txt +8 -0
- package/desktop/Cargo.toml +13 -0
- package/desktop/build.rs +38 -0
- package/desktop/cpp/GpuiSvgModule.cpp +178 -0
- package/desktop/src/cache.rs +156 -0
- package/desktop/src/key.rs +173 -0
- package/desktop/src/lib.rs +609 -0
- package/desktop/src/raster.rs +477 -0
- package/package.json +32 -4
- package/react-native.config.js +51 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@react-native-gpui/svg` are listed here. Each version's section becomes the notes of its GitHub release.
|
|
4
|
+
|
|
5
|
+
## 15.15.0-alpha.1 - 2026-11-12
|
|
6
|
+
|
|
7
|
+
- A drawing is looked up by a structural hash of its props, children, frame size and scale, so a render that finds it builds no document string. Before, every render built and escaped the whole SVG document just to hash it, even when the raster was cached.
|
|
8
|
+
- The rasters are kept in one cache shared by every node, bounded by bytes (64 MB; `RN_GPUI_SVG_CACHE_MB` sets another) and dropping the image used longest ago first. It held weak references before, so an icon that left the screen was rasterised again when it came back, and the same icon in two places was drawn twice.
|
|
9
|
+
- A drawing that is not cached is rasterised on a background thread when it is large (over about 48 CSS pixels square at a scale of 2) or has a filter, or when its node already shows an image (a theme switch). The node keeps showing its previous image until the new one is ready, then is built again; a node with nothing to show yet draws nothing until then. A small icon whose node has no image is still drawn at once, so a row that scrolls in has its icon on the first frame. Several nodes asking for the same drawing share one rasterisation.
|
|
10
|
+
- The four coverage samples of a drawing are rasterised side by side (a small one, under 128 by 128 pixels, one after the other, because starting a thread costs more than a sample) and combined in a fixed order, so the pixels are the same as when they were drawn one after the other (a test compares them in both ways). A 230 point illustration with drop shadows takes 30 ms to rasterise instead of 120, and now off the main thread.
|
|
11
|
+
- `RN_GPUI_SVG_TRACE=1` logs each rasterisation with the time of every sample, and `RN_GPUI_SVG_SYNC=1` draws everything on the main thread, for measuring.
|
|
12
|
+
- An SVG is drawn with the same antialiasing as Chrome: once per coverage sample at the window's scale, without antialiasing, with the four sample positions Chrome uses, and averaged. An icon's edge now takes the same five coverage steps (0, 1/4, 1/2, 3/4, 1) at the same pixels, so the Material star at 16 points differs from Chrome in 5 of 1024 pixels instead of 112, and over 80 icons in 124,480 pixels in 1,288 instead of 7,250. Needs `image` and `smallvec` in the crate.
|
|
13
|
+
- An SVG is placed at a whole CSS pixel, as Chrome places an inline SVG: a 15 point icon centred in a 20 point button, which lays out at 2.5, is drawn at 3 instead of half a point to the left. That half point was the bulk of the edge pixels that differed from the web app (97 of 1,600 in a toolbar arrow, 2 now).
|
|
14
|
+
- An element's `transform` is applied: react-native-svg sends it as a six number `matrix`, which was only read when it had sixteen, so every rotated or scaled shape was drawn untransformed.
|
|
15
|
+
- Filters are drawn: `filter` on a shape or group, `<filter>` and the `feBlend`, `feColorMatrix`, `feComposite`, `feFlood`, `feGaussianBlur`, `feMerge` and `feOffset` primitives reach the renderer, with `sRGB` colour interpolation. An illustration with rotated cards and drop shadows at 230 points differs from Chrome in 249 of 211,600 pixels by more than 8 levels instead of 14,817.
|
|
16
|
+
- Asks core for each node's subtree, which core now hands a component renderer only when it reads it.
|
|
17
|
+
|
|
18
|
+
## 15.15.0-alpha.0 - 2026-11-01
|
|
19
|
+
|
|
20
|
+
- First release, for `react-native-svg` 15.15.x.
|
|
21
|
+
- Renders SVG elements with the desktop renderer.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Streetwriters
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,33 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @react-native-gpui/svg
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Adapter for [`react-native-svg`](https://github.com/software-mansion/react-native-svg) (MIT): draws SVG elements with the desktop renderer.
|
|
4
|
+
|
|
5
|
+
Part of [react-native-gpui](https://github.com/streetwriters/react-native-gpui), React Native for macOS, Windows and Linux.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
Install it next to `react-native-svg`:
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
npm install react-native-svg@15.15.x @react-native-gpui/svg
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Keep using `react-native-svg` as you do on iOS and Android; its JavaScript API is unchanged. Autolinking builds the desktop side when the app is built.
|
|
16
|
+
|
|
17
|
+
## What is drawn
|
|
18
|
+
|
|
19
|
+
Shapes, paths, text, gradients, masks, clip paths, symbols, patterns and markers, with the transform of each element (react-native-svg sends it as a six number matrix). Filters are drawn too: `filter` on a shape or group and the `feBlend`, `feColorMatrix`, `feComposite`, `feFlood`, `feGaussianBlur`, `feMerge` and `feOffset` primitives, always with sRGB colour interpolation. An icon is drawn with the same antialiasing as Chrome and placed at a whole CSS pixel, as Chrome places an inline SVG.
|
|
20
|
+
|
|
21
|
+
Limits: `overflow="visible"` is not honoured (the drawing is clipped to the SVG's frame), the other filter primitives such as `feDropShadow` and `feMorphology` are not passed to the renderer, and a blurred shadow comes out about one level lighter than Chrome's for each shadow that overlaps another.
|
|
22
|
+
|
|
23
|
+
## Cost
|
|
24
|
+
|
|
25
|
+
A drawing is cached by a hash of what it is made of, so drawing it again costs a hash and a lookup. The cache is shared by every node and holds up to 64 MB of images (`RN_GPUI_SVG_CACHE_MB` changes that). A drawing that is not cached yet is rasterised on a background thread when it is large or filtered, and the node shows its previous image, if it has one, until the new one is ready; a small icon with nothing yet to show is drawn at once. `RN_GPUI_SVG_TRACE=1` logs each rasterisation and `RN_GPUI_SVG_SYNC=1` draws on the main thread, to measure.
|
|
26
|
+
|
|
27
|
+
## Documentation
|
|
28
|
+
|
|
29
|
+
[Supported libraries](https://streetwriters.github.io/react-native-gpui/docs/supported-libraries)
|
|
30
|
+
|
|
31
|
+
## License
|
|
32
|
+
|
|
33
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# SVG shared Fabric props/shadow-node descriptors are supplied by RN Codegen
|
|
2
|
+
# and react-native-svg's published common C++ shadow-node headers.
|
|
3
|
+
set(_svg_codegen_dir "${RN_GPUI_PROJECT_ROOT}/desktop/autolink/codegen/rnsvg")
|
|
4
|
+
set_property(GLOBAL APPEND PROPERTY RN_GPUI_PACKAGE_MODULE_SOURCES
|
|
5
|
+
"${CMAKE_CURRENT_LIST_DIR}/cpp/GpuiSvgModule.cpp")
|
|
6
|
+
set_property(GLOBAL APPEND PROPERTY RN_GPUI_PACKAGE_MODULE_INSTALLERS svg)
|
|
7
|
+
set_property(GLOBAL APPEND PROPERTY RN_GPUI_PACKAGE_MODULE_INCLUDE_DIRS
|
|
8
|
+
"${_svg_codegen_dir}")
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "rn-gpui-svg"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
edition = "2021"
|
|
5
|
+
|
|
6
|
+
[dependencies]
|
|
7
|
+
react-native-gpui = { version = "0.1" }
|
|
8
|
+
rn-gpui-sys = { version = "0.1" }
|
|
9
|
+
gpui = { version = "0.2" }
|
|
10
|
+
serde_json = "1"
|
|
11
|
+
# The same versions gpui is built with, for the averaged image it draws.
|
|
12
|
+
image = { version = "0.25", default-features = false }
|
|
13
|
+
smallvec = "1"
|
package/desktop/build.rs
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
//! Puts the C++ runtime's shared libraries on this crate's test binary rpath.
|
|
2
|
+
//!
|
|
3
|
+
//! `rn-gpui-svg`'s tests build gpui trees, which link `libjsi.dylib` and the rest
|
|
4
|
+
//! of the Node-API runtime. Core's `build.rs` emits
|
|
5
|
+
//! `cargo:rustc-link-arg=-Wl,-rpath,<dir>` for its own binary — the app — and that
|
|
6
|
+
//! flag is not inherited by a dependency's test binary, so the test process
|
|
7
|
+
//! aborted in dyld with `Library not loaded: @rpath/libjsi.dylib` before running a
|
|
8
|
+
//! single test.
|
|
9
|
+
//!
|
|
10
|
+
//! Core exports the directories it was given as `DEP_RN_GPUI_CORE_RPATHS`, which
|
|
11
|
+
//! is the same variable core's own `build.rs` reads. Re-emitting it here scopes
|
|
12
|
+
//! the flag to this crate's test binary, since `cargo:rustc-link-arg` without a
|
|
13
|
+
//! target suffix applies to the crate's own targets and not to the app that
|
|
14
|
+
//! depends on it.
|
|
15
|
+
|
|
16
|
+
fn main() {
|
|
17
|
+
println!("cargo:rerun-if-env-changed=DEP_RN_GPUI_CORE_RPATHS");
|
|
18
|
+
println!("cargo:rerun-if-changed=build.rs");
|
|
19
|
+
|
|
20
|
+
let Ok(dirs) = std::env::var("DEP_RN_GPUI_CORE_RPATHS") else {
|
|
21
|
+
return;
|
|
22
|
+
};
|
|
23
|
+
let dirs = dirs
|
|
24
|
+
.split('|')
|
|
25
|
+
.filter(|dir| !dir.is_empty())
|
|
26
|
+
.collect::<Vec<_>>();
|
|
27
|
+
if dirs.is_empty() {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
// MSVC and MinGW have no rpath, and the flag is rejected outright; Windows
|
|
31
|
+
// finds the DLLs through PATH, which the test runner already arranges.
|
|
32
|
+
if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("windows") {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
for dir in dirs {
|
|
36
|
+
println!("cargo:rustc-link-arg=-Wl,-rpath,{dir}");
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
#include "GpuiModules.h"
|
|
2
|
+
#include "MountBatchBuilder.h"
|
|
3
|
+
|
|
4
|
+
#include <react/renderer/components/rnsvg/Props.h>
|
|
5
|
+
|
|
6
|
+
#include <folly/json.h>
|
|
7
|
+
#include <string>
|
|
8
|
+
#include <type_traits>
|
|
9
|
+
#include <vector>
|
|
10
|
+
|
|
11
|
+
namespace rngpui {
|
|
12
|
+
using namespace facebook::react;
|
|
13
|
+
|
|
14
|
+
namespace {
|
|
15
|
+
constexpr const char* kSvgComponents[] = {
|
|
16
|
+
"RNSVGSvgView", "RNSVGCircle", "RNSVGEllipse",
|
|
17
|
+
"RNSVGLine", "RNSVGPath", "RNSVGRect",
|
|
18
|
+
"RNSVGGroup", "RNSVGDefs", "RNSVGClipPath",
|
|
19
|
+
"RNSVGLinearGradient", "RNSVGRadialGradient", "RNSVGText",
|
|
20
|
+
"RNSVGTSpan", "RNSVGFilter", "RNSVGFeBlend",
|
|
21
|
+
"RNSVGFeColorMatrix", "RNSVGFeComposite", "RNSVGFeFlood",
|
|
22
|
+
"RNSVGFeGaussianBlur", "RNSVGFeMerge", "RNSVGFeOffset",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
template <typename T>
|
|
26
|
+
requires(!std::is_enum_v<T>)
|
|
27
|
+
folly::dynamic svgJsonValue(const T& value) {
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
template <typename T>
|
|
32
|
+
requires std::is_enum_v<T>
|
|
33
|
+
folly::dynamic svgJsonValue(const T& value) {
|
|
34
|
+
return toString(value);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
folly::dynamic svgJsonValue(const Float& value) {
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
folly::dynamic svgJsonValue(const SharedColor& value) {
|
|
41
|
+
return value ? folly::dynamic(argb(value)) : folly::dynamic(nullptr);
|
|
42
|
+
}
|
|
43
|
+
folly::dynamic svgJsonValue(const std::vector<std::string>& values) {
|
|
44
|
+
folly::dynamic result = folly::dynamic::array();
|
|
45
|
+
for (const auto& value : values)
|
|
46
|
+
result.push_back(value);
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
folly::dynamic svgJsonValue(const std::vector<Float>& values) {
|
|
50
|
+
folly::dynamic result = folly::dynamic::array();
|
|
51
|
+
for (auto value : values)
|
|
52
|
+
result.push_back(value);
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
template <typename T>
|
|
57
|
+
folly::dynamic svgPropsJson(const T& props) {
|
|
58
|
+
folly::dynamic result = folly::dynamic::object();
|
|
59
|
+
#define RN_GPUI_COPY_SVG_PROP(name) \
|
|
60
|
+
if constexpr (requires { props.name; }) \
|
|
61
|
+
result[#name] = svgJsonValue(props.name)
|
|
62
|
+
RN_GPUI_COPY_SVG_PROP(name);
|
|
63
|
+
RN_GPUI_COPY_SVG_PROP(d);
|
|
64
|
+
RN_GPUI_COPY_SVG_PROP(x);
|
|
65
|
+
RN_GPUI_COPY_SVG_PROP(y);
|
|
66
|
+
RN_GPUI_COPY_SVG_PROP(x1);
|
|
67
|
+
RN_GPUI_COPY_SVG_PROP(y1);
|
|
68
|
+
RN_GPUI_COPY_SVG_PROP(x2);
|
|
69
|
+
RN_GPUI_COPY_SVG_PROP(y2);
|
|
70
|
+
RN_GPUI_COPY_SVG_PROP(cx);
|
|
71
|
+
RN_GPUI_COPY_SVG_PROP(cy);
|
|
72
|
+
RN_GPUI_COPY_SVG_PROP(r);
|
|
73
|
+
RN_GPUI_COPY_SVG_PROP(rx);
|
|
74
|
+
RN_GPUI_COPY_SVG_PROP(ry);
|
|
75
|
+
RN_GPUI_COPY_SVG_PROP(width);
|
|
76
|
+
RN_GPUI_COPY_SVG_PROP(height);
|
|
77
|
+
RN_GPUI_COPY_SVG_PROP(vbWidth);
|
|
78
|
+
RN_GPUI_COPY_SVG_PROP(vbHeight);
|
|
79
|
+
RN_GPUI_COPY_SVG_PROP(minX);
|
|
80
|
+
RN_GPUI_COPY_SVG_PROP(minY);
|
|
81
|
+
RN_GPUI_COPY_SVG_PROP(align);
|
|
82
|
+
RN_GPUI_COPY_SVG_PROP(meetOrSlice);
|
|
83
|
+
RN_GPUI_COPY_SVG_PROP(opacity);
|
|
84
|
+
RN_GPUI_COPY_SVG_PROP(color);
|
|
85
|
+
RN_GPUI_COPY_SVG_PROP(propList);
|
|
86
|
+
RN_GPUI_COPY_SVG_PROP(fill);
|
|
87
|
+
RN_GPUI_COPY_SVG_PROP(fillOpacity);
|
|
88
|
+
RN_GPUI_COPY_SVG_PROP(fillRule);
|
|
89
|
+
RN_GPUI_COPY_SVG_PROP(stroke);
|
|
90
|
+
RN_GPUI_COPY_SVG_PROP(strokeOpacity);
|
|
91
|
+
RN_GPUI_COPY_SVG_PROP(strokeWidth);
|
|
92
|
+
RN_GPUI_COPY_SVG_PROP(strokeLinecap);
|
|
93
|
+
RN_GPUI_COPY_SVG_PROP(strokeLinejoin);
|
|
94
|
+
RN_GPUI_COPY_SVG_PROP(strokeDasharray);
|
|
95
|
+
RN_GPUI_COPY_SVG_PROP(strokeDashoffset);
|
|
96
|
+
RN_GPUI_COPY_SVG_PROP(strokeMiterlimit);
|
|
97
|
+
RN_GPUI_COPY_SVG_PROP(matrix);
|
|
98
|
+
RN_GPUI_COPY_SVG_PROP(mask);
|
|
99
|
+
RN_GPUI_COPY_SVG_PROP(clipPath);
|
|
100
|
+
RN_GPUI_COPY_SVG_PROP(offset);
|
|
101
|
+
RN_GPUI_COPY_SVG_PROP(stopColor);
|
|
102
|
+
RN_GPUI_COPY_SVG_PROP(stopOpacity);
|
|
103
|
+
if constexpr (requires { props.filter; }) {
|
|
104
|
+
if constexpr (std::is_same_v<
|
|
105
|
+
std::remove_cvref_t<decltype(props.filter)>,
|
|
106
|
+
std::string>)
|
|
107
|
+
result["filter"] = props.filter;
|
|
108
|
+
}
|
|
109
|
+
RN_GPUI_COPY_SVG_PROP(filterUnits);
|
|
110
|
+
RN_GPUI_COPY_SVG_PROP(primitiveUnits);
|
|
111
|
+
RN_GPUI_COPY_SVG_PROP(result);
|
|
112
|
+
RN_GPUI_COPY_SVG_PROP(in1);
|
|
113
|
+
RN_GPUI_COPY_SVG_PROP(in2);
|
|
114
|
+
RN_GPUI_COPY_SVG_PROP(mode);
|
|
115
|
+
RN_GPUI_COPY_SVG_PROP(type);
|
|
116
|
+
RN_GPUI_COPY_SVG_PROP(values);
|
|
117
|
+
RN_GPUI_COPY_SVG_PROP(operator1);
|
|
118
|
+
RN_GPUI_COPY_SVG_PROP(k1);
|
|
119
|
+
RN_GPUI_COPY_SVG_PROP(k2);
|
|
120
|
+
RN_GPUI_COPY_SVG_PROP(k3);
|
|
121
|
+
RN_GPUI_COPY_SVG_PROP(k4);
|
|
122
|
+
RN_GPUI_COPY_SVG_PROP(dx);
|
|
123
|
+
RN_GPUI_COPY_SVG_PROP(dy);
|
|
124
|
+
RN_GPUI_COPY_SVG_PROP(edgeMode);
|
|
125
|
+
RN_GPUI_COPY_SVG_PROP(floodColor);
|
|
126
|
+
RN_GPUI_COPY_SVG_PROP(floodOpacity);
|
|
127
|
+
RN_GPUI_COPY_SVG_PROP(stdDeviationX);
|
|
128
|
+
RN_GPUI_COPY_SVG_PROP(stdDeviationY);
|
|
129
|
+
RN_GPUI_COPY_SVG_PROP(nodes);
|
|
130
|
+
#undef RN_GPUI_COPY_SVG_PROP
|
|
131
|
+
return result;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
folly::dynamic svgPropsJson(const std::string& component, const Props& props) {
|
|
135
|
+
#define RN_GPUI_SVG_PROPS(tag, type) \
|
|
136
|
+
if (component == tag) { \
|
|
137
|
+
if (auto typed = dynamic_cast<const type*>(&props)) \
|
|
138
|
+
return svgPropsJson(*typed); \
|
|
139
|
+
}
|
|
140
|
+
RN_GPUI_SVG_PROPS("RNSVGSvgView", RNSVGSvgViewProps)
|
|
141
|
+
RN_GPUI_SVG_PROPS("RNSVGCircle", RNSVGCircleProps)
|
|
142
|
+
RN_GPUI_SVG_PROPS("RNSVGEllipse", RNSVGEllipseProps)
|
|
143
|
+
RN_GPUI_SVG_PROPS("RNSVGLine", RNSVGLineProps)
|
|
144
|
+
RN_GPUI_SVG_PROPS("RNSVGPath", RNSVGPathProps)
|
|
145
|
+
RN_GPUI_SVG_PROPS("RNSVGRect", RNSVGRectProps)
|
|
146
|
+
RN_GPUI_SVG_PROPS("RNSVGGroup", RNSVGGroupProps)
|
|
147
|
+
RN_GPUI_SVG_PROPS("RNSVGDefs", RNSVGDefsProps)
|
|
148
|
+
RN_GPUI_SVG_PROPS("RNSVGClipPath", RNSVGClipPathProps)
|
|
149
|
+
RN_GPUI_SVG_PROPS("RNSVGLinearGradient", RNSVGLinearGradientProps)
|
|
150
|
+
RN_GPUI_SVG_PROPS("RNSVGRadialGradient", RNSVGRadialGradientProps)
|
|
151
|
+
RN_GPUI_SVG_PROPS("RNSVGText", RNSVGTextProps)
|
|
152
|
+
RN_GPUI_SVG_PROPS("RNSVGTSpan", RNSVGTSpanProps)
|
|
153
|
+
RN_GPUI_SVG_PROPS("RNSVGFilter", RNSVGFilterProps)
|
|
154
|
+
RN_GPUI_SVG_PROPS("RNSVGFeBlend", RNSVGFeBlendProps)
|
|
155
|
+
RN_GPUI_SVG_PROPS("RNSVGFeColorMatrix", RNSVGFeColorMatrixProps)
|
|
156
|
+
RN_GPUI_SVG_PROPS("RNSVGFeComposite", RNSVGFeCompositeProps)
|
|
157
|
+
RN_GPUI_SVG_PROPS("RNSVGFeFlood", RNSVGFeFloodProps)
|
|
158
|
+
RN_GPUI_SVG_PROPS("RNSVGFeGaussianBlur", RNSVGFeGaussianBlurProps)
|
|
159
|
+
RN_GPUI_SVG_PROPS("RNSVGFeMerge", RNSVGFeMergeProps)
|
|
160
|
+
RN_GPUI_SVG_PROPS("RNSVGFeOffset", RNSVGFeOffsetProps)
|
|
161
|
+
#undef RN_GPUI_SVG_PROPS
|
|
162
|
+
return folly::dynamic::object();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
} // namespace
|
|
166
|
+
|
|
167
|
+
void rn_gpui_install_svg_module() {
|
|
168
|
+
for (const char* component : kSvgComponents) {
|
|
169
|
+
registerComponentProps(
|
|
170
|
+
component, [](const std::string& name, const ShadowView& view) {
|
|
171
|
+
if (!view.props)
|
|
172
|
+
return std::string("{}");
|
|
173
|
+
return folly::toJson(svgPropsJson(name, *view.props));
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
} // namespace rngpui
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
//! The rasters, kept by key in a cache bounded by bytes.
|
|
2
|
+
//!
|
|
3
|
+
//! One cache for every node, so identical icons share one image. It holds the
|
|
4
|
+
//! images strongly: an icon that scrolls off the screen is still here when it
|
|
5
|
+
//! comes back. When the total passes the limit the image used longest ago goes
|
|
6
|
+
//! first. The limit is 64 MB; `RN_GPUI_SVG_CACHE_MB` sets another.
|
|
7
|
+
|
|
8
|
+
use crate::key::Key;
|
|
9
|
+
use std::collections::HashMap;
|
|
10
|
+
use std::sync::{Arc, Mutex, OnceLock};
|
|
11
|
+
|
|
12
|
+
pub const DEFAULT_LIMIT_BYTES: usize = 64 * 1024 * 1024;
|
|
13
|
+
|
|
14
|
+
struct Entry {
|
|
15
|
+
image: Arc<gpui::RenderImage>,
|
|
16
|
+
bytes: usize,
|
|
17
|
+
used: u64,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
pub struct Cache {
|
|
21
|
+
limit: usize,
|
|
22
|
+
bytes: usize,
|
|
23
|
+
clock: u64,
|
|
24
|
+
entries: HashMap<Key, Entry>,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
fn size_of(image: &gpui::RenderImage) -> usize {
|
|
28
|
+
let size = image.size(0);
|
|
29
|
+
size.width.0.max(0) as usize * size.height.0.max(0) as usize * 4
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
impl Cache {
|
|
33
|
+
pub fn new(limit: usize) -> Self {
|
|
34
|
+
Self {
|
|
35
|
+
limit,
|
|
36
|
+
bytes: 0,
|
|
37
|
+
clock: 0,
|
|
38
|
+
entries: HashMap::new(),
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
pub fn get(&mut self, key: &Key) -> Option<Arc<gpui::RenderImage>> {
|
|
43
|
+
self.clock += 1;
|
|
44
|
+
let clock = self.clock;
|
|
45
|
+
let entry = self.entries.get_mut(key)?;
|
|
46
|
+
entry.used = clock;
|
|
47
|
+
Some(entry.image.clone())
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
pub fn insert(&mut self, key: Key, image: Arc<gpui::RenderImage>) {
|
|
51
|
+
self.clock += 1;
|
|
52
|
+
let bytes = size_of(&image);
|
|
53
|
+
if let Some(old) = self.entries.insert(
|
|
54
|
+
key,
|
|
55
|
+
Entry {
|
|
56
|
+
image,
|
|
57
|
+
bytes,
|
|
58
|
+
used: self.clock,
|
|
59
|
+
},
|
|
60
|
+
) {
|
|
61
|
+
self.bytes -= old.bytes;
|
|
62
|
+
}
|
|
63
|
+
self.bytes += bytes;
|
|
64
|
+
// The newest image stays even when it alone is over the limit.
|
|
65
|
+
while self.bytes > self.limit && self.entries.len() > 1 {
|
|
66
|
+
let Some(oldest) = self
|
|
67
|
+
.entries
|
|
68
|
+
.iter()
|
|
69
|
+
.min_by_key(|(_, entry)| entry.used)
|
|
70
|
+
.map(|(key, _)| *key)
|
|
71
|
+
else {
|
|
72
|
+
break;
|
|
73
|
+
};
|
|
74
|
+
if let Some(removed) = self.entries.remove(&oldest) {
|
|
75
|
+
self.bytes -= removed.bytes;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
pub fn bytes(&self) -> usize {
|
|
81
|
+
self.bytes
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
pub fn len(&self) -> usize {
|
|
85
|
+
self.entries.len()
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
pub fn global() -> &'static Mutex<Cache> {
|
|
90
|
+
static CACHE: OnceLock<Mutex<Cache>> = OnceLock::new();
|
|
91
|
+
CACHE.get_or_init(|| {
|
|
92
|
+
let limit = std::env::var("RN_GPUI_SVG_CACHE_MB")
|
|
93
|
+
.ok()
|
|
94
|
+
.and_then(|value| value.parse::<usize>().ok())
|
|
95
|
+
.map_or(DEFAULT_LIMIT_BYTES, |megabytes| megabytes * 1024 * 1024);
|
|
96
|
+
Mutex::new(Cache::new(limit))
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
#[cfg(test)]
|
|
101
|
+
mod tests {
|
|
102
|
+
use super::*;
|
|
103
|
+
|
|
104
|
+
fn image(side: u32) -> Arc<gpui::RenderImage> {
|
|
105
|
+
let buffer =
|
|
106
|
+
image::ImageBuffer::from_raw(side, side, vec![0u8; (side * side * 4) as usize])
|
|
107
|
+
.expect("a buffer");
|
|
108
|
+
Arc::new(gpui::RenderImage::new(smallvec::SmallVec::from_const([
|
|
109
|
+
image::Frame::new(buffer),
|
|
110
|
+
])))
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
fn key(n: u64) -> Key {
|
|
114
|
+
crate::key::test_key(n)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
#[test]
|
|
118
|
+
fn it_returns_what_it_was_given_and_counts_bytes() {
|
|
119
|
+
let mut cache = Cache::new(1 << 20);
|
|
120
|
+
let a = image(10);
|
|
121
|
+
cache.insert(key(1), a.clone());
|
|
122
|
+
assert!(Arc::ptr_eq(&cache.get(&key(1)).unwrap(), &a));
|
|
123
|
+
assert!(cache.get(&key(2)).is_none());
|
|
124
|
+
assert_eq!(cache.bytes(), 400);
|
|
125
|
+
// Replacing a key does not count its old image twice.
|
|
126
|
+
cache.insert(key(1), image(20));
|
|
127
|
+
assert_eq!(cache.bytes(), 1600);
|
|
128
|
+
assert_eq!(cache.len(), 1);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
#[test]
|
|
132
|
+
fn it_stays_within_its_limit_and_drops_the_image_used_longest_ago() {
|
|
133
|
+
// Three 10 by 10 images are 1200 bytes; the limit holds two.
|
|
134
|
+
let mut cache = Cache::new(900);
|
|
135
|
+
cache.insert(key(1), image(10));
|
|
136
|
+
cache.insert(key(2), image(10));
|
|
137
|
+
// Using the first makes the second the oldest.
|
|
138
|
+
assert!(cache.get(&key(1)).is_some());
|
|
139
|
+
cache.insert(key(3), image(10));
|
|
140
|
+
assert!(cache.bytes() <= 900);
|
|
141
|
+
assert!(cache.get(&key(1)).is_some());
|
|
142
|
+
assert!(cache.get(&key(2)).is_none());
|
|
143
|
+
assert!(cache.get(&key(3)).is_some());
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
#[test]
|
|
147
|
+
fn one_image_over_the_limit_is_still_kept() {
|
|
148
|
+
let mut cache = Cache::new(100);
|
|
149
|
+
cache.insert(key(1), image(10));
|
|
150
|
+
assert_eq!(cache.len(), 1);
|
|
151
|
+
assert!(cache.get(&key(1)).is_some());
|
|
152
|
+
cache.insert(key(2), image(10));
|
|
153
|
+
assert_eq!(cache.len(), 1);
|
|
154
|
+
assert!(cache.get(&key(2)).is_some());
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
//! The cache key of a drawing: what decides its pixels, hashed in place.
|
|
2
|
+
//!
|
|
3
|
+
//! The document string is the expensive part of a render, so a lookup never
|
|
4
|
+
//! builds it: the key is a structural hash of the node, its props and its
|
|
5
|
+
//! children (the resolved colours are props), the frame's size and the scale.
|
|
6
|
+
//! A position is not part of it, so identical icons at different places share
|
|
7
|
+
//! one raster.
|
|
8
|
+
|
|
9
|
+
use react_native_gpui::extensions::ComponentNode;
|
|
10
|
+
use rn_gpui_sys::ffi::RectF;
|
|
11
|
+
use serde_json::Value;
|
|
12
|
+
use std::collections::hash_map::DefaultHasher;
|
|
13
|
+
use std::hash::Hasher;
|
|
14
|
+
|
|
15
|
+
/// Two independent 64 bit hashes of the same input.
|
|
16
|
+
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
|
17
|
+
pub struct Key(u64, u64);
|
|
18
|
+
|
|
19
|
+
struct Pair(DefaultHasher, DefaultHasher);
|
|
20
|
+
|
|
21
|
+
impl Hasher for Pair {
|
|
22
|
+
fn write(&mut self, bytes: &[u8]) {
|
|
23
|
+
self.0.write(bytes);
|
|
24
|
+
self.1.write(bytes);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
fn finish(&self) -> u64 {
|
|
28
|
+
self.0.finish()
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
fn hash_value(input: &Value, hasher: &mut Pair) {
|
|
33
|
+
match input {
|
|
34
|
+
Value::Null => hasher.write_u8(0),
|
|
35
|
+
Value::Bool(flag) => {
|
|
36
|
+
hasher.write_u8(1);
|
|
37
|
+
hasher.write_u8(u8::from(*flag));
|
|
38
|
+
}
|
|
39
|
+
Value::Number(number) => {
|
|
40
|
+
hasher.write_u8(2);
|
|
41
|
+
hasher.write_u64(number.as_f64().unwrap_or(f64::NAN).to_bits());
|
|
42
|
+
}
|
|
43
|
+
Value::String(text) => {
|
|
44
|
+
hasher.write_u8(3);
|
|
45
|
+
hasher.write_usize(text.len());
|
|
46
|
+
hasher.write(text.as_bytes());
|
|
47
|
+
}
|
|
48
|
+
Value::Array(items) => {
|
|
49
|
+
hasher.write_u8(4);
|
|
50
|
+
hasher.write_usize(items.len());
|
|
51
|
+
for item in items {
|
|
52
|
+
hash_value(item, hasher);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
Value::Object(entries) => {
|
|
56
|
+
hasher.write_u8(5);
|
|
57
|
+
hasher.write_usize(entries.len());
|
|
58
|
+
for (name, entry) in entries {
|
|
59
|
+
hasher.write_usize(name.len());
|
|
60
|
+
hasher.write(name.as_bytes());
|
|
61
|
+
hash_value(entry, hasher);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
fn node(node: &ComponentNode, hasher: &mut Pair) {
|
|
68
|
+
hasher.write_usize(node.component.len());
|
|
69
|
+
hasher.write(node.component.as_bytes());
|
|
70
|
+
hash_value(&node.props, hasher);
|
|
71
|
+
hasher.write_usize(node.children.len());
|
|
72
|
+
for child in &node.children {
|
|
73
|
+
self::node(child, hasher);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
pub fn node_key(root: &ComponentNode, frame: RectF, scale: f32) -> Key {
|
|
78
|
+
let mut hasher = Pair(DefaultHasher::new(), DefaultHasher::new());
|
|
79
|
+
hasher.1.write_u8(0x5a);
|
|
80
|
+
node(root, &mut hasher);
|
|
81
|
+
hasher.write_u32(frame.w.to_bits());
|
|
82
|
+
hasher.write_u32(frame.h.to_bits());
|
|
83
|
+
hasher.write_u32(scale.to_bits());
|
|
84
|
+
Key(hasher.0.finish(), hasher.1.finish())
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
#[cfg(test)]
|
|
88
|
+
mod tests {
|
|
89
|
+
use super::*;
|
|
90
|
+
use serde_json::json;
|
|
91
|
+
use std::sync::Arc;
|
|
92
|
+
|
|
93
|
+
fn shape(props: Value) -> ComponentNode {
|
|
94
|
+
ComponentNode {
|
|
95
|
+
component: "RNSVGPath".into(),
|
|
96
|
+
tag: 1,
|
|
97
|
+
frame: RectF {
|
|
98
|
+
x: 0.0,
|
|
99
|
+
y: 0.0,
|
|
100
|
+
w: 10.0,
|
|
101
|
+
h: 10.0,
|
|
102
|
+
},
|
|
103
|
+
scale: (1.0, 1.0),
|
|
104
|
+
props: Arc::new(props),
|
|
105
|
+
parent: None,
|
|
106
|
+
children: Vec::new(),
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
fn svg(color: u32, d: &str) -> ComponentNode {
|
|
111
|
+
let mut root = shape(json!({"color": color, "vbWidth": 24, "vbHeight": 24}));
|
|
112
|
+
root.component = "RNSVGSvgView".into();
|
|
113
|
+
root.children.push(shape(
|
|
114
|
+
json!({"d": d, "fill": {"type": 0, "payload": color}}),
|
|
115
|
+
));
|
|
116
|
+
root
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const FRAME: RectF = RectF {
|
|
120
|
+
x: 0.0,
|
|
121
|
+
y: 0.0,
|
|
122
|
+
w: 16.0,
|
|
123
|
+
h: 16.0,
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
#[test]
|
|
127
|
+
fn the_same_drawing_has_the_same_key_wherever_it_is() {
|
|
128
|
+
let a = node_key(&svg(0xff112233, "M0 0L1 1"), FRAME, 2.0);
|
|
129
|
+
let mut moved = svg(0xff112233, "M0 0L1 1");
|
|
130
|
+
moved.frame.x = 99.0;
|
|
131
|
+
moved.tag = 7;
|
|
132
|
+
assert_eq!(a, node_key(&moved, RectF { x: 40.0, ..FRAME }, 2.0));
|
|
133
|
+
assert_eq!(a, node_key(&svg(0xff112233, "M0 0L1 1"), FRAME, 2.0));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
#[test]
|
|
137
|
+
fn anything_that_changes_the_pixels_changes_the_key() {
|
|
138
|
+
let base = node_key(&svg(0xff112233, "M0 0L1 1"), FRAME, 2.0);
|
|
139
|
+
// A colour, a path, a child, the size and the scale each make a new key.
|
|
140
|
+
assert_ne!(base, node_key(&svg(0xff112234, "M0 0L1 1"), FRAME, 2.0));
|
|
141
|
+
assert_ne!(base, node_key(&svg(0xff112233, "M0 0L1 2"), FRAME, 2.0));
|
|
142
|
+
let mut more = svg(0xff112233, "M0 0L1 1");
|
|
143
|
+
more.children.push(shape(json!({"d": "M1 1"})));
|
|
144
|
+
assert_ne!(base, node_key(&more, FRAME, 2.0));
|
|
145
|
+
assert_ne!(
|
|
146
|
+
base,
|
|
147
|
+
node_key(
|
|
148
|
+
&svg(0xff112233, "M0 0L1 1"),
|
|
149
|
+
RectF { w: 17.0, ..FRAME },
|
|
150
|
+
2.0
|
|
151
|
+
)
|
|
152
|
+
);
|
|
153
|
+
assert_ne!(base, node_key(&svg(0xff112233, "M0 0L1 1"), FRAME, 1.0));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
#[test]
|
|
157
|
+
fn values_of_different_kinds_do_not_collide() {
|
|
158
|
+
let flag = node_key(&shape(json!({"a": true})), FRAME, 1.0);
|
|
159
|
+
let text = node_key(&shape(json!({"a": "true"})), FRAME, 1.0);
|
|
160
|
+
let number = node_key(&shape(json!({"a": 1})), FRAME, 1.0);
|
|
161
|
+
assert_ne!(flag, text);
|
|
162
|
+
assert_ne!(flag, number);
|
|
163
|
+
assert_ne!(
|
|
164
|
+
node_key(&shape(json!({"ab": "c"})), FRAME, 1.0),
|
|
165
|
+
node_key(&shape(json!({"a": "bc"})), FRAME, 1.0)
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
#[cfg(test)]
|
|
171
|
+
pub fn test_key(n: u64) -> Key {
|
|
172
|
+
Key(n, n)
|
|
173
|
+
}
|