@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 +13 -0
- package/README.md +11 -1
- package/desktop/Cargo.toml +3 -0
- package/desktop/cpp/GpuiSvgModule.cpp +50 -13
- package/desktop/src/cache.rs +156 -0
- package/desktop/src/key.rs +173 -0
- package/desktop/src/lib.rs +210 -66
- package/desktop/src/raster.rs +477 -0
- package/package.json +7 -5
package/desktop/src/lib.rs
CHANGED
|
@@ -3,9 +3,11 @@ use react_native_gpui::extensions::{
|
|
|
3
3
|
register_component_renderer, ComponentNode, ComponentRenderer,
|
|
4
4
|
};
|
|
5
5
|
use rn_gpui_sys::ffi::RectF;
|
|
6
|
-
use std::
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
use std::sync::Arc;
|
|
7
|
+
|
|
8
|
+
mod cache;
|
|
9
|
+
mod key;
|
|
10
|
+
mod raster;
|
|
9
11
|
|
|
10
12
|
const COMPONENTS: &[&str] = &[
|
|
11
13
|
"RNSVGSvgView",
|
|
@@ -27,11 +29,24 @@ const COMPONENTS: &[&str] = &[
|
|
|
27
29
|
"RNSVGMarker",
|
|
28
30
|
"RNSVGTextPath",
|
|
29
31
|
"RNSVGUse",
|
|
32
|
+
"RNSVGFilter",
|
|
33
|
+
"RNSVGFeBlend",
|
|
34
|
+
"RNSVGFeColorMatrix",
|
|
35
|
+
"RNSVGFeComposite",
|
|
36
|
+
"RNSVGFeFlood",
|
|
37
|
+
"RNSVGFeGaussianBlur",
|
|
38
|
+
"RNSVGFeMerge",
|
|
39
|
+
"RNSVGFeOffset",
|
|
30
40
|
];
|
|
31
41
|
|
|
32
42
|
struct SvgRenderer;
|
|
33
43
|
|
|
34
44
|
impl ComponentRenderer for SvgRenderer {
|
|
45
|
+
// The SVG document is drawn from the shapes under its root.
|
|
46
|
+
fn reads_children(&self) -> bool {
|
|
47
|
+
true
|
|
48
|
+
}
|
|
49
|
+
|
|
35
50
|
fn render(
|
|
36
51
|
&self,
|
|
37
52
|
node: &ComponentNode,
|
|
@@ -52,14 +67,37 @@ impl ComponentRenderer for SvgRenderer {
|
|
|
52
67
|
let (scale_x, scale_y) = node.scale;
|
|
53
68
|
let width = frame.w * scale_x;
|
|
54
69
|
let height = frame.h * scale_y;
|
|
55
|
-
let document = svg_document(node, frame, cx);
|
|
56
70
|
let scale = window.scale_factor();
|
|
57
|
-
let image =
|
|
71
|
+
let image = raster::image_for(node, frame, scale, window, cx);
|
|
72
|
+
// Chrome paints an inline SVG at a whole CSS pixel: a box laid out at
|
|
73
|
+
// 2.5 is drawn at 3. The image is placed the same way, so a 15 point
|
|
74
|
+
// icon centred in a 20 point button lands where the web app's does.
|
|
58
75
|
Some(
|
|
59
|
-
gpui::
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
76
|
+
gpui::canvas(
|
|
77
|
+
|bounds, _, _| bounds,
|
|
78
|
+
move |_, bounds, window, _| {
|
|
79
|
+
let snapped = gpui::Bounds::new(
|
|
80
|
+
gpui::point(
|
|
81
|
+
px((bounds.origin.x.as_f32() + 0.5).floor()),
|
|
82
|
+
px((bounds.origin.y.as_f32() + 0.5).floor()),
|
|
83
|
+
),
|
|
84
|
+
bounds.size,
|
|
85
|
+
);
|
|
86
|
+
if let Some(image) = &image {
|
|
87
|
+
let _ = window.paint_image(
|
|
88
|
+
snapped,
|
|
89
|
+
snapped,
|
|
90
|
+
gpui::Corners::default(),
|
|
91
|
+
image.clone(),
|
|
92
|
+
0,
|
|
93
|
+
false,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
)
|
|
98
|
+
.w(px(width))
|
|
99
|
+
.h(px(height))
|
|
100
|
+
.into_any_element(),
|
|
63
101
|
)
|
|
64
102
|
}
|
|
65
103
|
}
|
|
@@ -70,13 +108,37 @@ pub fn rn_gpui_register() {
|
|
|
70
108
|
}
|
|
71
109
|
}
|
|
72
110
|
|
|
73
|
-
|
|
111
|
+
/// Where one coverage sample sits, so a shape can be drawn once per sample.
|
|
112
|
+
#[derive(Clone, Copy)]
|
|
113
|
+
pub(crate) struct Sample {
|
|
114
|
+
/// The sample's offset from the pixel centre, in device pixels.
|
|
115
|
+
pub(crate) dx: f32,
|
|
116
|
+
pub(crate) dy: f32,
|
|
117
|
+
/// Device pixels per CSS pixel.
|
|
118
|
+
pub(crate) scale: f32,
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/// Chrome rasterises with four coverage samples per pixel at these offsets (in
|
|
122
|
+
/// pixels from the centre): the standard Direct3D 4x pattern. A pixel is
|
|
123
|
+
/// therefore never covered by anything but 0, 1/4, 1/2, 3/4 or all, and the edge
|
|
124
|
+
/// of an icon takes the same five steps here.
|
|
125
|
+
pub(crate) const SAMPLE_OFFSETS: [(f32, f32); 4] = [
|
|
126
|
+
(-2.0 / 16.0, -6.0 / 16.0),
|
|
127
|
+
(6.0 / 16.0, -2.0 / 16.0),
|
|
128
|
+
(-6.0 / 16.0, 2.0 / 16.0),
|
|
129
|
+
(2.0 / 16.0, 6.0 / 16.0),
|
|
130
|
+
];
|
|
131
|
+
|
|
132
|
+
pub(crate) fn svg_document(node: &ComponentNode, frame: RectF, sample: Option<Sample>) -> String {
|
|
74
133
|
let props = &node.props;
|
|
75
134
|
let mut attributes = format!(
|
|
76
135
|
"xmlns=\"http://www.w3.org/2000/svg\" width=\"{}\" height=\"{}\"",
|
|
77
136
|
svg_number(frame.w),
|
|
78
137
|
svg_number(frame.h)
|
|
79
138
|
);
|
|
139
|
+
if sample.is_some() {
|
|
140
|
+
attributes.push_str(" shape-rendering=\"crispEdges\"");
|
|
141
|
+
}
|
|
80
142
|
for (name, attribute) in [
|
|
81
143
|
("color", "color"),
|
|
82
144
|
("fill", "fill"),
|
|
@@ -112,11 +174,11 @@ fn svg_document(node: &ComponentNode, frame: RectF, cx: &App) -> String {
|
|
|
112
174
|
.unwrap_or("miter");
|
|
113
175
|
attributes.push_str(&format!(" stroke-linejoin=\"{join}\""));
|
|
114
176
|
}
|
|
115
|
-
|
|
177
|
+
let view_box = match (
|
|
116
178
|
props.get("vbWidth").and_then(serde_json::Value::as_f64),
|
|
117
179
|
props.get("vbHeight").and_then(serde_json::Value::as_f64),
|
|
118
180
|
) {
|
|
119
|
-
if width > 0.0 && height > 0.0 {
|
|
181
|
+
(Some(width), Some(height)) if width > 0.0 && height > 0.0 => {
|
|
120
182
|
let min_x = props
|
|
121
183
|
.get("minX")
|
|
122
184
|
.and_then(serde_json::Value::as_f64)
|
|
@@ -125,6 +187,36 @@ fn svg_document(node: &ComponentNode, frame: RectF, cx: &App) -> String {
|
|
|
125
187
|
.get("minY")
|
|
126
188
|
.and_then(serde_json::Value::as_f64)
|
|
127
189
|
.unwrap_or(0.0);
|
|
190
|
+
let align = props.get("align").and_then(serde_json::Value::as_str);
|
|
191
|
+
let mode = match props
|
|
192
|
+
.get("meetOrSlice")
|
|
193
|
+
.and_then(serde_json::Value::as_i64)
|
|
194
|
+
.unwrap_or(0)
|
|
195
|
+
{
|
|
196
|
+
1 => "slice",
|
|
197
|
+
2 => "none",
|
|
198
|
+
_ => "meet",
|
|
199
|
+
};
|
|
200
|
+
Some((min_x, min_y, width, height, align, mode))
|
|
201
|
+
}
|
|
202
|
+
_ => None,
|
|
203
|
+
};
|
|
204
|
+
match (view_box, sample) {
|
|
205
|
+
(Some((min_x, min_y, width, height, align, mode)), sample) => {
|
|
206
|
+
// The sample's shift in user units: how many of them one device
|
|
207
|
+
// pixel covers depends on how the view box is fitted to the frame.
|
|
208
|
+
let (mut min_x, mut min_y) = (min_x, min_y);
|
|
209
|
+
if let Some(sample) = sample {
|
|
210
|
+
let fit_x = f64::from(frame.w * sample.scale) / width;
|
|
211
|
+
let fit_y = f64::from(frame.h * sample.scale) / height;
|
|
212
|
+
let (unit_x, unit_y) = match (align, mode) {
|
|
213
|
+
(Some("none"), _) | (Some(_), "none") => (fit_x, fit_y),
|
|
214
|
+
(_, "slice") => (fit_x.max(fit_y), fit_x.max(fit_y)),
|
|
215
|
+
_ => (fit_x.min(fit_y), fit_x.min(fit_y)),
|
|
216
|
+
};
|
|
217
|
+
min_x += f64::from(sample.dx) / unit_x;
|
|
218
|
+
min_y += f64::from(sample.dy) / unit_y;
|
|
219
|
+
}
|
|
128
220
|
attributes.push_str(&format!(
|
|
129
221
|
" viewBox=\"{} {} {} {}\"",
|
|
130
222
|
svg_number(min_x as f32),
|
|
@@ -132,16 +224,7 @@ fn svg_document(node: &ComponentNode, frame: RectF, cx: &App) -> String {
|
|
|
132
224
|
svg_number(width as f32),
|
|
133
225
|
svg_number(height as f32)
|
|
134
226
|
));
|
|
135
|
-
if let Some(align) =
|
|
136
|
-
let mode = match props
|
|
137
|
-
.get("meetOrSlice")
|
|
138
|
-
.and_then(serde_json::Value::as_i64)
|
|
139
|
-
.unwrap_or(0)
|
|
140
|
-
{
|
|
141
|
-
1 => "slice",
|
|
142
|
-
2 => "none",
|
|
143
|
-
_ => "meet",
|
|
144
|
-
};
|
|
227
|
+
if let Some(align) = align {
|
|
145
228
|
attributes.push_str(&format!(
|
|
146
229
|
" preserveAspectRatio=\"{} {}\"",
|
|
147
230
|
svg_escape(align),
|
|
@@ -149,16 +232,28 @@ fn svg_document(node: &ComponentNode, frame: RectF, cx: &App) -> String {
|
|
|
149
232
|
));
|
|
150
233
|
}
|
|
151
234
|
}
|
|
235
|
+
(None, Some(sample)) => {
|
|
236
|
+
// Without a view box the user unit is the CSS pixel; one is added
|
|
237
|
+
// only to carry the shift.
|
|
238
|
+
attributes.push_str(&format!(
|
|
239
|
+
" viewBox=\"{} {} {} {}\"",
|
|
240
|
+
svg_number(sample.dx / sample.scale),
|
|
241
|
+
svg_number(sample.dy / sample.scale),
|
|
242
|
+
svg_number(frame.w),
|
|
243
|
+
svg_number(frame.h)
|
|
244
|
+
));
|
|
245
|
+
}
|
|
246
|
+
(None, None) => {}
|
|
152
247
|
}
|
|
153
248
|
let mut document = format!("<svg {attributes}>");
|
|
154
249
|
for child in &node.children {
|
|
155
|
-
svg_node(child,
|
|
250
|
+
svg_node(child, &mut document);
|
|
156
251
|
}
|
|
157
252
|
document.push_str("</svg>");
|
|
158
253
|
document
|
|
159
254
|
}
|
|
160
255
|
|
|
161
|
-
fn svg_node(node: &ComponentNode,
|
|
256
|
+
fn svg_node(node: &ComponentNode, output: &mut String) {
|
|
162
257
|
let props = &node.props;
|
|
163
258
|
let element = match node.component.as_str() {
|
|
164
259
|
"RNSVGCircle" => "circle",
|
|
@@ -179,11 +274,19 @@ fn svg_node(node: &ComponentNode, cx: &App, output: &mut String) {
|
|
|
179
274
|
"RNSVGTSpan" => "tspan",
|
|
180
275
|
"RNSVGTextPath" => "textPath",
|
|
181
276
|
"RNSVGUse" => "use",
|
|
277
|
+
"RNSVGFilter" => "filter",
|
|
278
|
+
"RNSVGFeBlend" => "feBlend",
|
|
279
|
+
"RNSVGFeColorMatrix" => "feColorMatrix",
|
|
280
|
+
"RNSVGFeComposite" => "feComposite",
|
|
281
|
+
"RNSVGFeFlood" => "feFlood",
|
|
282
|
+
"RNSVGFeGaussianBlur" => "feGaussianBlur",
|
|
283
|
+
"RNSVGFeMerge" => "feMerge",
|
|
284
|
+
"RNSVGFeOffset" => "feOffset",
|
|
182
285
|
_ => "",
|
|
183
286
|
};
|
|
184
287
|
if element.is_empty() {
|
|
185
288
|
for child in &node.children {
|
|
186
|
-
svg_node(child,
|
|
289
|
+
svg_node(child, output);
|
|
187
290
|
}
|
|
188
291
|
return;
|
|
189
292
|
}
|
|
@@ -287,26 +390,101 @@ fn svg_node(node: &ComponentNode, cx: &App, output: &mut String) {
|
|
|
287
390
|
attributes.push_str(&format!(" stroke-linejoin=\"{join}\""));
|
|
288
391
|
}
|
|
289
392
|
if let Some(matrix) = props.get("matrix").and_then(serde_json::Value::as_array) {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
393
|
+
let component = |index: usize| matrix.get(index).and_then(serde_json::Value::as_f64);
|
|
394
|
+
let values = match matrix.len() {
|
|
395
|
+
6 => [0, 1, 2, 3, 4, 5],
|
|
396
|
+
16.. => [0, 1, 4, 5, 12, 13],
|
|
397
|
+
_ => [usize::MAX; 6],
|
|
398
|
+
}
|
|
399
|
+
.map(component);
|
|
400
|
+
if let [Some(a), Some(b), Some(c), Some(d), Some(e), Some(f)] = values {
|
|
401
|
+
attributes.push_str(&format!(" transform=\"matrix({a} {b} {c} {d} {e} {f})\""));
|
|
296
402
|
}
|
|
297
403
|
}
|
|
404
|
+
if let Some(id) = props
|
|
405
|
+
.get("name")
|
|
406
|
+
.and_then(serde_json::Value::as_str)
|
|
407
|
+
.filter(|id| !id.is_empty())
|
|
408
|
+
{
|
|
409
|
+
attributes.push_str(&format!(" id=\"{}\"", svg_escape(id)));
|
|
410
|
+
}
|
|
411
|
+
if let Some(filter) = props
|
|
412
|
+
.get("filter")
|
|
413
|
+
.and_then(serde_json::Value::as_str)
|
|
414
|
+
.filter(|filter| !filter.is_empty())
|
|
415
|
+
{
|
|
416
|
+
attributes.push_str(&format!(" filter=\"url(#{})\"", svg_escape(filter)));
|
|
417
|
+
}
|
|
418
|
+
if element == "filter" {
|
|
419
|
+
attributes.push_str(" color-interpolation-filters=\"sRGB\"");
|
|
420
|
+
}
|
|
421
|
+
if element.starts_with("fe") || element == "filter" {
|
|
422
|
+
attributes.push_str(&filter_primitive_attributes(props));
|
|
423
|
+
}
|
|
298
424
|
output.push('<');
|
|
299
425
|
output.push_str(element);
|
|
300
426
|
output.push_str(&attributes);
|
|
301
427
|
output.push('>');
|
|
428
|
+
if element == "feMerge" {
|
|
429
|
+
for id in props
|
|
430
|
+
.get("nodes")
|
|
431
|
+
.and_then(serde_json::Value::as_array)
|
|
432
|
+
.into_iter()
|
|
433
|
+
.flatten()
|
|
434
|
+
.filter_map(serde_json::Value::as_str)
|
|
435
|
+
{
|
|
436
|
+
output.push_str(&format!("<feMergeNode in=\"{}\"/>", svg_escape(id)));
|
|
437
|
+
}
|
|
438
|
+
}
|
|
302
439
|
for child in &node.children {
|
|
303
|
-
svg_node(child,
|
|
440
|
+
svg_node(child, output);
|
|
304
441
|
}
|
|
305
442
|
output.push_str("</");
|
|
306
443
|
output.push_str(element);
|
|
307
444
|
output.push('>');
|
|
308
445
|
}
|
|
309
446
|
|
|
447
|
+
fn filter_primitive_attributes(props: &serde_json::Value) -> String {
|
|
448
|
+
let mut attributes = String::new();
|
|
449
|
+
let text = |name: &str| match props.get(name) {
|
|
450
|
+
Some(serde_json::Value::String(value)) if !value.is_empty() => Some(value.clone()),
|
|
451
|
+
Some(value) if value.is_array() || value.is_number() => svg_value(value),
|
|
452
|
+
_ => None,
|
|
453
|
+
};
|
|
454
|
+
for (name, attribute) in [
|
|
455
|
+
("in1", "in"),
|
|
456
|
+
("in2", "in2"),
|
|
457
|
+
("result", "result"),
|
|
458
|
+
("mode", "mode"),
|
|
459
|
+
("type", "type"),
|
|
460
|
+
("values", "values"),
|
|
461
|
+
("operator1", "operator"),
|
|
462
|
+
("k1", "k1"),
|
|
463
|
+
("k2", "k2"),
|
|
464
|
+
("k3", "k3"),
|
|
465
|
+
("k4", "k4"),
|
|
466
|
+
("dx", "dx"),
|
|
467
|
+
("dy", "dy"),
|
|
468
|
+
("edgeMode", "edgeMode"),
|
|
469
|
+
("floodOpacity", "flood-opacity"),
|
|
470
|
+
("filterUnits", "filterUnits"),
|
|
471
|
+
("primitiveUnits", "primitiveUnits"),
|
|
472
|
+
] {
|
|
473
|
+
if let Some(value) = text(name) {
|
|
474
|
+
attributes.push_str(&format!(" {attribute}=\"{}\"", svg_escape(&value)));
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
if let Some(color) = props.get("floodColor").and_then(svg_value) {
|
|
478
|
+
attributes.push_str(&format!(" flood-color=\"{}\"", svg_escape(&color)));
|
|
479
|
+
}
|
|
480
|
+
let deviation = |name: &str| props.get(name).and_then(serde_json::Value::as_f64);
|
|
481
|
+
if let Some(x) = deviation("stdDeviationX") {
|
|
482
|
+
let y = deviation("stdDeviationY").unwrap_or(x);
|
|
483
|
+
attributes.push_str(&format!(" stdDeviation=\"{x} {y}\""));
|
|
484
|
+
}
|
|
485
|
+
attributes
|
|
486
|
+
}
|
|
487
|
+
|
|
310
488
|
fn svg_property_is_set(props: &serde_json::Value, name: &str) -> bool {
|
|
311
489
|
if !matches!(
|
|
312
490
|
name,
|
|
@@ -398,40 +576,6 @@ fn svg_number(value: f32) -> String {
|
|
|
398
576
|
}
|
|
399
577
|
}
|
|
400
578
|
|
|
401
|
-
fn cached_svg_image(
|
|
402
|
-
document: String,
|
|
403
|
-
width: f32,
|
|
404
|
-
height: f32,
|
|
405
|
-
scale: f32,
|
|
406
|
-
) -> std::sync::Arc<gpui::Image> {
|
|
407
|
-
type SvgCache = HashMap<(u64, u32, u32, u32), Weak<gpui::Image>>;
|
|
408
|
-
static CACHE: OnceLock<Mutex<SvgCache>> = OnceLock::new();
|
|
409
|
-
let mut hasher = std::collections::hash_map::DefaultHasher::new();
|
|
410
|
-
document.hash(&mut hasher);
|
|
411
|
-
let key = (
|
|
412
|
-
hasher.finish(),
|
|
413
|
-
width.to_bits(),
|
|
414
|
-
height.to_bits(),
|
|
415
|
-
scale.to_bits(),
|
|
416
|
-
);
|
|
417
|
-
let cache = CACHE.get_or_init(|| Mutex::new(HashMap::new()));
|
|
418
|
-
let mut cache = cache
|
|
419
|
-
.lock()
|
|
420
|
-
.unwrap_or_else(|poisoned| poisoned.into_inner());
|
|
421
|
-
if let Some(image) = cache.get(&key).and_then(Weak::upgrade) {
|
|
422
|
-
return image;
|
|
423
|
-
}
|
|
424
|
-
let image = std::sync::Arc::new(gpui::Image::from_bytes(
|
|
425
|
-
gpui::ImageFormat::Svg,
|
|
426
|
-
document.into_bytes(),
|
|
427
|
-
));
|
|
428
|
-
cache.insert(key, std::sync::Arc::downgrade(&image));
|
|
429
|
-
if cache.len() > 512 {
|
|
430
|
-
cache.retain(|_, image| image.strong_count() > 0);
|
|
431
|
-
}
|
|
432
|
-
image
|
|
433
|
-
}
|
|
434
|
-
|
|
435
579
|
#[cfg(test)]
|
|
436
580
|
mod svg_paint_tests {
|
|
437
581
|
use super::{svg_property_is_set, svg_value};
|