@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.
@@ -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::collections::HashMap;
7
- use std::hash::{Hash, Hasher};
8
- use std::sync::{Arc, Mutex, OnceLock, Weak};
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 = cached_svg_image(document, frame.w, frame.h, scale);
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::img(image)
60
- .w(px(width))
61
- .h(px(height))
62
- .into_any_element(),
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
- fn svg_document(node: &ComponentNode, frame: RectF, cx: &App) -> String {
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
- if let (Some(width), Some(height)) = (
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) = props.get("align").and_then(serde_json::Value::as_str) {
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, cx, &mut document);
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, cx: &App, output: &mut String) {
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, cx, output);
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
- if matrix.len() >= 16 {
291
- let values = [0, 1, 4, 5, 12, 13].map(|index| matrix[index].as_f64().unwrap_or(0.0));
292
- attributes.push_str(&format!(
293
- " transform=\"matrix({} {} {} {} {} {})\"",
294
- values[0], values[1], values[2], values[3], values[4], values[5]
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, cx, output);
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};