basalt-appkit 0.0.0-rc5
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/LICENSE +21 -0
- package/cli/runMacos.js +33 -0
- package/cli/shared.js +75 -0
- package/native/CMakeLists.txt +298 -0
- package/native/appkit/AppKitAnimationChoreographer.h +54 -0
- package/native/appkit/AppKitAnimationChoreographer.mm +125 -0
- package/native/appkit/AppKitDropTarget.h +34 -0
- package/native/appkit/AppKitDropTarget.mm +292 -0
- package/native/appkit/AppKitFocus.h +120 -0
- package/native/appkit/AppKitFocus.mm +293 -0
- package/native/appkit/AppKitImageLoader.h +80 -0
- package/native/appkit/AppKitImageLoader.mm +156 -0
- package/native/appkit/AppKitKeyEvents.h +26 -0
- package/native/appkit/AppKitKeyEvents.mm +97 -0
- package/native/appkit/AppKitMenuBar.mm +424 -0
- package/native/appkit/AppKitMountingManager.h +135 -0
- package/native/appkit/AppKitMountingManager.mm +946 -0
- package/native/appkit/AppKitRunLoopObserver.h +34 -0
- package/native/appkit/AppKitRunLoopObserver.mm +42 -0
- package/native/appkit/AppKitScrollView.h +168 -0
- package/native/appkit/AppKitScrollView.mm +550 -0
- package/native/appkit/AppKitSkiaContext.h +33 -0
- package/native/appkit/AppKitSkiaContext.mm +255 -0
- package/native/appkit/AppKitSkiaModule.h +53 -0
- package/native/appkit/AppKitSkiaModule.mm +81 -0
- package/native/appkit/AppKitSkiaPeer.h +37 -0
- package/native/appkit/AppKitSkiaPeer.mm +123 -0
- package/native/appkit/AppKitSkiaView.h +62 -0
- package/native/appkit/AppKitSnapshot.h +21 -0
- package/native/appkit/AppKitSnapshot.mm +91 -0
- package/native/appkit/AppKitTextInput.h +140 -0
- package/native/appkit/AppKitTextInput.mm +588 -0
- package/native/appkit/AppKitTextPeer.h +74 -0
- package/native/appkit/AppKitTextPeer.mm +353 -0
- package/native/appkit/AppKitTitleBar.h +90 -0
- package/native/appkit/AppKitTitleBar.mm +186 -0
- package/native/appkit/AppKitTouchDispatcher.h +138 -0
- package/native/appkit/AppKitTouchDispatcher.mm +514 -0
- package/native/appkit/AppKitWindowControl.mm +222 -0
- package/native/appkit/AppKitWindowModule.h +62 -0
- package/native/appkit/AppKitWindowModule.mm +194 -0
- package/native/appkit/ColorSchemeAppKit.mm +71 -0
- package/native/appkit/ComponentRegistryAppKit.mm +121 -0
- package/native/appkit/CoreTextLayout.h +43 -0
- package/native/appkit/CoreTextLayout.mm +252 -0
- package/native/appkit/CoreTextLayoutManager.mm +138 -0
- package/native/appkit/FontRegistryCoreText.mm +148 -0
- package/native/appkit/PlatformServicesAppKit.mm +540 -0
- package/native/appkit/RnAppKitView.h +417 -0
- package/native/appkit/RnAppKitView.mm +1605 -0
- package/native/appkit/RnTextLayout.h +38 -0
- package/native/appkit/RnTextLayout.mm +168 -0
- package/native/appkit/demo_layout_appkit.mm +93 -0
- package/native/appkit/main_appkit.mm +1860 -0
- package/native/appkit/mount_harness_appkit.mm +197 -0
- package/native/tests/test_appkit_accessibility.mm +170 -0
- package/native/tests/test_appkit_image.mm +237 -0
- package/native/tests/test_appkit_input.mm +852 -0
- package/native/tests/test_appkit_key_events.mm +150 -0
- package/native/tests/test_appkit_mounting.mm +430 -0
- package/native/tests/test_appkit_scrollbar.mm +132 -0
- package/native/tests/test_appkit_services.mm +57 -0
- package/native/tests/test_appkit_skia.mm +212 -0
- package/native/tests/test_appkit_text.mm +180 -0
- package/native/tests/test_appkit_textinput.mm +748 -0
- package/native/tests/test_appkit_titlebar.mm +107 -0
- package/native/tests/test_appkit_view.mm +237 -0
- package/package.json +26 -0
- package/react-native.config.js +22 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
#include "AppKitSkiaContext.h"
|
|
2
|
+
|
|
3
|
+
#include "PlatformServices.h"
|
|
4
|
+
|
|
5
|
+
#include "MetalContext.h"
|
|
6
|
+
#include "RNSkAppleVideo.h"
|
|
7
|
+
|
|
8
|
+
#import <AppKit/AppKit.h>
|
|
9
|
+
#import <Metal/Metal.h>
|
|
10
|
+
|
|
11
|
+
#include "include/core/SkCanvas.h"
|
|
12
|
+
#include "include/core/SkData.h"
|
|
13
|
+
#include "include/core/SkFontMgr.h"
|
|
14
|
+
#include "include/core/SkImage.h"
|
|
15
|
+
#include "include/core/SkStream.h"
|
|
16
|
+
#include "include/core/SkSurface.h"
|
|
17
|
+
#include "include/gpu/ganesh/GrBackendSurface.h"
|
|
18
|
+
#include "include/gpu/ganesh/SkImageGanesh.h"
|
|
19
|
+
#include "include/gpu/ganesh/SkSurfaceGanesh.h"
|
|
20
|
+
#include "include/gpu/ganesh/mtl/GrMtlBackendSurface.h"
|
|
21
|
+
#include "include/ports/SkFontMgr_mac_ct.h"
|
|
22
|
+
|
|
23
|
+
#include <glog/logging.h>
|
|
24
|
+
|
|
25
|
+
#include <cstdint>
|
|
26
|
+
#include <memory>
|
|
27
|
+
#include <stdexcept>
|
|
28
|
+
#include <string>
|
|
29
|
+
#include <thread>
|
|
30
|
+
#include <utility>
|
|
31
|
+
#include <vector>
|
|
32
|
+
|
|
33
|
+
namespace basalt {
|
|
34
|
+
namespace {
|
|
35
|
+
|
|
36
|
+
SkColorType colorTypeFor(MTLPixelFormat format) {
|
|
37
|
+
// The package's own table, which is the one Skia's Metal backend agrees with.
|
|
38
|
+
switch (format) {
|
|
39
|
+
case MTLPixelFormatRGBA8Unorm:
|
|
40
|
+
return kRGBA_8888_SkColorType;
|
|
41
|
+
case MTLPixelFormatBGRA8Unorm:
|
|
42
|
+
return kBGRA_8888_SkColorType;
|
|
43
|
+
case MTLPixelFormatRGB10A2Unorm:
|
|
44
|
+
return kRGBA_1010102_SkColorType;
|
|
45
|
+
case MTLPixelFormatR8Unorm:
|
|
46
|
+
return kGray_8_SkColorType;
|
|
47
|
+
case MTLPixelFormatRGBA16Float:
|
|
48
|
+
return kRGBA_F16_SkColorType;
|
|
49
|
+
case MTLPixelFormatRG8Unorm:
|
|
50
|
+
return kR8G8_unorm_SkColorType;
|
|
51
|
+
case MTLPixelFormatR16Float:
|
|
52
|
+
return kA16_float_SkColorType;
|
|
53
|
+
case MTLPixelFormatRG16Float:
|
|
54
|
+
return kR16G16_float_SkColorType;
|
|
55
|
+
case MTLPixelFormatR16Unorm:
|
|
56
|
+
return kA16_unorm_SkColorType;
|
|
57
|
+
case MTLPixelFormatRG16Unorm:
|
|
58
|
+
return kR16G16_unorm_SkColorType;
|
|
59
|
+
case MTLPixelFormatRGBA16Unorm:
|
|
60
|
+
return kR16G16B16A16_unorm_SkColorType;
|
|
61
|
+
case MTLPixelFormatRGBA8Unorm_sRGB:
|
|
62
|
+
return kSRGBA_8888_SkColorType;
|
|
63
|
+
default:
|
|
64
|
+
return kUnknown_SkColorType;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
class AppKitSkiaPlatformContext : public RNSkia::RNSkPlatformContext {
|
|
69
|
+
public:
|
|
70
|
+
AppKitSkiaPlatformContext(std::shared_ptr<facebook::react::CallInvoker> callInvoker,
|
|
71
|
+
float pixelDensity)
|
|
72
|
+
: RNSkia::RNSkPlatformContext(std::move(callInvoker), pixelDensity) {}
|
|
73
|
+
|
|
74
|
+
// ---------------------------------------------------------------- threading
|
|
75
|
+
|
|
76
|
+
void runOnMainThread(std::function<void()> func) override {
|
|
77
|
+
// basalt's own, so Skia's idea of the main thread and this host's are the
|
|
78
|
+
// same one. Fire and forget, which is all `postToUiThread` offers and all
|
|
79
|
+
// this seam promises.
|
|
80
|
+
postToUiThread(std::move(func));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ------------------------------------------------------------------- errors
|
|
84
|
+
|
|
85
|
+
void raiseError(const std::exception &err) override {
|
|
86
|
+
// Logged, not fatal. The package's Apple version calls `RCTFatal`, which
|
|
87
|
+
// takes the app down with a red box; there is no red box here yet and a
|
|
88
|
+
// shader that fails to compile is not a reason to end a session. When there
|
|
89
|
+
// is one, this is the single line that should reach it.
|
|
90
|
+
LOG(ERROR) << "react-native-skia: " << err.what();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ------------------------------------------------------------------ surfaces
|
|
94
|
+
|
|
95
|
+
sk_sp<SkSurface> makeOffscreenSurface(int width, int height,
|
|
96
|
+
bool useP3ColorSpace = false) override {
|
|
97
|
+
// The one that matters most for a first port: an app rendering frames for
|
|
98
|
+
// export goes through here, not through a view.
|
|
99
|
+
return MetalContext::getInstance().MakeOffscreen(width, height, useP3ColorSpace);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
GrDirectContext *getDirectContext() override {
|
|
103
|
+
return MetalContext::getInstance().getDirectContext();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ------------------------------------------------------------------- images
|
|
107
|
+
|
|
108
|
+
sk_sp<SkImage> makeImageFromNativeBuffer(void *buffer) override {
|
|
109
|
+
return MetalContext::getInstance().MakeImageFromBuffer(buffer);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
sk_sp<SkImage> makeImageFromNativeTexture(const RNSkia::TextureInfo &info, int width,
|
|
113
|
+
int height, bool mipMapped) override {
|
|
114
|
+
id<MTLTexture> texture = (__bridge id<MTLTexture>)(info.mtlTexture);
|
|
115
|
+
const SkColorType colorType = colorTypeFor(texture.pixelFormat);
|
|
116
|
+
if (colorType == kUnknown_SkColorType) {
|
|
117
|
+
throw std::runtime_error("react-native-skia: unsupported Metal pixel format");
|
|
118
|
+
}
|
|
119
|
+
GrMtlTextureInfo mtlInfo;
|
|
120
|
+
mtlInfo.fTexture.retain((__bridge const void *)texture);
|
|
121
|
+
GrBackendTexture backend = GrBackendTextures::MakeMtl(
|
|
122
|
+
width, height, mipMapped ? skgpu::Mipmapped::kYes : skgpu::Mipmapped::kNo, mtlInfo);
|
|
123
|
+
return SkImages::BorrowTextureFrom(getDirectContext(), backend, kTopLeft_GrSurfaceOrigin,
|
|
124
|
+
colorType, kPremul_SkAlphaType, nullptr);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const RNSkia::TextureInfo getTexture(sk_sp<SkImage> image) override {
|
|
128
|
+
GrBackendTexture texture;
|
|
129
|
+
if (!SkImages::GetBackendTextureFromImage(image, &texture, true)) {
|
|
130
|
+
throw std::runtime_error("react-native-skia: no backend texture for this image");
|
|
131
|
+
}
|
|
132
|
+
return metalTextureInfo(texture);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const RNSkia::TextureInfo getTexture(sk_sp<SkSurface> surface) override {
|
|
136
|
+
GrBackendTexture texture = SkSurfaces::GetBackendTexture(
|
|
137
|
+
surface.get(), SkSurfaces::BackendHandleAccess::kFlushRead);
|
|
138
|
+
return metalTextureInfo(texture);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// -------------------------------------------------------------------- fonts
|
|
142
|
+
|
|
143
|
+
sk_sp<SkFontMgr> createFontMgr() override { return SkFontMgr_New_CoreText(nullptr); }
|
|
144
|
+
|
|
145
|
+
std::vector<std::string> getSystemFontFamilies() override {
|
|
146
|
+
std::vector<std::string> families;
|
|
147
|
+
NSArray<NSString *> *names = NSFontManager.sharedFontManager.availableFontFamilies;
|
|
148
|
+
families.reserve(names.count);
|
|
149
|
+
for (NSString *name in names) {
|
|
150
|
+
families.emplace_back(name.UTF8String);
|
|
151
|
+
}
|
|
152
|
+
return families;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// -------------------------------------------------------------------- media
|
|
156
|
+
|
|
157
|
+
std::shared_ptr<RNSkia::RNSkVideo> createVideo(const std::string &url) override {
|
|
158
|
+
// The package's own AVFoundation implementation, which is one of the files
|
|
159
|
+
// that mentions no React and so compiles here unchanged.
|
|
160
|
+
return std::make_shared<RNSkia::RNSkAppleVideo>(url, this);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ------------------------------------------------------------------ loading
|
|
164
|
+
|
|
165
|
+
void performStreamOperation(
|
|
166
|
+
const std::string &sourceUri,
|
|
167
|
+
const std::function<void(std::unique_ptr<SkStreamAsset>)> &op) override {
|
|
168
|
+
// On a thread, because callers treat this as asynchronous and the operation
|
|
169
|
+
// may read from the network. Detached for the same reason the package's is:
|
|
170
|
+
// the continuation is the completion.
|
|
171
|
+
//
|
|
172
|
+
// No asset-catalog branch. The package falls back to `[NSImage imageNamed:]`
|
|
173
|
+
// for a bare name with no scheme and no extension, which is an iOS app
|
|
174
|
+
// bundle's image set; a desktop host has files and URLs, and a bare name
|
|
175
|
+
// that reaches here is a bug worth seeing rather than silently resolving.
|
|
176
|
+
std::thread([sourceUri, op]() {
|
|
177
|
+
@autoreleasepool {
|
|
178
|
+
NSString *uri = [NSString stringWithUTF8String:sourceUri.c_str()];
|
|
179
|
+
NSURL *url = [NSURL URLWithString:uri];
|
|
180
|
+
NSData *data = nil;
|
|
181
|
+
if (url != nil && url.scheme != nil) {
|
|
182
|
+
data = [NSData dataWithContentsOfURL:url];
|
|
183
|
+
} else {
|
|
184
|
+
// A path rather than a URL, which is what a bundled asset looks like
|
|
185
|
+
// when the bundle was built on disk.
|
|
186
|
+
data = [NSData dataWithContentsOfFile:uri];
|
|
187
|
+
}
|
|
188
|
+
if (data == nil) {
|
|
189
|
+
LOG(WARNING) << "react-native-skia: could not read " << sourceUri;
|
|
190
|
+
op(nullptr);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
sk_sp<SkData> copy = SkData::MakeWithCopy(data.bytes, data.length);
|
|
194
|
+
op(SkMemoryStream::Make(copy));
|
|
195
|
+
}
|
|
196
|
+
}).detach();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ------------------------------------------------- not implemented, and why
|
|
200
|
+
|
|
201
|
+
sk_sp<SkImage> takeScreenshotFromViewTag(size_t tag) override {
|
|
202
|
+
// Needs a tag-to-view lookup and a render of that view into a surface. The
|
|
203
|
+
// host has the first half -- the mounting manager keeps a tag registry --
|
|
204
|
+
// and nothing yet asks for the second. Named rather than blank: an empty
|
|
205
|
+
// image here would read as a broken renderer.
|
|
206
|
+
throw std::runtime_error(
|
|
207
|
+
"react-native-skia: makeImageSnapshot of a view is not implemented on this "
|
|
208
|
+
"host (tag " + std::to_string(tag) + "). Render to an offscreen surface instead.");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
uint64_t makeNativeBuffer(sk_sp<SkImage>) override {
|
|
212
|
+
throw std::runtime_error(unsupportedNativeBuffers());
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
uint64_t makeTestNativeBuffer(int, int) override {
|
|
216
|
+
throw std::runtime_error(unsupportedNativeBuffers());
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
void releaseNativeBuffer(uint64_t) override {
|
|
220
|
+
// Deliberately quiet: nothing here hands out a buffer, so nothing can
|
|
221
|
+
// return one, and throwing from a release path would turn a leak into a
|
|
222
|
+
// crash.
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private:
|
|
226
|
+
static const RNSkia::TextureInfo metalTextureInfo(const GrBackendTexture &texture) {
|
|
227
|
+
if (!texture.isValid()) {
|
|
228
|
+
throw std::runtime_error("react-native-skia: invalid backend texture");
|
|
229
|
+
}
|
|
230
|
+
GrMtlTextureInfo mtlInfo;
|
|
231
|
+
if (!GrBackendTextures::GetMtlTextureInfo(texture, &mtlInfo)) {
|
|
232
|
+
throw std::runtime_error("react-native-skia: no Metal texture info");
|
|
233
|
+
}
|
|
234
|
+
RNSkia::TextureInfo info;
|
|
235
|
+
info.mtlTexture = mtlInfo.fTexture.get();
|
|
236
|
+
return info;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
static std::string unsupportedNativeBuffers() {
|
|
240
|
+
return "react-native-skia: CVPixelBuffer interchange is not implemented on this "
|
|
241
|
+
"host. It is used for camera and video frame sharing, which nothing here "
|
|
242
|
+
"provides yet.";
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
} // namespace
|
|
247
|
+
|
|
248
|
+
std::shared_ptr<RNSkia::RNSkPlatformContext>
|
|
249
|
+
makeSkiaPlatformContext(std::shared_ptr<facebook::react::CallInvoker> callInvoker) {
|
|
250
|
+
const float density =
|
|
251
|
+
NSScreen.mainScreen != nil ? static_cast<float>(NSScreen.mainScreen.backingScaleFactor) : 1.0f;
|
|
252
|
+
return std::make_shared<AppKitSkiaPlatformContext>(std::move(callInvoker), density);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
} // namespace basalt
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// `RNSkiaModule`, the TurboModule @shopify/react-native-skia asks for by name.
|
|
2
|
+
//
|
|
3
|
+
// The package's own is 61 lines: one blocking synchronous `install` that builds
|
|
4
|
+
// a `SkiaManager` from an `RCTBridge` and a call invoker, and the manager wraps
|
|
5
|
+
// `RNSkia::RNSkManager`, which is portable C++ over JSI. basalt has a runtime and
|
|
6
|
+
// a call invoker, so all that is left is the platform context -- see
|
|
7
|
+
// AppKitSkiaContext.h -- and this.
|
|
8
|
+
//
|
|
9
|
+
// `install` is where Skia's whole JavaScript API arrives. Constructing
|
|
10
|
+
// RNSkManager installs `SkiaApi` and the rest onto the global object, so
|
|
11
|
+
// everything the package's JavaScript reaches for exists from that moment. Until
|
|
12
|
+
// it is called, `getEnforcing('RNSkiaModule')` throws and an app that imports
|
|
13
|
+
// Skia does not start at all.
|
|
14
|
+
//
|
|
15
|
+
// In the AppKit package rather than in core, for the reason Skia.cmake gives:
|
|
16
|
+
// the Metal and AppKit halves are a host's business, and `basalt_core_probe`
|
|
17
|
+
// said so by refusing to link NSScreen.
|
|
18
|
+
|
|
19
|
+
#pragma once
|
|
20
|
+
|
|
21
|
+
#include <ReactCommon/TurboModule.h>
|
|
22
|
+
|
|
23
|
+
#include "RNSkManager.h"
|
|
24
|
+
|
|
25
|
+
#include <memory>
|
|
26
|
+
|
|
27
|
+
namespace basalt {
|
|
28
|
+
|
|
29
|
+
class AppKitSkiaModule : public facebook::react::TurboModule {
|
|
30
|
+
public:
|
|
31
|
+
static constexpr const char *kModuleName = "RNSkiaModule";
|
|
32
|
+
|
|
33
|
+
explicit AppKitSkiaModule(std::shared_ptr<facebook::react::CallInvoker> jsInvoker);
|
|
34
|
+
~AppKitSkiaModule() override;
|
|
35
|
+
|
|
36
|
+
// The manager, or null before `install` has run. A `<Canvas>` needs it to
|
|
37
|
+
// register itself by nativeId, and the lifetime is the runtime's rather than
|
|
38
|
+
// any one view's -- which is why it is reachable here and not owned by a view.
|
|
39
|
+
//
|
|
40
|
+
// Only valid on the thread that installed it. Every caller so far is the UI
|
|
41
|
+
// thread, which is also where mounting happens.
|
|
42
|
+
static RNSkia::RNSkManager *manager();
|
|
43
|
+
|
|
44
|
+
private:
|
|
45
|
+
// A plain function, because `MethodMetadata::invoker` is a function pointer
|
|
46
|
+
// rather than a std::function -- a TurboModule's methods are static dispatch.
|
|
47
|
+
static facebook::jsi::Value install(facebook::jsi::Runtime &runtime,
|
|
48
|
+
facebook::react::TurboModule &module,
|
|
49
|
+
const facebook::jsi::Value *args,
|
|
50
|
+
size_t count);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
} // namespace basalt
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#include "AppKitSkiaModule.h"
|
|
2
|
+
|
|
3
|
+
#include "AppKitSkiaContext.h"
|
|
4
|
+
|
|
5
|
+
#include "RNSkManager.h"
|
|
6
|
+
|
|
7
|
+
#include <glog/logging.h>
|
|
8
|
+
|
|
9
|
+
#include <memory>
|
|
10
|
+
#include <utility>
|
|
11
|
+
|
|
12
|
+
namespace basalt {
|
|
13
|
+
namespace {
|
|
14
|
+
|
|
15
|
+
// The manager outlives the call that made it and is the owner of everything
|
|
16
|
+
// Skia installed into the runtime, so it is held here rather than on the module:
|
|
17
|
+
// a TurboModule is recreated whenever the runtime is, and Skia's bindings are
|
|
18
|
+
// torn down and rebuilt with it.
|
|
19
|
+
//
|
|
20
|
+
// Reset rather than leaked on destruction, which matters for a reload: the
|
|
21
|
+
// bindings hold a jsi::Runtime reference, and keeping them past the runtime they
|
|
22
|
+
// were installed into is how a reload turns into a crash in somebody else's
|
|
23
|
+
// frame.
|
|
24
|
+
std::unique_ptr<RNSkia::RNSkManager> &skManager() {
|
|
25
|
+
static std::unique_ptr<RNSkia::RNSkManager> manager;
|
|
26
|
+
return manager;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
} // namespace
|
|
30
|
+
|
|
31
|
+
AppKitSkiaModule::AppKitSkiaModule(std::shared_ptr<facebook::react::CallInvoker> jsInvoker)
|
|
32
|
+
: TurboModule(kModuleName, std::move(jsInvoker)) {
|
|
33
|
+
// Blocking and synchronous on the JavaScript thread, because the package's
|
|
34
|
+
// JavaScript calls it and then immediately expects `global.SkiaApi` to be
|
|
35
|
+
// there. The arity is zero and the return is a boolean, matching what
|
|
36
|
+
// NativeSkiaModule's spec declares.
|
|
37
|
+
methodMap_["install"] = MethodMetadata{0, install};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
AppKitSkiaModule::~AppKitSkiaModule() { skManager().reset(); }
|
|
41
|
+
|
|
42
|
+
RNSkia::RNSkManager *AppKitSkiaModule::manager() { return skManager().get(); }
|
|
43
|
+
|
|
44
|
+
facebook::jsi::Value AppKitSkiaModule::install(facebook::jsi::Runtime &runtime,
|
|
45
|
+
facebook::react::TurboModule &module,
|
|
46
|
+
const facebook::jsi::Value * /*args*/,
|
|
47
|
+
size_t /*count*/) {
|
|
48
|
+
if (skManager() != nullptr) {
|
|
49
|
+
// Already installed. The package calls this from module scope and React
|
|
50
|
+
// Native may evaluate that more than once; a second manager over the same
|
|
51
|
+
// runtime would install a second set of bindings over the first.
|
|
52
|
+
return facebook::jsi::Value(true);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Through the derived type: `jsInvoker_` is protected on TurboModule, so a
|
|
56
|
+
// static member of this class may reach it on an AppKitSkiaModule and not on a
|
|
57
|
+
// TurboModule. The reference is always ours -- the registry only calls this
|
|
58
|
+
// through the method table we filled in.
|
|
59
|
+
auto invoker = static_cast<AppKitSkiaModule &>(module).jsInvoker_;
|
|
60
|
+
if (invoker == nullptr) {
|
|
61
|
+
LOG(ERROR) << "RNSkiaModule: no call invoker, so Skia cannot be installed";
|
|
62
|
+
return facebook::jsi::Value(false);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
skManager() = std::make_unique<RNSkia::RNSkManager>(
|
|
67
|
+
&runtime, invoker, makeSkiaPlatformContext(invoker));
|
|
68
|
+
} catch (const std::exception &error) {
|
|
69
|
+
// Reported as false rather than thrown. The package's JavaScript checks the
|
|
70
|
+
// return and says something useful about it; an exception here crosses the
|
|
71
|
+
// JSI boundary during module initialisation, where the message tends to
|
|
72
|
+
// arrive without a stack and without naming Skia.
|
|
73
|
+
LOG(ERROR) << "RNSkiaModule: install failed: " << error.what();
|
|
74
|
+
return facebook::jsi::Value(false);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
LOG(INFO) << "RNSkiaModule: Skia installed";
|
|
78
|
+
return facebook::jsi::Value(true);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
} // namespace basalt
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// The AppKit half of a `<Canvas>`: a Metal layer inside an ordinary view.
|
|
2
|
+
//
|
|
3
|
+
// The package's SkiaUIView is an RCTViewComponentView that does three things --
|
|
4
|
+
// creates a drawing view, adds its layer as a sublayer of its own, and registers
|
|
5
|
+
// it with RNSkManager under the nativeId. None of the three needs React Native;
|
|
6
|
+
// what needed React Native was being an RCTViewComponentView. Here the view is
|
|
7
|
+
// already an `RnAppKitView` that the mounting manager made, so this is the three
|
|
8
|
+
// things and nothing else.
|
|
9
|
+
//
|
|
10
|
+
// A sublayer rather than a view of its own, exactly as the package does it: the
|
|
11
|
+
// Skia layer draws itself on the GPU and takes no part in layout, hit testing or
|
|
12
|
+
// the view tree. Making it a view would put it in all three.
|
|
13
|
+
|
|
14
|
+
#pragma once
|
|
15
|
+
|
|
16
|
+
#include <react/renderer/core/ReactPrimitives.h>
|
|
17
|
+
#include <react/renderer/mounting/ShadowView.h>
|
|
18
|
+
|
|
19
|
+
@class RnAppKitView;
|
|
20
|
+
|
|
21
|
+
namespace basalt {
|
|
22
|
+
|
|
23
|
+
// Gives `view` a Skia canvas if `shadowView` is a SkiaPictureView, updating its
|
|
24
|
+
// size if it already has one. False when the component is something else, so the
|
|
25
|
+
// caller can treat this as a filter rather than asking twice.
|
|
26
|
+
//
|
|
27
|
+
// Safe to call before `install` has run: without a manager there is nothing to
|
|
28
|
+
// register with, and it does nothing and says so in the log rather than creating
|
|
29
|
+
// a drawing view that no picture could ever reach.
|
|
30
|
+
bool applySkiaCanvas(RnAppKitView *view, const facebook::react::ShadowView &shadowView);
|
|
31
|
+
|
|
32
|
+
// Unregisters and releases the canvas for `tag`, if it had one. Called when the
|
|
33
|
+
// mounting walk forgets a tag, which is the one moment a view is known to be
|
|
34
|
+
// finished with.
|
|
35
|
+
void forgetSkiaCanvas(facebook::react::Tag tag);
|
|
36
|
+
|
|
37
|
+
} // namespace basalt
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
#include "AppKitSkiaPeer.h"
|
|
2
|
+
|
|
3
|
+
#include "AppKitSkiaModule.h"
|
|
4
|
+
#include "AppKitSkiaView.h"
|
|
5
|
+
#include "SkiaPictureViewComponent.h"
|
|
6
|
+
|
|
7
|
+
#import "RnAppKitView.h"
|
|
8
|
+
|
|
9
|
+
#include "RNSkPictureView.h"
|
|
10
|
+
|
|
11
|
+
#include <glog/logging.h>
|
|
12
|
+
|
|
13
|
+
#include <memory>
|
|
14
|
+
#include <string>
|
|
15
|
+
#include <unordered_map>
|
|
16
|
+
|
|
17
|
+
namespace basalt {
|
|
18
|
+
namespace {
|
|
19
|
+
|
|
20
|
+
using Canvas = SkiaDrawingViewOf<RNSkia::RNSkPictureView>;
|
|
21
|
+
|
|
22
|
+
struct Attached {
|
|
23
|
+
std::shared_ptr<Canvas> canvas;
|
|
24
|
+
// The id it was registered under, kept so the unregister matches: a view whose
|
|
25
|
+
// nativeID changed would otherwise leave the old registration behind, pointing
|
|
26
|
+
// at a canvas nothing draws into.
|
|
27
|
+
size_t nativeId = 0;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
std::unordered_map<facebook::react::Tag, Attached> &attached() {
|
|
31
|
+
static std::unordered_map<facebook::react::Tag, Attached> map;
|
|
32
|
+
return map;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// `<Canvas>` stringifies a counter into nativeID. Anything else on a
|
|
36
|
+
// SkiaPictureView is not ours to interpret.
|
|
37
|
+
bool parseNativeId(NSString *identifier, size_t &out) {
|
|
38
|
+
if (identifier == nil || identifier.length == 0) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
const std::string text = identifier.UTF8String;
|
|
42
|
+
try {
|
|
43
|
+
size_t consumed = 0;
|
|
44
|
+
const unsigned long long value = std::stoull(text, &consumed);
|
|
45
|
+
if (consumed != text.size()) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
out = static_cast<size_t>(value);
|
|
49
|
+
return true;
|
|
50
|
+
} catch (const std::exception &) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
} // namespace
|
|
56
|
+
|
|
57
|
+
bool applySkiaCanvas(RnAppKitView *view, const facebook::react::ShadowView &shadowView) {
|
|
58
|
+
if (shadowView.componentName == nullptr ||
|
|
59
|
+
std::string(shadowView.componentName) !=
|
|
60
|
+
facebook::react::SkiaPictureViewComponentName) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const auto tag = shadowView.tag;
|
|
65
|
+
// In points, which is what the drawing view wants: the canvas provider scales
|
|
66
|
+
// by the layer's contentsScale itself, and passing pixels here draws at twice
|
|
67
|
+
// the size on a Retina display.
|
|
68
|
+
const auto &frame = shadowView.layoutMetrics.frame;
|
|
69
|
+
const int width = static_cast<int>(frame.size.width);
|
|
70
|
+
const int height = static_cast<int>(frame.size.height);
|
|
71
|
+
|
|
72
|
+
size_t nativeId = 0;
|
|
73
|
+
if (!parseNativeId(view.rnNativeId, nativeId)) {
|
|
74
|
+
// Not an error: a SkiaPictureView is mounted before its nativeID has been
|
|
75
|
+
// applied in some orders, and the next updateView carries it.
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
RNSkia::RNSkManager *manager = AppKitSkiaModule::manager();
|
|
80
|
+
if (manager == nullptr) {
|
|
81
|
+
LOG(WARNING) << "SkiaPictureView tag " << tag
|
|
82
|
+
<< " mounted before RNSkiaModule.install ran, so it has no canvas";
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
auto &entry = attached()[tag];
|
|
87
|
+
if (entry.canvas == nullptr) {
|
|
88
|
+
entry.canvas = std::make_shared<Canvas>(manager->getPlatformContext());
|
|
89
|
+
entry.nativeId = nativeId;
|
|
90
|
+
// Under the view's own layer, and below nothing: a canvas has no React
|
|
91
|
+
// children to sit above, and the package adds it the same way.
|
|
92
|
+
[view.layer addSublayer:entry.canvas->layer()];
|
|
93
|
+
manager->setSkiaView(nativeId, entry.canvas->drawView());
|
|
94
|
+
LOG(INFO) << "SkiaPictureView tag " << tag << " registered as Skia view " << nativeId;
|
|
95
|
+
} else if (entry.nativeId != nativeId) {
|
|
96
|
+
// The id moved. Re-register rather than leave the manager pointing the old
|
|
97
|
+
// canvas at the new id's pictures.
|
|
98
|
+
manager->unregisterSkiaView(entry.nativeId);
|
|
99
|
+
entry.nativeId = nativeId;
|
|
100
|
+
manager->setSkiaView(nativeId, entry.canvas->drawView());
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (width > 0 && height > 0) {
|
|
104
|
+
entry.canvas->setSize(width, height);
|
|
105
|
+
}
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
void forgetSkiaCanvas(facebook::react::Tag tag) {
|
|
110
|
+
auto it = attached().find(tag);
|
|
111
|
+
if (it == attached().end()) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (RNSkia::RNSkManager *manager = AppKitSkiaModule::manager()) {
|
|
115
|
+
manager->unregisterSkiaView(it->second.nativeId);
|
|
116
|
+
}
|
|
117
|
+
if (it->second.canvas != nullptr) {
|
|
118
|
+
[it->second.canvas->layer() removeFromSuperlayer];
|
|
119
|
+
}
|
|
120
|
+
attached().erase(it);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
} // namespace basalt
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// The drawing half of a `<Canvas>`, which is Skia's and not ours.
|
|
2
|
+
//
|
|
3
|
+
// `RNSkAppleView.h` in the package is this, in fifty header-only lines, and
|
|
4
|
+
// basalt cannot include it: its first `#import` is
|
|
5
|
+
// "RNSkApplePlatformContext.h", whose own first line is
|
|
6
|
+
// <React/RCTBridge+Private.h>. Nothing in the template needs either -- it uses
|
|
7
|
+
// `RNSkia::RNSkPlatformContext`, which is portable C++ in cpp/rnskia, and
|
|
8
|
+
// `RNSkMetalCanvasProvider`, which compiles here already. So this is the same
|
|
9
|
+
// template with the include it does not need left out.
|
|
10
|
+
//
|
|
11
|
+
// Copied rather than shimmed. A shim `React/RCTBridge+Private.h` would make the
|
|
12
|
+
// original compile and would then have to keep making it compile, for a header
|
|
13
|
+
// whose contents are not ours to predict. Fifty lines that say what they do are
|
|
14
|
+
// the cheaper of the two, and the thing being copied is a bridge between two
|
|
15
|
+
// interfaces rather than logic that can drift.
|
|
16
|
+
|
|
17
|
+
#pragma once
|
|
18
|
+
|
|
19
|
+
#import <QuartzCore/QuartzCore.h>
|
|
20
|
+
|
|
21
|
+
#include "RNSkMetalCanvasProvider.h"
|
|
22
|
+
#include "RNSkPlatformContext.h"
|
|
23
|
+
#include "RNSkView.h"
|
|
24
|
+
|
|
25
|
+
#include <functional>
|
|
26
|
+
#include <memory>
|
|
27
|
+
|
|
28
|
+
namespace basalt {
|
|
29
|
+
|
|
30
|
+
// What the AppKit view needs of whatever is drawing into it, without knowing
|
|
31
|
+
// which RNSkView subclass that is.
|
|
32
|
+
class SkiaDrawingView {
|
|
33
|
+
public:
|
|
34
|
+
virtual ~SkiaDrawingView() = default;
|
|
35
|
+
virtual CALayer *layer() = 0;
|
|
36
|
+
virtual void setSize(int width, int height) = 0;
|
|
37
|
+
virtual std::shared_ptr<RNSkia::RNSkView> drawView() = 0;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// T is an RNSkView subclass -- RNSkPictureView for a `<Canvas>`. The canvas
|
|
41
|
+
// provider is Metal's, and its redraw callback is the view's own
|
|
42
|
+
// `requestRedraw`, which is how a picture pushed from JavaScript reaches the
|
|
43
|
+
// screen without anybody polling.
|
|
44
|
+
template <class T> class SkiaDrawingViewOf final : public SkiaDrawingView, public T {
|
|
45
|
+
public:
|
|
46
|
+
explicit SkiaDrawingViewOf(std::shared_ptr<RNSkia::RNSkPlatformContext> context)
|
|
47
|
+
: T(context, std::make_shared<RNSkMetalCanvasProvider>(
|
|
48
|
+
std::bind(&RNSkia::RNSkView::requestRedraw, this), context, true)) {}
|
|
49
|
+
|
|
50
|
+
CALayer *layer() override { return provider()->getLayer(); }
|
|
51
|
+
|
|
52
|
+
void setSize(int width, int height) override { provider()->setSize(width, height); }
|
|
53
|
+
|
|
54
|
+
std::shared_ptr<RNSkia::RNSkView> drawView() override { return this->shared_from_this(); }
|
|
55
|
+
|
|
56
|
+
private:
|
|
57
|
+
std::shared_ptr<RNSkMetalCanvasProvider> provider() {
|
|
58
|
+
return std::static_pointer_cast<RNSkMetalCanvasProvider>(this->getCanvasProvider());
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
} // namespace basalt
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Rendering a view tree to a PNG without showing it.
|
|
2
|
+
//
|
|
3
|
+
// Shared by the demo and the mount harness, and the only way either of them
|
|
4
|
+
// proves anything: a tree dump prints the frames that were *set*, which looks
|
|
5
|
+
// identical whether or not the coordinate system, the layer tree and the paint
|
|
6
|
+
// path are right. A picture does not.
|
|
7
|
+
|
|
8
|
+
#pragma once
|
|
9
|
+
|
|
10
|
+
#ifdef __OBJC__
|
|
11
|
+
#import "RnAppKitView.h"
|
|
12
|
+
|
|
13
|
+
// Renders `root` to a PNG at `path`. Returns false if the file could not be
|
|
14
|
+
// written. Needs no window server and shows nothing on screen.
|
|
15
|
+
//
|
|
16
|
+
// Works on a detached tree and on one already mounted in a live window; a
|
|
17
|
+
// detached one gets an offscreen window of its own, because AppKit only
|
|
18
|
+
// assembles a layer tree during a display cycle and a view with no window never
|
|
19
|
+
// has one.
|
|
20
|
+
bool RnAppKitWriteSnapshot(RnAppKitView *root, NSString *path);
|
|
21
|
+
#endif
|