@react-native-gpui/svg 15.15.0-alpha.0 → 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 CHANGED
@@ -2,6 +2,19 @@
2
2
 
3
3
  All notable changes to `@react-native-gpui/svg` are listed here. Each version's section becomes the notes of its GitHub release.
4
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
+
5
18
  ## 15.15.0-alpha.0 - 2026-11-01
6
19
 
7
20
  - First release, for `react-native-svg` 15.15.x.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @react-native-gpui/svg
2
2
 
3
- Desktop support for [`react-native-svg`](https://github.com/software-mansion/react-native-svg): SVG elements drawn by the desktop renderer.
3
+ Adapter for [`react-native-svg`](https://github.com/software-mansion/react-native-svg) (MIT): draws SVG elements with the desktop renderer.
4
4
 
5
5
  Part of [react-native-gpui](https://github.com/streetwriters/react-native-gpui), React Native for macOS, Windows and Linux.
6
6
 
@@ -14,6 +14,16 @@ npm install react-native-svg@15.15.x @react-native-gpui/svg
14
14
 
15
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
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
+
17
27
  ## Documentation
18
28
 
19
29
  [Supported libraries](https://streetwriters.github.io/react-native-gpui/docs/supported-libraries)
@@ -8,3 +8,6 @@ react-native-gpui = { version = "0.1" }
8
8
  rn-gpui-sys = { version = "0.1" }
9
9
  gpui = { version = "0.2" }
10
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"
@@ -5,6 +5,7 @@
5
5
 
6
6
  #include <folly/json.h>
7
7
  #include <string>
8
+ #include <type_traits>
8
9
  #include <vector>
9
10
 
10
11
  namespace rngpui {
@@ -12,26 +13,27 @@ using namespace facebook::react;
12
13
 
13
14
  namespace {
14
15
  constexpr const char* kSvgComponents[] = {
15
- "RNSVGSvgView",
16
- "RNSVGCircle",
17
- "RNSVGEllipse",
18
- "RNSVGLine",
19
- "RNSVGPath",
20
- "RNSVGRect",
21
- "RNSVGGroup",
22
- "RNSVGDefs",
23
- "RNSVGClipPath",
24
- "RNSVGLinearGradient",
25
- "RNSVGRadialGradient",
26
- "RNSVGText",
27
- "RNSVGTSpan",
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",
28
23
  };
29
24
 
30
25
  template <typename T>
26
+ requires(!std::is_enum_v<T>)
31
27
  folly::dynamic svgJsonValue(const T& value) {
32
28
  return value;
33
29
  }
34
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
+
35
37
  folly::dynamic svgJsonValue(const Float& value) {
36
38
  return value;
37
39
  }
@@ -98,6 +100,33 @@ folly::dynamic svgPropsJson(const T& props) {
98
100
  RN_GPUI_COPY_SVG_PROP(offset);
99
101
  RN_GPUI_COPY_SVG_PROP(stopColor);
100
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);
101
130
  #undef RN_GPUI_COPY_SVG_PROP
102
131
  return result;
103
132
  }
@@ -121,6 +150,14 @@ folly::dynamic svgPropsJson(const std::string& component, const Props& props) {
121
150
  RN_GPUI_SVG_PROPS("RNSVGRadialGradient", RNSVGRadialGradientProps)
122
151
  RN_GPUI_SVG_PROPS("RNSVGText", RNSVGTextProps)
123
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)
124
161
  #undef RN_GPUI_SVG_PROPS
125
162
  return folly::dynamic::object();
126
163
  }
@@ -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
+ }