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.
Files changed (69) hide show
  1. package/LICENSE +21 -0
  2. package/cli/runMacos.js +33 -0
  3. package/cli/shared.js +75 -0
  4. package/native/CMakeLists.txt +298 -0
  5. package/native/appkit/AppKitAnimationChoreographer.h +54 -0
  6. package/native/appkit/AppKitAnimationChoreographer.mm +125 -0
  7. package/native/appkit/AppKitDropTarget.h +34 -0
  8. package/native/appkit/AppKitDropTarget.mm +292 -0
  9. package/native/appkit/AppKitFocus.h +120 -0
  10. package/native/appkit/AppKitFocus.mm +293 -0
  11. package/native/appkit/AppKitImageLoader.h +80 -0
  12. package/native/appkit/AppKitImageLoader.mm +156 -0
  13. package/native/appkit/AppKitKeyEvents.h +26 -0
  14. package/native/appkit/AppKitKeyEvents.mm +97 -0
  15. package/native/appkit/AppKitMenuBar.mm +424 -0
  16. package/native/appkit/AppKitMountingManager.h +135 -0
  17. package/native/appkit/AppKitMountingManager.mm +946 -0
  18. package/native/appkit/AppKitRunLoopObserver.h +34 -0
  19. package/native/appkit/AppKitRunLoopObserver.mm +42 -0
  20. package/native/appkit/AppKitScrollView.h +168 -0
  21. package/native/appkit/AppKitScrollView.mm +550 -0
  22. package/native/appkit/AppKitSkiaContext.h +33 -0
  23. package/native/appkit/AppKitSkiaContext.mm +255 -0
  24. package/native/appkit/AppKitSkiaModule.h +53 -0
  25. package/native/appkit/AppKitSkiaModule.mm +81 -0
  26. package/native/appkit/AppKitSkiaPeer.h +37 -0
  27. package/native/appkit/AppKitSkiaPeer.mm +123 -0
  28. package/native/appkit/AppKitSkiaView.h +62 -0
  29. package/native/appkit/AppKitSnapshot.h +21 -0
  30. package/native/appkit/AppKitSnapshot.mm +91 -0
  31. package/native/appkit/AppKitTextInput.h +140 -0
  32. package/native/appkit/AppKitTextInput.mm +588 -0
  33. package/native/appkit/AppKitTextPeer.h +74 -0
  34. package/native/appkit/AppKitTextPeer.mm +353 -0
  35. package/native/appkit/AppKitTitleBar.h +90 -0
  36. package/native/appkit/AppKitTitleBar.mm +186 -0
  37. package/native/appkit/AppKitTouchDispatcher.h +138 -0
  38. package/native/appkit/AppKitTouchDispatcher.mm +514 -0
  39. package/native/appkit/AppKitWindowControl.mm +222 -0
  40. package/native/appkit/AppKitWindowModule.h +62 -0
  41. package/native/appkit/AppKitWindowModule.mm +194 -0
  42. package/native/appkit/ColorSchemeAppKit.mm +71 -0
  43. package/native/appkit/ComponentRegistryAppKit.mm +121 -0
  44. package/native/appkit/CoreTextLayout.h +43 -0
  45. package/native/appkit/CoreTextLayout.mm +252 -0
  46. package/native/appkit/CoreTextLayoutManager.mm +138 -0
  47. package/native/appkit/FontRegistryCoreText.mm +148 -0
  48. package/native/appkit/PlatformServicesAppKit.mm +540 -0
  49. package/native/appkit/RnAppKitView.h +417 -0
  50. package/native/appkit/RnAppKitView.mm +1605 -0
  51. package/native/appkit/RnTextLayout.h +38 -0
  52. package/native/appkit/RnTextLayout.mm +168 -0
  53. package/native/appkit/demo_layout_appkit.mm +93 -0
  54. package/native/appkit/main_appkit.mm +1860 -0
  55. package/native/appkit/mount_harness_appkit.mm +197 -0
  56. package/native/tests/test_appkit_accessibility.mm +170 -0
  57. package/native/tests/test_appkit_image.mm +237 -0
  58. package/native/tests/test_appkit_input.mm +852 -0
  59. package/native/tests/test_appkit_key_events.mm +150 -0
  60. package/native/tests/test_appkit_mounting.mm +430 -0
  61. package/native/tests/test_appkit_scrollbar.mm +132 -0
  62. package/native/tests/test_appkit_services.mm +57 -0
  63. package/native/tests/test_appkit_skia.mm +212 -0
  64. package/native/tests/test_appkit_text.mm +180 -0
  65. package/native/tests/test_appkit_textinput.mm +748 -0
  66. package/native/tests/test_appkit_titlebar.mm +107 -0
  67. package/native/tests/test_appkit_view.mm +237 -0
  68. package/package.json +26 -0
  69. 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