Skip to content

Commit 5cf121c

Browse files
authored
fix(📏): substancial improvements on getting canvas sizes (#3256)
See https://shopify.github.io/react-native-skia/docs/canvas/overview
1 parent 2335967 commit 5cf121c

File tree

13 files changed

+351
-187
lines changed

13 files changed

+351
-187
lines changed

apps/docs/docs/animations/hooks.md

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -108,25 +108,6 @@ export default function App() {
108108
}
109109
```
110110

111-
## Canvas Size
112-
113-
The Canvas element has an `onSize` property that can receive a shared value, which will be updated whenever the canvas size changes.
114-
115-
```tsx twoslash
116-
import {useSharedValue} from "react-native-reanimated";
117-
import {Fill, Canvas} from "@shopify/react-native-skia";
118-
119-
const Demo = () => {
120-
// size will be updated as the canvas size changes
121-
const size = useSharedValue({ width: 0, height: 0 });
122-
return (
123-
<Canvas style={{ flex: 1 }} onSize={size}>
124-
<Fill color="white" />
125-
</Canvas>
126-
);
127-
};
128-
```
129-
130111
## useRectBuffer
131112

132113
Creates an array for rectangle to be animated.

apps/docs/docs/canvas/canvas.md

Lines changed: 69 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,75 @@ Behind the scenes, it is using its own React renderer.
1414
| style? | `ViewStyle` | View style |
1515
| ref? | `Ref<SkiaView>` | Reference to the `SkiaView` object |
1616
| onSize? | `SharedValue<Size>` | Reanimated value to which the canvas size will be assigned (see [canvas size](/docs/animations/hooks#canvas-size)) |
17-
| onLayout? | `NativeEvent<LayoutEvent>` | Invoked on mount and on layout changes (see [onLayout](https://reactnative.dev/docs/view#onlayout)) |
1817

19-
## Getting the Canvas size
18+
## Canvas size
19+
20+
The size of the canvas is available on both the UI and JS thread.
21+
22+
### UI thread
23+
24+
The `onSize` property receives a shared value, which will be updated whenever the canvas size changes.
25+
You can see it in action in the example below.
26+
27+
```tsx twoslash
28+
import {useSharedValue, useDerivedValue} from "react-native-reanimated";
29+
import {Fill, Canvas, Rect} from "@shopify/react-native-skia";
30+
31+
const Demo = () => {
32+
// size will be updated as the canvas size changes
33+
const size = useSharedValue({ width: 0, height: 0 });
34+
const rect = useDerivedValue(() => {
35+
const {width, height} = size.value;
36+
return { x: 0, y: 0, width, height };
37+
});
38+
return (
39+
<Canvas style={{ flex: 1 }} onSize={size}>
40+
<Rect color="cyan" rect={rect} />
41+
</Canvas>
42+
);
43+
};
44+
```
45+
46+
### JS thread
47+
48+
To get the canvas size on the JS thread, you can use `useLayoutEffect` and `measure()`.
49+
Since this is a very common pattern, we offer a `useCanvasSize` hook you can use for convenience.
50+
51+
```tsx twoslash
52+
import {Fill, Canvas, Rect, useCanvasSize} from "@shopify/react-native-skia";
53+
54+
const Demo = () => {
55+
const {ref, size: {width, height}} = useCanvasSize();
56+
return (
57+
<Canvas style={{ flex: 1 }} ref={ref}>
58+
<Rect color="cyan" rect={{ x: 0, y: 0, width, height }} />
59+
</Canvas>
60+
);
61+
};
62+
```
63+
64+
This example is equivalent to the code below:
65+
66+
```tsx twoslash
67+
import {useLayoutEffect, useState} from "react";
68+
import {Fill, Canvas, Rect, useCanvasRef} from "@shopify/react-native-skia";
69+
70+
const Demo = () => {
71+
const ref = useCanvasRef();
72+
const [rect, setRect] = useState({ x: 0, y: 0, width: 0, height: 0 });
73+
useLayoutEffect(() => {
74+
ref.current?.measure((_x, _y, width, height) => {
75+
setRect({ x: 0, y: 0, width, height });
76+
});
77+
}, []);
78+
return (
79+
<Canvas style={{ flex: 1 }} ref={ref}>
80+
<Rect color="cyan" rect={rect} />
81+
</Canvas>
82+
);
83+
};
84+
```
2085

21-
If the size of the Canvas is unknown, there are two ways to access it:
22-
* **On the JS thread**, using the [`onLayout`](https://reactnative.dev/docs/view#onlayout) prop, like you would on any regular React Native View.
23-
* **On the UI thread**, using the [`onSize`](/docs/animations/hooks#canvas-size) prop with [Reanimated](/docs/animations/animations).
2486

2587
## Getting a Canvas Snapshot
2688

@@ -58,8 +120,8 @@ export const Demo = () => {
58120
};
59121
```
60122

61-
## Accessibilty
123+
## Accessibility
62124

63125
The Canvas component supports the same properties as a View component including its [accessibility properties](https://reactnative.dev/docs/accessibility#accessible).
64126
You can make elements inside the canvas accessible as well by overlayings views on top of your canvas.
65-
This is the same recipe used for [applying gestures on specific canvas elements](https://shopify.github.io/react-native-skia/docs/animations/gestures/#element-tracking).
127+
This is the same recipe used for [applying gestures on specific canvas elements](https://shopify.github.io/react-native-skia/docs/animations/gestures/#element-tracking).

apps/example/ios/Podfile.lock

Lines changed: 60 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1327,7 +1327,7 @@ PODS:
13271327
- ReactCommon/turbomodule/bridging
13281328
- ReactCommon/turbomodule/core
13291329
- Yoga
1330-
- react-native-slider (4.5.7):
1330+
- react-native-slider (4.5.6):
13311331
- DoubleConversion
13321332
- glog
13331333
- hermes-engine
@@ -1340,7 +1340,7 @@ PODS:
13401340
- React-featureflags
13411341
- React-graphics
13421342
- React-ImageManager
1343-
- react-native-slider/common (= 4.5.7)
1343+
- react-native-slider/common (= 4.5.6)
13441344
- React-NativeModulesApple
13451345
- React-RCTFabric
13461346
- React-rendererdebug
@@ -1349,7 +1349,7 @@ PODS:
13491349
- ReactCommon/turbomodule/bridging
13501350
- ReactCommon/turbomodule/core
13511351
- Yoga
1352-
- react-native-slider/common (4.5.7):
1352+
- react-native-slider/common (4.5.6):
13531353
- DoubleConversion
13541354
- glog
13551355
- hermes-engine
@@ -2162,77 +2162,77 @@ SPEC CHECKSUMS:
21622162
fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd
21632163
glog: eb93e2f488219332457c3c4eafd2738ddc7e80b8
21642164
hermes-engine: b417d2b2aee3b89b58e63e23a51e02be91dc876d
2165-
RCT-Folly: 36fe2295e44b10d831836cc0d1daec5f8abcf809
2165+
RCT-Folly: e78785aa9ba2ed998ea4151e314036f6c49e6d82
21662166
RCTDeprecation: b2eecf2d60216df56bc5e6be5f063826d3c1ee35
21672167
RCTRequired: 78522de7dc73b81f3ed7890d145fa341f5bb32ea
21682168
RCTTypeSafety: c135dd2bf50402d87fd12884cbad5d5e64850edd
21692169
React: b229c49ed5898dab46d60f61ed5a0bfa2ee2fadb
21702170
React-callinvoker: 2ac508e92c8bd9cf834cc7d7787d94352e4af58f
2171-
React-Core: 13cdd1558d0b3f6d9d5a22e14d89150280e79f02
2172-
React-CoreModules: b07a6744f48305405e67c845ebf481b6551b712a
2173-
React-cxxreact: 1055a86c66ac35b4e80bd5fb766aed5f494dfff4
2171+
React-Core: 325b4f6d9162ae8b9a6ff42fe78e260eb124180d
2172+
React-CoreModules: 558041e5258f70cd1092f82778d07b8b2ff01897
2173+
React-cxxreact: 8fff17cbe76e6a8f9991b59552e1235429f9c74b
21742174
React-debug: 0a5fcdbacc6becba0521e910c1bcfdb20f32a3f6
2175-
React-defaultsnativemodule: 4bb28fc97fee5be63a9ebf8f7a435cfe8ba69459
2176-
React-domnativemodule: b36a11c2597243d7563985028c51ece988d8ae33
2177-
React-Fabric: afc561718f25b2cd800b709d934101afe376a12c
2178-
React-FabricComponents: f4e0a4e18a27bf6d39cbf2a0b42f37a92fa4e37f
2179-
React-FabricImage: 37d8e8b672eda68a19d71143eb65148084efb325
2175+
React-defaultsnativemodule: 618dc50a0fad41b489997c3eb7aba3a74479fd14
2176+
React-domnativemodule: 7ba599afb6c2a7ec3eb6450153e2efe0b8747e9a
2177+
React-Fabric: 252112089d2c63308f4cbfade4010b6606db67d1
2178+
React-FabricComponents: 3c0f75321680d14d124438ab279c64ec2a3d13c4
2179+
React-FabricImage: 728b8061cdec2857ca885fd605ee03ad43ffca98
21802180
React-featureflags: 19682e02ef5861d96b992af16a19109c3dfc1200
2181-
React-featureflagsnativemodule: d7cddf6d907b4e5ab84f9e744b7e88461656e48c
2182-
React-graphics: b0f78580cdaf5800d25437e3d41cc6c3d83b7aea
2183-
React-hermes: 71186f872c932e4574d5feb3ed754dda63a0b3bd
2184-
React-idlecallbacksnativemodule: dd2af19cdd3bc55149d17a2409ed72b694dfbe9c
2185-
React-ImageManager: a77dde8d5aa6a2b6962c702bf3a47695ef0aa32b
2186-
React-jserrorhandler: 9c14e89f12d5904257a79aaf84a70cd2e5ac07ba
2187-
React-jsi: 0775a66820496769ad83e629f0f5cce621a57fc7
2188-
React-jsiexecutor: 2cf5ba481386803f3c88b85c63fa102cba5d769e
2189-
React-jsinspector: 8052d532bb7a98b6e021755674659802fb140cc5
2190-
React-jsinspectortracing: bdd8fd0adcb4813663562e7874c5842449df6d8a
2191-
React-jsitracing: 2bab3bf55de3d04baf205def375fa6643c47c794
2192-
React-logger: 795cd5055782db394f187f9db0477d4b25b44291
2193-
React-Mapbuffer: 0502faf46cab8fb89cfc7bf3e6c6109b6ef9b5de
2194-
React-microtasksnativemodule: 663bc64e3a96c5fc91081923ae7481adc1359a78
2195-
react-native-safe-area-context: 286b3e7b5589795bb85ffc38faf4c0706c48a092
2196-
react-native-skia: 36463ca61a318c139330095cc2314d72088aea4c
2197-
react-native-slider: 27263d134d55db948a4706f1e47d0ec88fb354dd
2198-
React-NativeModulesApple: 16fbd5b040ff6c492dacc361d49e63cba7a6a7a1
2199-
React-perflogger: ab51b7592532a0ea45bf6eed7e6cae14a368b678
2200-
React-performancetimeline: bc2e48198ec814d578ac8401f65d78a574358203
2181+
React-featureflagsnativemodule: 23528c7e7d50782b7ef0804168ba40bbaf1e86ab
2182+
React-graphics: fefe48f71bfe6f48fd037f59e8277b12e91b6be1
2183+
React-hermes: a9a0c8377627b5506ef9a7b6f60a805c306e3f51
2184+
React-idlecallbacksnativemodule: 7e2b6a3b70e042f89cd91dbd73c479bb39a72a7e
2185+
React-ImageManager: e3300996ac2e2914bf821f71e2f2c92ae6e62ae2
2186+
React-jserrorhandler: fa75876c662e5d7e79d6efc763fc9f4c88e26986
2187+
React-jsi: f3f51595cc4c089037b536368f016d4742bf9cf7
2188+
React-jsiexecutor: cca6c232db461e2fd213a11e9364cfa6fdaa20eb
2189+
React-jsinspector: 2bd4c9fddf189d6ec2abf4948461060502582bef
2190+
React-jsinspectortracing: a417d8a0ad481edaa415734b4dac81e3e5ee7dc6
2191+
React-jsitracing: 1ff7172c5b0522cbf6c98d82bdbb160e49b5804e
2192+
React-logger: 018826bfd51b9f18e87f67db1590bc510ad20664
2193+
React-Mapbuffer: 3c11cee7737609275c7b66bd0b1de475f094cedf
2194+
React-microtasksnativemodule: 843f352b32aacbe13a9c750190d34df44c3e6c2c
2195+
react-native-safe-area-context: 0f14bce545abcdfbff79ce2e3c78c109f0be283e
2196+
react-native-skia: d696b43cb1a6560ec3902856cdc8f0838cc25b64
2197+
react-native-slider: bb7eb4732940fab78217e1c096bb647d8b0d1cf3
2198+
React-NativeModulesApple: 88433b6946778bea9c153e27b671de15411bf225
2199+
React-perflogger: 9e8d3c0dc0194eb932162812a168aa5dc662f418
2200+
React-performancetimeline: 5a2d6efef52bdcefac079c7baa30934978acd023
22012201
React-RCTActionSheet: 592674cf61142497e0e820688f5a696e41bf16dd
2202-
React-RCTAnimation: 8fbb8dba757b49c78f4db403133ab6399a4ce952
2203-
React-RCTAppDelegate: 7f88baa8cb4e5d6c38bb4d84339925c70c9ac864
2204-
React-RCTBlob: f89b162d0fe6b570a18e755eb16cbe356d3c6d17
2205-
React-RCTFabric: 8ad6d875abe6e87312cef90e4b15ef7f6bed72e6
2206-
React-RCTFBReactNativeSpec: 8c29630c2f379c729300e4c1e540f3d1b78d1936
2207-
React-RCTImage: ccac9969940f170503857733f9a5f63578e106e1
2208-
React-RCTLinking: d82427bbf18415a3732105383dff119131cadd90
2209-
React-RCTNetwork: 12ad4d0fbde939e00251ca5ca890da2e6825cc3c
2210-
React-RCTSettings: e7865bf9f455abf427da349c855f8644b5c39afa
2211-
React-RCTText: 2cdfd88745059ec3202a0842ea75a956c7d6f27d
2212-
React-RCTVibration: a3a1458e6230dfd64b3768ebc0a4aac430d9d508
2202+
React-RCTAnimation: e6d669872f9b3b4ab9527aab283b7c49283236b7
2203+
React-RCTAppDelegate: de2343fe08be4c945d57e0ecce44afcc7dd8fc03
2204+
React-RCTBlob: 3e2dce94c56218becc4b32b627fc2293149f798d
2205+
React-RCTFabric: cac2c033381d79a5956e08550b0220cb2d78ea93
2206+
React-RCTFBReactNativeSpec: d10ca5e0ccbfeac8c047361fedf8e4ac653887b6
2207+
React-RCTImage: dc04b176c022d12a8f55ae7a7279b1e091066ae0
2208+
React-RCTLinking: 88f5e37fe4f26fbc80791aa2a5f01baf9b9a3fd5
2209+
React-RCTNetwork: f213693565efbd698b8e9c18d700a514b49c0c8e
2210+
React-RCTSettings: a2d32a90c45a3575568cad850abc45924999b8a5
2211+
React-RCTText: 54cdcd1cbf6f6a91dc6317f5d2c2b7fc3f6bf7a0
2212+
React-RCTVibration: 11dae0e7f577b5807bb7d31e2e881eb46f854fd4
22132213
React-rendererconsistency: 64e897e00d2568fd8dfe31e2496f80e85c0aaad1
2214-
React-rendererdebug: a3f6d3ae7d2fa0035885026756281c07ee32479e
2214+
React-rendererdebug: 41ce452460c44bba715d9e41d5493a96de277764
22152215
React-rncore: 58748c2aa445f56b99e5118dad0aedb51c40ce9f
2216-
React-RuntimeApple: f0fda7bacabd32daa099cfda8f07466c30acd149
2217-
React-RuntimeCore: 683ee0b6a76d4b4bf6fbf83a541895b4887cc636
2216+
React-RuntimeApple: 7785ed0d8ae54da65a88736bb63ca97608a6d933
2217+
React-RuntimeCore: 6029ea70bc77f98cfd43ebe69217f14e93ba1f12
22182218
React-runtimeexecutor: a188df372373baf5066e6e229177836488799f80
2219-
React-RuntimeHermes: 907c8e9bec13ea6466b94828c088c24590d4d0b6
2220-
React-runtimescheduler: a2e2a39125dd6426b5d8b773f689d660cd7c5f60
2219+
React-RuntimeHermes: a264609c28b796edfffc8ae4cb8fad1773ab948b
2220+
React-runtimescheduler: 23ec3a1e0fb1ec752d1a9c1fb15258c30bfc7222
22212221
React-timing: bb220a53a795ed57976a4855c521f3de2f298fe5
2222-
React-utils: 300d8bbb6555dcffaca71e7a0663201b5c7edbbc
2223-
ReactAppDependencyProvider: f2e81d80afd71a8058589e19d8a134243fa53f17
2224-
ReactCodegen: 50b6e45bbbef9b39d9798820cdbe87bfc7922e22
2225-
ReactCommon: 3d39389f8e2a2157d5c999f8fba57bd1c8f226f0
2226-
ReactNativeHost: f2ecc49200441384efb6c6e8bffe62ba29ee16ae
2227-
ReactTestApp-DevSupport: 15d2ef4884e8f5fd30ded3dec59b010f76384f37
2222+
React-utils: 3b054aaebe658fc710a8d239d0e4b9fd3e0b78f9
2223+
ReactAppDependencyProvider: a1fb08dfdc7ebc387b2e54cfc9decd283ed821d8
2224+
ReactCodegen: e232f8db3a40721044ec81b9388f95a7afaad36a
2225+
ReactCommon: 0c097b53f03d6bf166edbcd0915da32f3015dd90
2226+
ReactNativeHost: f9584a700dc379cfa223203d0d51e492df84a7a8
2227+
ReactTestApp-DevSupport: 16672810b0675a3bab6be3b3e85f1ce4b93144da
22282228
ReactTestApp-Resources: 1bd9ff10e4c24f2ad87101a32023721ae923bccf
2229-
RNGestureHandler: 66e593addd8952725107cfaa4f5e3378e946b541
2230-
RNReanimated: b292a2aee945230a9c5e01889043ba088b5fb9b8
2231-
RNScreens: 0f01bbed9bd8045a8d58e4b46993c28c7f498f3c
2232-
RNSVG: 8588ee1ca9b2e6fd2c99466e35b3db0e9f81bb40
2229+
RNGestureHandler: dcb1b1db024f3744b03af56d132f4f72c4c27195
2230+
RNReanimated: 3b2312c84f8c747ab1e9d9c3ce879e93a5ba96f3
2231+
RNScreens: 790123c4a28783d80a342ce42e8c7381bed62db1
2232+
RNSVG: 8126581b369adf6a0004b6a6cab1a55e3002d5b0
22332233
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
2234-
Yoga: 9b7fb56e7b08cde60e2153344fa6afbd88e5d99f
2234+
Yoga: afd04ff05ebe0121a00c468a8a3c8080221cb14c
22352235

22362236
PODFILE CHECKSUM: 87506345285a0371afb28b9c3e6daaa999c214f3
22372237

2238-
COCOAPODS: 1.15.2
2238+
COCOAPODS: 1.16.2

apps/example/src/Examples/API/OnLayout.tsx

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,13 @@
1-
import React, { useState } from "react";
2-
import type { LayoutRectangle } from "react-native";
1+
import React from "react";
32
import { Text } from "react-native";
4-
import { Canvas, Fill } from "@shopify/react-native-skia";
3+
import { Canvas, Fill, useCanvasSize } from "@shopify/react-native-skia";
54

65
export const OnLayoutDemo = () => {
7-
const [layout, setLayout] = useState<LayoutRectangle>();
6+
const { ref, size } = useCanvasSize();
87
return (
98
<>
10-
<Text>{`OnLayout: ${layout?.width} / ${layout?.height}`}</Text>
11-
<Canvas
12-
style={{ flex: 1 }}
13-
onLayout={(evt) => setLayout(evt.nativeEvent.layout)}
14-
>
9+
<Text>{`OnLayout: ${size?.width} / ${size?.height}`}</Text>
10+
<Canvas style={{ flex: 1 }} ref={ref}>
1511
<Fill color="red" />
1612
</Canvas>
1713
</>

apps/example/src/Examples/LiquidGlass/components/Scene.tsx

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@ import {
1212
processUniforms,
1313
Skia,
1414
TileMode,
15+
useCanvasSize,
1516
} from "@shopify/react-native-skia";
16-
import React, { useState } from "react";
17+
import React from "react";
1718
import {
1819
ReduceMotion,
1920
useDerivedValue,
@@ -104,7 +105,8 @@ export const Scene = ({
104105
filter: filterCB,
105106
shader: shaderFilter,
106107
}: SceneProps) => {
107-
const [{ width, height }, setSize] = useState({ width: 0, height: 0 });
108+
const { ref, size } = useCanvasSize();
109+
const { width, height } = size;
108110
const props = useButtonGroup({ width, height }, r);
109111
const { progress, c1, box, bounds } = props;
110112
const uniforms = useDerivedValue(() => {
@@ -165,15 +167,7 @@ export const Scene = ({
165167
return (
166168
<View style={{ flex: 1 }}>
167169
<GestureDetector gesture={gesture}>
168-
<Canvas
169-
style={{ flex: 1 }}
170-
onLayout={({ nativeEvent: { layout } }) => {
171-
setSize({
172-
width: layout.width,
173-
height: layout.height,
174-
});
175-
}}
176-
>
170+
<Canvas style={{ flex: 1 }} ref={ref}>
177171
<Pattern />
178172
<BackdropFilter filter={<ImageFilter filter={filter} />} />
179173
<ButtonGroup {...props} />

packages/skia/cpp/rnskia/RNSkJsiViewApi.h

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,10 +232,45 @@ class RNSkJsiViewApi : public RNJsi::JsiHostObject,
232232
});
233233
}
234234

235+
JSI_HOST_FUNCTION(size) {
236+
if (count != 1) {
237+
_platformContext->raiseError(
238+
std::string("size: Expected 1 argument, got " +
239+
std::to_string(count) + "."));
240+
return jsi::Value::undefined();
241+
}
242+
243+
if (!arguments[0].isNumber()) {
244+
_platformContext->raiseError(
245+
"size: First argument must be a number");
246+
return jsi::Value::undefined();
247+
}
248+
249+
// find Skia View
250+
int nativeId = arguments[0].asNumber();
251+
std::shared_ptr<RNSkView> view = ViewRegistry::getInstance().withViewInfo(
252+
nativeId,
253+
[](std::shared_ptr<RNSkViewInfo> info) { return info->view; });
254+
if (view != nullptr) {
255+
auto pixelDensity = _platformContext->getPixelDensity();
256+
auto sizeObj = jsi::Object(runtime);
257+
sizeObj.setProperty(runtime, "width", view->getScaledWidth() / pixelDensity);
258+
sizeObj.setProperty(runtime, "height", view->getScaledHeight() / pixelDensity);
259+
return sizeObj;
260+
}
261+
262+
// Return default size if view not found
263+
auto sizeObj = jsi::Object(runtime);
264+
sizeObj.setProperty(runtime, "width", 0);
265+
sizeObj.setProperty(runtime, "height", 0);
266+
return sizeObj;
267+
}
268+
235269
JSI_EXPORT_FUNCTIONS(JSI_EXPORT_FUNC(RNSkJsiViewApi, setJsiProperty),
236270
JSI_EXPORT_FUNC(RNSkJsiViewApi, requestRedraw),
237271
JSI_EXPORT_FUNC(RNSkJsiViewApi, makeImageSnapshotAsync),
238-
JSI_EXPORT_FUNC(RNSkJsiViewApi, makeImageSnapshot))
272+
JSI_EXPORT_FUNC(RNSkJsiViewApi, makeImageSnapshot),
273+
JSI_EXPORT_FUNC(RNSkJsiViewApi, size))
239274

240275
/**
241276
* Constructor

0 commit comments

Comments
 (0)