basalt-win32 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 (83) hide show
  1. package/LICENSE +21 -0
  2. package/cli/runWindows.js +35 -0
  3. package/cli/shared.js +75 -0
  4. package/native/CMakeLists.txt +398 -0
  5. package/native/tests/test_win32_accessibility.cpp +271 -0
  6. package/native/tests/test_win32_clipboard.cpp +121 -0
  7. package/native/tests/test_win32_focus.cpp +195 -0
  8. package/native/tests/test_win32_hittest.cpp +410 -0
  9. package/native/tests/test_win32_image.cpp +239 -0
  10. package/native/tests/test_win32_imageloader.cpp +208 -0
  11. package/native/tests/test_win32_input.cpp +361 -0
  12. package/native/tests/test_win32_key_events.cpp +84 -0
  13. package/native/tests/test_win32_mounting.cpp +410 -0
  14. package/native/tests/test_win32_paint.cpp +505 -0
  15. package/native/tests/test_win32_scrollview.cpp +385 -0
  16. package/native/tests/test_win32_text.cpp +258 -0
  17. package/native/tests/test_win32_textinput.cpp +609 -0
  18. package/native/tests/test_win32_titlebar.cpp +111 -0
  19. package/native/tests/test_win32_titlebar_actions.cpp +62 -0
  20. package/native/tests/test_win32_view.cpp +222 -0
  21. package/native/win32/ColorSchemeWin32.cpp +56 -0
  22. package/native/win32/ComponentRegistryWin32.cpp +124 -0
  23. package/native/win32/DirectWriteLayout.cpp +116 -0
  24. package/native/win32/DirectWriteLayout.h +36 -0
  25. package/native/win32/DirectWriteLayoutManager.cpp +124 -0
  26. package/native/win32/FontRegistryDirectWrite.cpp +117 -0
  27. package/native/win32/PlatformServicesWin32.cpp +643 -0
  28. package/native/win32/RnWin32Accessible.cpp +258 -0
  29. package/native/win32/RnWin32Accessible.h +91 -0
  30. package/native/win32/RnWin32Image.cpp +265 -0
  31. package/native/win32/RnWin32Image.h +93 -0
  32. package/native/win32/RnWin32TextLayout.cpp +310 -0
  33. package/native/win32/RnWin32TextLayout.h +160 -0
  34. package/native/win32/RnWin32View.cpp +1281 -0
  35. package/native/win32/RnWin32View.h +506 -0
  36. package/native/win32/Win32AnimationChoreographer.cpp +76 -0
  37. package/native/win32/Win32AnimationChoreographer.h +61 -0
  38. package/native/win32/Win32Clip.h +173 -0
  39. package/native/win32/Win32DropTarget.cpp +567 -0
  40. package/native/win32/Win32DropTarget.h +42 -0
  41. package/native/win32/Win32Focus.cpp +262 -0
  42. package/native/win32/Win32Focus.h +109 -0
  43. package/native/win32/Win32ImageLoader.cpp +167 -0
  44. package/native/win32/Win32ImageLoader.h +104 -0
  45. package/native/win32/Win32KeyEvents.cpp +126 -0
  46. package/native/win32/Win32KeyEvents.h +20 -0
  47. package/native/win32/Win32MenuBar.cpp +352 -0
  48. package/native/win32/Win32MenuBar.h +16 -0
  49. package/native/win32/Win32MountingManager.cpp +727 -0
  50. package/native/win32/Win32MountingManager.h +203 -0
  51. package/native/win32/Win32Packaging.cpp +161 -0
  52. package/native/win32/Win32Packaging.h +56 -0
  53. package/native/win32/Win32RunLoopObserver.cpp +48 -0
  54. package/native/win32/Win32RunLoopObserver.h +42 -0
  55. package/native/win32/Win32ScrollView.cpp +515 -0
  56. package/native/win32/Win32ScrollView.h +208 -0
  57. package/native/win32/Win32SkiaContext.cpp +221 -0
  58. package/native/win32/Win32SkiaContext.h +53 -0
  59. package/native/win32/Win32SkiaModule.cpp +80 -0
  60. package/native/win32/Win32SkiaModule.h +50 -0
  61. package/native/win32/Win32SkiaPeer.cpp +358 -0
  62. package/native/win32/Win32SkiaPeer.h +66 -0
  63. package/native/win32/Win32SkiaUnicode.cpp +42 -0
  64. package/native/win32/Win32Snapshot.cpp +201 -0
  65. package/native/win32/Win32Snapshot.h +66 -0
  66. package/native/win32/Win32Strings.h +54 -0
  67. package/native/win32/Win32TextInput.cpp +811 -0
  68. package/native/win32/Win32TextInput.h +232 -0
  69. package/native/win32/Win32TitleBar.cpp +562 -0
  70. package/native/win32/Win32TitleBar.h +146 -0
  71. package/native/win32/Win32TitleBarLayout.cpp +46 -0
  72. package/native/win32/Win32TitleBarLayout.h +58 -0
  73. package/native/win32/Win32TouchDispatcher.cpp +443 -0
  74. package/native/win32/Win32TouchDispatcher.h +153 -0
  75. package/native/win32/Win32UiThread.h +27 -0
  76. package/native/win32/Win32WindowControl.cpp +329 -0
  77. package/native/win32/Win32WindowModule.cpp +209 -0
  78. package/native/win32/Win32WindowModule.h +72 -0
  79. package/native/win32/demo_layout_win32.cpp +232 -0
  80. package/native/win32/main_win32.cpp +2481 -0
  81. package/native/win32/mount_harness_win32.cpp +316 -0
  82. package/package.json +26 -0
  83. package/react-native.config.js +22 -0
@@ -0,0 +1,208 @@
1
+ // <ScrollView> on Win32.
2
+ //
3
+ // Yoga does the hard part. A ScrollView's Yoga node carries `overflow: scroll`,
4
+ // which lets its child exceed the viewport instead of being clamped to it, so
5
+ // by the time a mutation arrives the content is already laid out at its full
6
+ // size and the only thing missing is the offset.
7
+ //
8
+ // Structure: a mounted ScrollView has exactly **one** child, a content view.
9
+ // React Native's JS wraps the children in an `RCTScrollContentView`, which the
10
+ // C++ registry rewrites to a plain `View`, so no extra descriptor is needed and
11
+ // nothing here should expect N children.
12
+ //
13
+ // Scrolling is `RnWin32View::setScrollOffset`, which shifts children while
14
+ // painting rather than moving them. The frames the mounting manager wrote stay
15
+ // exactly the ones Yoga produced, nothing has to be undone on the next
16
+ // mutation, and `hitTest` adds the same offset back on the way down -- so
17
+ // picking follows the scroll without knowing what a ScrollView is. GTK reaches
18
+ // the same place by shifting children in its layout manager and AppKit by
19
+ // moving `bounds.origin`.
20
+ //
21
+ // Two things must happen on every scroll, and they are not the same thing:
22
+ //
23
+ // - `onScroll` goes to JavaScript, throttled by `scrollEventThrottle`. Without
24
+ // it VirtualizedList never renders past its first window.
25
+ // - `contentOffset` is written back into `ScrollViewState`, unthrottled.
26
+ // `ScrollViewShadowNode::getContentOriginOffset` reads it, and through that
27
+ // so do `measure`, `measureLayout`, C++ hit testing and view culling. Skip
28
+ // it and those all silently report unscrolled coordinates.
29
+ //
30
+ // All of that is the same on all three desktops, which is why this file reads
31
+ // like GtkScrollView.cpp with different event plumbing.
32
+ //
33
+ // ## What is different here
34
+ //
35
+ // **Routing.** GTK attaches a `GtkEventControllerScroll` per widget and AppKit
36
+ // walks the responder chain; both get "a wheel over a row scrolls the list
37
+ // containing it, and a list inside a list scrolls the inner one first" for
38
+ // free. A Win32 view has no window of its own and there is no chain to walk, so
39
+ // `scrollAt` does it: hit test for the view under the pointer, then walk up
40
+ // parents offering the wheel to each ScrollView until one takes it. The first
41
+ // that does is the innermost, which is the same answer the other two get.
42
+ //
43
+ // **Drag phases.** GTK's controller reports scroll-begin and scroll-end, and
44
+ // AppKit's NSEvent carries a phase. Win32 has neither: `WM_MOUSEWHEEL` is a
45
+ // bare notch with nothing around it. So a run of wheel messages is treated as
46
+ // one drag, ended by a short idle timeout -- see `kWheelIdleMs`. Without that
47
+ // there would be no `onScrollBeginDrag` or `onScrollEndDrag` on this platform
48
+ // at all, and the components that wait for them would wait forever.
49
+ //
50
+ // What Windows does not have: momentum. `onMomentumScrollBegin` and
51
+ // `onMomentumScrollEnd` never fire here, and that is deliberate rather than
52
+ // unfinished. A precision touchpad reports `WM_MOUSEWHEEL` with fine deltas and
53
+ // no fling velocity -- inertia on Windows belongs to Direct Manipulation, which
54
+ // wants to own the viewport and so cannot be used by a platform whose whole
55
+ // invariant is that React Native decides where things go. macOS gets momentum
56
+ // from the system and GTK gets a velocity this project animates
57
+ // (core/ScrollMomentum.h); a wheel supplies neither, and modelling a fling off
58
+ // one would report a throw that never happened.
59
+
60
+ #pragma once
61
+
62
+ #include "ScrollAnimation.h"
63
+ #include "ScrollBounds.h"
64
+ #include "ScrollIndicator.h"
65
+ #include "ScrollSnap.h"
66
+ #include "RnWin32View.h"
67
+
68
+ #include <react/renderer/components/scrollview/ScrollViewShadowNode.h>
69
+ #include <react/renderer/core/EventEmitter.h>
70
+ #include <react/renderer/mounting/ShadowView.h>
71
+
72
+ #include <cstdint>
73
+ #include <functional>
74
+ #include <string>
75
+ #include <unordered_map>
76
+
77
+ namespace basalt {
78
+
79
+ class Win32ScrollViewManager {
80
+ public:
81
+ // Emitters live in the mounting manager's registry, which is also what owns
82
+ // this, so they are reached through a lookup rather than a second copy.
83
+ using EmitterLookup = std::function<facebook::react::EventEmitter::Shared(facebook::react::Tag)>;
84
+
85
+ explicit Win32ScrollViewManager(EmitterLookup lookup);
86
+ ~Win32ScrollViewManager();
87
+
88
+ Win32ScrollViewManager(const Win32ScrollViewManager &) = delete;
89
+ Win32ScrollViewManager &operator=(const Win32ScrollViewManager &) = delete;
90
+ Win32ScrollViewManager(Win32ScrollViewManager &&) = delete;
91
+ Win32ScrollViewManager &operator=(Win32ScrollViewManager &&) = delete;
92
+
93
+ // Called for every mutation touching a ScrollView. Registers the view the
94
+ // first time, and refreshes the geometry and props after.
95
+ void update(win32::RnWin32View *view, const facebook::react::ShadowView &shadowView);
96
+
97
+ void remove(facebook::react::Tag tag);
98
+
99
+ // ScrollView's imperative commands: scrollTo, scrollToEnd. Returns false if
100
+ // the command is not one this handles.
101
+ bool dispatchCommand(facebook::react::Tag tag,
102
+ const std::string &name,
103
+ const folly::dynamic &args);
104
+
105
+ // The wheel, in pixels already: a notch is resolved against kWheelStepPixels
106
+ // by the caller, because only the message knows how many notches it was. The
107
+ // point is in `root`'s coordinates. Returns false when nothing under it
108
+ // scrolls, which is what lets the host leave the message to DefWindowProc.
109
+ bool scrollAt(win32::RnWin32View *root, double x, double y, double dx, double dy);
110
+
111
+ // A wheel notch carries no pixel distance of its own, so a step has to be
112
+ // chosen. Roughly three lines of 16pt text, which is what browsers and both
113
+ // other desktops settle on -- and, at 96 DPI, close to what Windows' own
114
+ // three-line default works out to. Public because the host resolves notches
115
+ // into pixels before calling, and the two must use one number.
116
+ //
117
+ // Deliberately not `SPI_GETWHEELSCROLLLINES`. Honouring it would be the more
118
+ // native thing and would make one notch move a different distance here than
119
+ // on the other two desktops, which is the trade this project keeps making the
120
+ // other way: matching the other platform matters more than matching the
121
+ // toolkit's own default.
122
+ static constexpr double kWheelStepPixels = 53.0;
123
+
124
+ // The pull past the top of the list, which is what a <RefreshControl> is
125
+ // waiting for. Called with how far past the top this scroll asked to go, or
126
+ // with zero when the gesture went the other way or the view actually moved --
127
+ // which re-arms it. See core/PullToRefresh.h for why a desktop has to count
128
+ // this rather than measure a rubber band.
129
+ void setOverscrollTopHandler(std::function<void(facebook::react::Tag, double)> handler) {
130
+ overscrollTop_ = std::move(handler);
131
+ }
132
+
133
+ private:
134
+ std::function<void(facebook::react::Tag, double)> overscrollTop_;
135
+
136
+ struct Entry {
137
+ win32::RnWin32View *view{nullptr};
138
+ facebook::react::Tag tag{0};
139
+
140
+ // An animated `scrollTo`. Windows models no fling -- a wheel supplies no
141
+ // velocity -- but an animated scroll is given its target, so the argument
142
+ // that rules out momentum does not rule this out. Driven by a thread timer,
143
+ // which needs no window: the scroll manager has a tag, not an HWND.
144
+ ScrollSnapConfig snap;
145
+ ScrollAnimation animation;
146
+ UINT_PTR animationTimer{0};
147
+ unsigned long long animationLastMillis{0};
148
+
149
+ std::shared_ptr<const facebook::react::ScrollViewShadowNode::ConcreteState> state;
150
+
151
+ facebook::react::Size contentSize{};
152
+ facebook::react::Size containerSize{};
153
+ // `contentInset` changes how far the view scrolls;
154
+ // `scrollIndicatorInsets` changes only where the bar is drawn. See
155
+ // core/ScrollBounds.h.
156
+ facebook::react::EdgeInsets contentInset{};
157
+ facebook::react::EdgeInsets indicatorInset{};
158
+
159
+ bool scrollEnabled{true};
160
+ // `showsVerticalScrollIndicator` / `showsHorizontalScrollIndicator`. Both
161
+ // default to true, as React Native's own props do.
162
+ bool showsVerticalIndicator{true};
163
+ bool showsHorizontalIndicator{true};
164
+ // Milliseconds, as React Native's prop is. Zero means every scroll.
165
+ double eventThrottleMs{0};
166
+
167
+ double offsetX{0};
168
+ double offsetY{0};
169
+ double lastEmitMs{0};
170
+
171
+ bool dragging{false};
172
+ // Bumped on every wheel. An idle timer that wakes and finds a different
173
+ // value knows another notch arrived after it was scheduled, and leaves the
174
+ // drag alone.
175
+ std::uint64_t wheelGeneration{0};
176
+ };
177
+
178
+ bool scrollEntry(Entry &entry, double dx, double dy);
179
+ void endWheelDrag(facebook::react::Tag tag, std::uint64_t generation);
180
+
181
+ void applyOffset(Entry &entry, double x, double y, bool emitEvent);
182
+ // Recomputes both overlay scrollbars and hands them to the view to draw.
183
+ // Called wherever the offset, the content size or the container size can
184
+ // have changed, which is more places than the offset alone.
185
+ void updateIndicators(const Entry &entry);
186
+ void scrollTowards(Entry &entry, double x, double y, bool animated);
187
+ bool settleOnSnapPoint(Entry &entry, double velocityY);
188
+ void stopAnimation(Entry &entry);
189
+ void advanceAnimation(facebook::react::Tag tag, double seconds);
190
+
191
+ // The timer procedure cannot carry a pointer, so it looks the animation up by
192
+ // timer id. Static because `SetTimer` calls a plain function.
193
+ static void CALLBACK onAnimationTimer(HWND hwnd, UINT message, UINT_PTR id, DWORD now);
194
+ void emitScrollEvent(Entry &entry, const char *which);
195
+ void writeStateOffset(const Entry &entry);
196
+
197
+ EmitterLookup lookup_;
198
+ std::unordered_map<facebook::react::Tag, Entry> entries_;
199
+
200
+ // Cleared by the destructor, and held by every idle timer in flight. A timer
201
+ // that outlives this object -- a surface torn down inside the 150ms after the
202
+ // last notch -- finds the flag false and does nothing, instead of calling
203
+ // through a dangling `this`. The same arrangement Win32ImageLoader uses for
204
+ // the same reason, which is that `postDelayed` has no cancel.
205
+ std::shared_ptr<bool> alive_ = std::make_shared<bool>(true);
206
+ };
207
+
208
+ } // namespace basalt
@@ -0,0 +1,221 @@
1
+ #include "Win32SkiaContext.h"
2
+
3
+ #include "ImageBytes.h"
4
+ #include "PlatformServices.h"
5
+
6
+ #include <windows.h>
7
+
8
+ #include "include/core/SkData.h"
9
+ #include "include/core/SkFontMgr.h"
10
+ #include "include/core/SkImage.h"
11
+ #include "include/core/SkStream.h"
12
+ #include "include/core/SkSurface.h"
13
+ #include "include/core/SkTypeface.h"
14
+ #include "include/ports/SkTypeface_win.h"
15
+
16
+ #include <glog/logging.h>
17
+
18
+ #include <cstdint>
19
+ #include <memory>
20
+ #include <stdexcept>
21
+ #include <string>
22
+ #include <thread>
23
+ #include <utility>
24
+ #include <vector>
25
+
26
+ namespace basalt {
27
+ namespace {
28
+
29
+ class Win32SkiaPlatformContext : public RNSkia::RNSkPlatformContext {
30
+ public:
31
+ Win32SkiaPlatformContext(std::shared_ptr<facebook::react::CallInvoker> callInvoker,
32
+ float pixelDensity)
33
+ : RNSkia::RNSkPlatformContext(std::move(callInvoker), pixelDensity) {}
34
+
35
+ // ---------------------------------------------------------------- threading
36
+
37
+ void runOnMainThread(std::function<void()> func) override {
38
+ // basalt's own, so Skia's idea of the main thread and this host's are the
39
+ // same one. Fire and forget, which is all `postToUiThread` offers and all
40
+ // this seam promises.
41
+ postToUiThread(std::move(func));
42
+ }
43
+
44
+ // ------------------------------------------------------------------- errors
45
+
46
+ void raiseError(const std::exception &err) override {
47
+ // Logged, not fatal, as on AppKit: the package's Apple version calls
48
+ // RCTFatal and takes the app down with a red box, and a shader that fails
49
+ // to compile is not a reason to end a session.
50
+ LOG(ERROR) << "react-native-skia: " << err.what();
51
+ }
52
+
53
+ // ----------------------------------------------------------------- surfaces
54
+
55
+ sk_sp<SkSurface> makeOffscreenSurface(int width, int height,
56
+ bool useP3ColorSpace = false) override {
57
+ // The colour space is accepted and not honoured: a Display P3 surface is a
58
+ // wide-gamut answer for a host that has no colour management of its own
59
+ // yet, and silently returning sRGB is the same picture on a machine that
60
+ // is not wide-gamut. Named here so the day it matters, this is the line.
61
+ (void)useP3ColorSpace;
62
+ // CPU. See Win32SkiaContext.h for why this host is raster: everything the
63
+ // imperative API does -- paths, decoded images, typefaces -- works here,
64
+ // and so does `<Canvas>`, which draws into one of these and is then copied
65
+ // into the Direct2D walk by win32/Win32SkiaPeer.h.
66
+ return SkSurfaces::Raster(SkImageInfo::MakeN32Premul(width, height));
67
+ }
68
+
69
+ GrDirectContext *getDirectContext() override {
70
+ // Null is a documented answer rather than a failure: Skia's own call sites
71
+ // either skip the GPU path for a raster image or say "No GPU context
72
+ // available" by name. What must not happen is inventing a context that
73
+ // draws nowhere.
74
+ return nullptr;
75
+ }
76
+
77
+ // ------------------------------------------------------------------- images
78
+
79
+ sk_sp<SkImage> makeImageFromNativeBuffer(void *) override {
80
+ throw std::runtime_error(unsupportedNativeBuffers());
81
+ }
82
+
83
+ sk_sp<SkImage> makeImageFromNativeTexture(const RNSkia::TextureInfo &, int, int,
84
+ bool) override {
85
+ throw std::runtime_error(unsupportedTextures());
86
+ }
87
+
88
+ const RNSkia::TextureInfo getTexture(sk_sp<SkImage>) override {
89
+ throw std::runtime_error(unsupportedTextures());
90
+ }
91
+
92
+ const RNSkia::TextureInfo getTexture(sk_sp<SkSurface>) override {
93
+ throw std::runtime_error(unsupportedTextures());
94
+ }
95
+
96
+ // -------------------------------------------------------------------- fonts
97
+
98
+ sk_sp<SkFontMgr> createFontMgr() override {
99
+ // DirectWrite, which is what the Windows archives are built with -- the GN
100
+ // args turn freetype and fontconfig off, where the Linux build turns them
101
+ // on. Same seam, each platform's real font system.
102
+ return SkFontMgr_New_DirectWrite();
103
+ }
104
+
105
+ std::vector<std::string> getSystemFontFamilies() override {
106
+ std::vector<std::string> families;
107
+ const sk_sp<SkFontMgr> manager = createFontMgr();
108
+ if (manager == nullptr) {
109
+ return families;
110
+ }
111
+ const int count = manager->countFamilies();
112
+ families.reserve(static_cast<size_t>(count));
113
+ for (int i = 0; i < count; i++) {
114
+ SkString name;
115
+ manager->getFamilyName(i, &name);
116
+ families.emplace_back(name.c_str());
117
+ }
118
+ return families;
119
+ }
120
+
121
+ // -------------------------------------------------------------------- media
122
+
123
+ std::shared_ptr<RNSkia::RNSkVideo> createVideo(const std::string &) override {
124
+ // The package's video is AVFoundation on Apple and MediaCodec through JNI
125
+ // on Android; neither exists here. Media Foundation is the Windows answer
126
+ // and is its own piece of work -- the same conclusion kino's own audio
127
+ // module reached for the same reason.
128
+ throw std::runtime_error(
129
+ "react-native-skia: video is not implemented on this host. It needs Media "
130
+ "Foundation, which nothing here wraps yet.");
131
+ }
132
+
133
+ // ------------------------------------------------------------------ loading
134
+
135
+ void performStreamOperation(
136
+ const std::string &sourceUri,
137
+ const std::function<void(std::unique_ptr<SkStreamAsset>)> &op) override {
138
+ // On a thread, because callers treat this as asynchronous and the operation
139
+ // may read from the network. Detached for the same reason the package's is:
140
+ // the continuation is the completion.
141
+ //
142
+ // Through core's own fetcher rather than a second one written here, so a
143
+ // URI means to Skia what it means to <Image>: file, data and http, resolved
144
+ // the same way in both.
145
+ std::thread([sourceUri, op]() {
146
+ std::string bytes;
147
+ std::string error;
148
+ if (!fetchImageBytes(sourceUri, &bytes, &error)) {
149
+ LOG(WARNING) << "react-native-skia: could not read " << sourceUri << ": " << error;
150
+ // An empty stream, not a null one. The continuation in
151
+ // JsiSkDataFactory::fromURI calls `stream->getLength()` without
152
+ // checking, so a null here is an access violation rather than a
153
+ // missing image -- and a 404 on one asset would take the app down.
154
+ // Apple's implementation reaches the same place by accident: a failed
155
+ // `dataWithContentsOfURL:` leaves `data` nil, and nil's bytes and
156
+ // length are null and zero, so it too hands over an empty stream.
157
+ // Doing it deliberately keeps the two hosts on the same behaviour --
158
+ // the promise resolves with no bytes, the image draws as nothing, and
159
+ // the reason is in the log above.
160
+ op(SkMemoryStream::Make(SkData::MakeEmpty()));
161
+ return;
162
+ }
163
+ sk_sp<SkData> data = SkData::MakeWithCopy(bytes.data(), bytes.size());
164
+ op(SkMemoryStream::Make(std::move(data)));
165
+ }).detach();
166
+ }
167
+
168
+ // ------------------------------------------------- not implemented, and why
169
+
170
+ sk_sp<SkImage> takeScreenshotFromViewTag(size_t tag) override {
171
+ // Needs a tag-to-view lookup and a render of that view into a surface. The
172
+ // host has the first half -- the mounting manager keeps a tag registry --
173
+ // and nothing yet asks for the second. Named rather than blank: an empty
174
+ // image here would read as a broken renderer.
175
+ throw std::runtime_error(
176
+ "react-native-skia: makeImageSnapshot of a view is not implemented on this "
177
+ "host (tag " +
178
+ std::to_string(tag) + "). Render to an offscreen surface instead.");
179
+ }
180
+
181
+ uint64_t makeNativeBuffer(sk_sp<SkImage>) override {
182
+ throw std::runtime_error(unsupportedNativeBuffers());
183
+ }
184
+
185
+ uint64_t makeTestNativeBuffer(int, int) override {
186
+ throw std::runtime_error(unsupportedNativeBuffers());
187
+ }
188
+
189
+ void releaseNativeBuffer(uint64_t) override {
190
+ // Deliberately quiet: nothing here hands out a buffer, so nothing can
191
+ // return one, and throwing from a release path would turn a leak into a
192
+ // crash.
193
+ }
194
+
195
+ private:
196
+ static std::string unsupportedNativeBuffers() {
197
+ return "react-native-skia: native buffer interchange is not implemented on this "
198
+ "host. It is used for camera and video frame sharing, which nothing here "
199
+ "provides yet.";
200
+ }
201
+
202
+ static std::string unsupportedTextures() {
203
+ return "react-native-skia: this host has no GPU context yet, so there are no "
204
+ "textures to share. See win32/Win32SkiaContext.h.";
205
+ }
206
+ };
207
+
208
+ } // namespace
209
+
210
+ std::shared_ptr<RNSkia::RNSkPlatformContext>
211
+ makeSkiaPlatformContext(std::shared_ptr<facebook::react::CallInvoker> callInvoker) {
212
+ // The system DPI rather than a window's: Skia asks for one density up front,
213
+ // and there is no window yet when the module installs. A window dragged to a
214
+ // display of another scale is a thing to fix when something notices, which is
215
+ // the same position the AppKit half takes with NSScreen.mainScreen.
216
+ const UINT dpi = GetDpiForSystem();
217
+ const float density = dpi > 0 ? static_cast<float>(dpi) / 96.0f : 1.0f;
218
+ return std::make_shared<Win32SkiaPlatformContext>(std::move(callInvoker), density);
219
+ }
220
+
221
+ } // namespace basalt
@@ -0,0 +1,53 @@
1
+ // What @shopify/react-native-skia needs from this host.
2
+ //
3
+ // `RNSkPlatformContext` is the package's one seam: it hands Skia a runtime, a
4
+ // call invoker and this. The package ships an Apple implementation and an
5
+ // Android one, and neither can stand in here -- the Apple header takes an
6
+ // `RCTBridge *`, and Android's platform context and GL canvas provider are JNI
7
+ // all the way down. So this is basalt's, as appkit/AppKitSkiaContext.h is.
8
+ //
9
+ // ## Raster, for now
10
+ //
11
+ // AppKit's delegates to the package's `MetalContext` and has a GPU context to
12
+ // hand out. This one has none: `getDirectContext` answers null and
13
+ // `makeOffscreenSurface` returns a CPU surface.
14
+ //
15
+ // That is a deliberate first stage rather than an oversight, and it is enough
16
+ // for the imperative API, which is what an app reaching for `Skia.Path`, image
17
+ // decoding and typefaces uses. Reading the package's own call sites at 2.11.1:
18
+ // `encodeToBytes` asks for the context only when an image `isTextureBacked()`,
19
+ // which a decoded one is not; `readPixels` passes it straight to Skia, where
20
+ // null means the CPU path; and `makeNonTextureImage` throws "No GPU context
21
+ // available", which is the honest answer for an image that is already raster.
22
+ //
23
+ // `<Canvas>` is raster too, and it works: win32/Win32SkiaPeer.h renders each
24
+ // one into a surface from `makeOffscreenSurface` and hands the pixels to the
25
+ // Direct2D walk, because a view here is not a window and there is no layer to
26
+ // give the package instead. What that costs is a copy per frame and a CPU
27
+ // rasteriser; what it buys is that a canvas is clipped, scrolled and faded by
28
+ // the views above it, and appears in the offscreen snapshot, without a line of
29
+ // code for any of it.
30
+ //
31
+ // A GPU stage would be WGL or ANGLE over D3D behind `getDirectContext`, and
32
+ // would replace the surface in that peer and nothing else. The archives are
33
+ // already built with `skia_use_gl=true` against the day it is wanted.
34
+ //
35
+ // ## Fonts
36
+ //
37
+ // DirectWrite, through Skia's own `SkFontMgr_New_DirectWrite`, which is what
38
+ // the Windows archives are built with -- freetype and fontconfig are off in the
39
+ // GN args, where the Linux build has them on. The same seam, answered by
40
+ // whatever the platform's real font system is.
41
+
42
+ #pragma once
43
+
44
+ #include "RNSkPlatformContext.h"
45
+
46
+ #include <memory>
47
+
48
+ namespace basalt {
49
+
50
+ std::shared_ptr<RNSkia::RNSkPlatformContext>
51
+ makeSkiaPlatformContext(std::shared_ptr<facebook::react::CallInvoker> callInvoker);
52
+
53
+ } // namespace basalt
@@ -0,0 +1,80 @@
1
+ #include "Win32SkiaModule.h"
2
+
3
+ #include "Win32SkiaContext.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 owns everything Skia installed
16
+ // into the runtime, so it is held here rather than on the module: a TurboModule
17
+ // is recreated whenever the runtime is, and Skia's bindings are torn down and
18
+ // 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
22
+ // they were installed into is how a reload turns into a crash in somebody
23
+ // else's 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
+ Win32SkiaModule::Win32SkiaModule(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. Zero arity, boolean return, matching what NativeSkiaModule declares.
36
+ methodMap_["install"] = MethodMetadata{0, install};
37
+ }
38
+
39
+ Win32SkiaModule::~Win32SkiaModule() { skManager().reset(); }
40
+
41
+ RNSkia::RNSkManager *Win32SkiaModule::manager() { return skManager().get(); }
42
+
43
+ facebook::jsi::Value Win32SkiaModule::install(facebook::jsi::Runtime &runtime,
44
+ facebook::react::TurboModule &module,
45
+ const facebook::jsi::Value * /*args*/,
46
+ size_t /*count*/) {
47
+ if (skManager() != nullptr) {
48
+ // Already installed. The package calls this from module scope and React
49
+ // Native may evaluate that more than once; a second manager over the same
50
+ // runtime would install a second set of bindings over the first.
51
+ return facebook::jsi::Value(true);
52
+ }
53
+
54
+ // Through the derived type: `jsInvoker_` is protected on TurboModule, so a
55
+ // static member of this class may reach it on a Win32SkiaModule and not on a
56
+ // TurboModule. The reference is always ours -- the registry only calls this
57
+ // through the method table filled in above.
58
+ auto invoker = static_cast<Win32SkiaModule &>(module).jsInvoker_;
59
+ if (invoker == nullptr) {
60
+ LOG(ERROR) << "RNSkiaModule: no call invoker, so Skia cannot be installed";
61
+ return facebook::jsi::Value(false);
62
+ }
63
+
64
+ try {
65
+ skManager() = std::make_unique<RNSkia::RNSkManager>(
66
+ &runtime, invoker, makeSkiaPlatformContext(invoker));
67
+ } catch (const std::exception &error) {
68
+ // Reported as false rather than thrown, as on AppKit: the package's
69
+ // JavaScript checks the return and says something useful about it, whereas
70
+ // an exception crossing the JSI boundary during module initialisation
71
+ // arrives without a stack and without naming Skia.
72
+ LOG(ERROR) << "RNSkiaModule: install failed: " << error.what();
73
+ return facebook::jsi::Value(false);
74
+ }
75
+
76
+ LOG(INFO) << "RNSkiaModule: Skia installed";
77
+ return facebook::jsi::Value(true);
78
+ }
79
+
80
+ } // namespace basalt
@@ -0,0 +1,50 @@
1
+ // `RNSkiaModule`, the TurboModule @shopify/react-native-skia asks for by name.
2
+ //
3
+ // The AppKit half of this is appkit/AppKitSkiaModule.h and this is the same
4
+ // shape, for the same reason: the package's own module is 61 lines of
5
+ // Objective-C that builds a manager from an `RCTBridge`, and what it actually
6
+ // needs -- a runtime, a call invoker and a platform context -- basalt already
7
+ // has. So the module is ours and the manager is the package's.
8
+ //
9
+ // `install` is where Skia's whole JavaScript API arrives. Constructing
10
+ // RNSkManager installs `SkiaApi` onto the global object, so everything the
11
+ // package's JavaScript reaches for exists from that moment. Until it is called,
12
+ // `getEnforcing('RNSkiaModule')` throws and an app that imports Skia does not
13
+ // start at all -- which is exactly where kino stopped on this platform.
14
+ //
15
+ // In the win32 package rather than in core, for the reason Skia.cmake gives
16
+ // about the Apple one: a platform context is a host's business.
17
+
18
+ #pragma once
19
+
20
+ #include <ReactCommon/TurboModule.h>
21
+
22
+ #include "RNSkManager.h"
23
+
24
+ #include <memory>
25
+
26
+ namespace basalt {
27
+
28
+ class Win32SkiaModule : public facebook::react::TurboModule {
29
+ public:
30
+ static constexpr const char *kModuleName = "RNSkiaModule";
31
+
32
+ explicit Win32SkiaModule(std::shared_ptr<facebook::react::CallInvoker> jsInvoker);
33
+ ~Win32SkiaModule() override;
34
+
35
+ // The manager, or null before `install` has run. A `<Canvas>` registers
36
+ // itself with it by nativeId -- see win32/Win32SkiaPeer.h -- and the lifetime
37
+ // is the runtime's rather than any one view's, which is why it is reachable
38
+ // here and not owned by a view.
39
+ static RNSkia::RNSkManager *manager();
40
+
41
+ private:
42
+ // A plain function, because `MethodMetadata::invoker` is a function pointer
43
+ // rather than a std::function -- a TurboModule's methods are static dispatch.
44
+ static facebook::jsi::Value install(facebook::jsi::Runtime &runtime,
45
+ facebook::react::TurboModule &module,
46
+ const facebook::jsi::Value *args,
47
+ size_t count);
48
+ };
49
+
50
+ } // namespace basalt