basalt-gtk 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 (71) hide show
  1. package/LICENSE +21 -0
  2. package/cli/runLinux.js +29 -0
  3. package/cli/shared.js +75 -0
  4. package/native/CMakeLists.txt +362 -0
  5. package/native/gtk/ColorSchemeGtk.cpp +58 -0
  6. package/native/gtk/ComponentRegistryGtk.cpp +101 -0
  7. package/native/gtk/FontRegistryFontconfig.cpp +127 -0
  8. package/native/gtk/GtkAnimationChoreographer.cpp +73 -0
  9. package/native/gtk/GtkAnimationChoreographer.h +49 -0
  10. package/native/gtk/GtkDropTarget.cpp +269 -0
  11. package/native/gtk/GtkDropTarget.h +40 -0
  12. package/native/gtk/GtkFocus.cpp +296 -0
  13. package/native/gtk/GtkFocus.h +113 -0
  14. package/native/gtk/GtkImageLoader.cpp +128 -0
  15. package/native/gtk/GtkImageLoader.h +87 -0
  16. package/native/gtk/GtkKeyEvents.cpp +91 -0
  17. package/native/gtk/GtkKeyEvents.h +21 -0
  18. package/native/gtk/GtkMenuBar.cpp +45 -0
  19. package/native/gtk/GtkMountingManager.cpp +1027 -0
  20. package/native/gtk/GtkMountingManager.h +146 -0
  21. package/native/gtk/GtkRunLoopObserver.cpp +77 -0
  22. package/native/gtk/GtkRunLoopObserver.h +41 -0
  23. package/native/gtk/GtkScrollView.cpp +626 -0
  24. package/native/gtk/GtkScrollView.h +209 -0
  25. package/native/gtk/GtkSkiaContext.cpp +248 -0
  26. package/native/gtk/GtkSkiaContext.h +62 -0
  27. package/native/gtk/GtkSkiaModule.cpp +80 -0
  28. package/native/gtk/GtkSkiaModule.h +51 -0
  29. package/native/gtk/GtkSkiaPeer.cpp +285 -0
  30. package/native/gtk/GtkSkiaPeer.h +63 -0
  31. package/native/gtk/GtkTextInput.cpp +428 -0
  32. package/native/gtk/GtkTextInput.h +123 -0
  33. package/native/gtk/GtkTextPeer.cpp +425 -0
  34. package/native/gtk/GtkTextPeer.h +87 -0
  35. package/native/gtk/GtkTitleBar.cpp +273 -0
  36. package/native/gtk/GtkTitleBar.h +96 -0
  37. package/native/gtk/GtkTitleBarLayout.cpp +45 -0
  38. package/native/gtk/GtkTitleBarLayout.h +36 -0
  39. package/native/gtk/GtkTouchDispatcher.cpp +671 -0
  40. package/native/gtk/GtkTouchDispatcher.h +139 -0
  41. package/native/gtk/GtkWindowControl.cpp +223 -0
  42. package/native/gtk/GtkWindowModule.cpp +194 -0
  43. package/native/gtk/GtkWindowModule.h +62 -0
  44. package/native/gtk/PangoTextLayout.cpp +295 -0
  45. package/native/gtk/PangoTextLayout.h +54 -0
  46. package/native/gtk/PangoTextLayoutManager.cpp +140 -0
  47. package/native/gtk/PlatformServicesGtk.cpp +583 -0
  48. package/native/gtk/RnView.cpp +1407 -0
  49. package/native/gtk/RnView.h +339 -0
  50. package/native/gtk/demo_layout_gtk.cpp +57 -0
  51. package/native/gtk/main_gtk.cpp +1981 -0
  52. package/native/gtk/mount_harness_gtk.cpp +143 -0
  53. package/native/tests/TestMainGtk.cpp +25 -0
  54. package/native/tests/notification_stub.cpp +103 -0
  55. package/native/tests/test_accessibility.cpp +251 -0
  56. package/native/tests/test_focus.cpp +194 -0
  57. package/native/tests/test_hittest.cpp +242 -0
  58. package/native/tests/test_image.cpp +135 -0
  59. package/native/tests/test_key_events_gtk.cpp +112 -0
  60. package/native/tests/test_mounting.cpp +415 -0
  61. package/native/tests/test_scrollview.cpp +258 -0
  62. package/native/tests/test_skia_texture.cpp +120 -0
  63. package/native/tests/test_text.cpp +147 -0
  64. package/native/tests/test_textinput.cpp +598 -0
  65. package/native/tests/test_titlebar.cpp +137 -0
  66. package/native/tests/test_titlebar_drag.cpp +138 -0
  67. package/native/tests/test_version.cpp +49 -0
  68. package/native/tests/test_view.cpp +162 -0
  69. package/native/tests/test_viewprops.cpp +254 -0
  70. package/package.json +26 -0
  71. package/react-native.config.js +21 -0
@@ -0,0 +1,209 @@
1
+ // <ScrollView> on GTK4.
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` (`componentNameByReactViewName`), so
11
+ // no extra descriptor is needed and nothing here should expect N children.
12
+ //
13
+ // Scrolling is expressed as an offset on the ScrollView's own RnView rather
14
+ // than by a GtkScrolledWindow. GtkScrolledWindow wants to size its child
15
+ // through the measure/allocate protocol, and this platform's whole invariant is
16
+ // that React Native decides sizes and the layout manager only places things.
17
+ // Shifting children in `RnLayout::allocate` keeps that invariant and makes GTK's
18
+ // hit testing follow the scroll for free.
19
+ //
20
+ // Two things must happen on every scroll, and they are not the same thing:
21
+ //
22
+ // - `onScroll` goes to JavaScript, throttled by `scrollEventThrottle`. Without
23
+ // it VirtualizedList never renders past its first window.
24
+ // - `contentOffset` is written back into `ScrollViewState`, unthrottled.
25
+ // `ScrollViewShadowNode::getContentOriginOffset` reads it, and through that
26
+ // so do `measure`, `measureLayout`, C++ hit testing and view culling. Skip
27
+ // it and those all silently report unscrolled coordinates.
28
+
29
+ #pragma once
30
+
31
+ #include "RnView.h"
32
+ #include "ScrollAnimation.h"
33
+ #include "ScrollBounds.h"
34
+ #include "ScrollIndicator.h"
35
+ #include "ScrollSnap.h"
36
+ #include "ScrollMomentum.h"
37
+
38
+ #include <react/renderer/components/scrollview/ScrollViewShadowNode.h>
39
+ #include <react/renderer/core/EventEmitter.h>
40
+ #include <react/renderer/mounting/ShadowView.h>
41
+
42
+ #include <functional>
43
+ #include <unordered_map>
44
+
45
+ namespace basalt {
46
+
47
+ class GtkScrollViewManager {
48
+ public:
49
+ // Emitters live in the mounting manager's registry, which is also what owns
50
+ // this, so they are reached through a lookup rather than a second copy.
51
+ using EmitterLookup = std::function<facebook::react::EventEmitter::Shared(facebook::react::Tag)>;
52
+
53
+ explicit GtkScrollViewManager(EmitterLookup lookup);
54
+
55
+ // Called for every mutation touching a ScrollView. Attaches the scroll
56
+ // controller the first time, and refreshes the geometry and props after.
57
+ void update(RnView *view, const facebook::react::ShadowView &shadowView);
58
+
59
+ void remove(facebook::react::Tag tag);
60
+
61
+ // Starts a fling at a velocity in pixels per second, as GTK's `decelerate`
62
+ // reports one. Returns false if it was too slow to be worth animating.
63
+ //
64
+ // Public, and not only because the signal handler is static. The coasting is
65
+ // otherwise untestable: a real fling needs a touchscreen, and the frame clock
66
+ // that drives it needs a mapped window and two seconds of main loop. This and
67
+ // `advanceFling` are the same escape hatch `GtkTouchDispatcher::synthesiseTap`
68
+ // is, entering exactly where GDK does and skipping nothing above it.
69
+ bool fling(facebook::react::Tag tag, double velocityX, double velocityY);
70
+
71
+ // Advances a running fling by `seconds`, applying the offset and emitting
72
+ // what falls out of it. Returns false once it has finished, which is also
73
+ // when onMomentumScrollEnd has been emitted.
74
+ bool advanceFling(facebook::react::Tag tag, double seconds);
75
+
76
+ // Advances an animated `scrollTo` -- or the settle a snapping list does at
77
+ // the end of a fling -- by `seconds`, returning false once it has arrived.
78
+ //
79
+ // Public for the same reason `advanceFling` is: the animation runs on a frame
80
+ // clock, and a frame clock needs a mapped window and a main loop. Without
81
+ // this a paging list could be shown to *decide* on a page and never shown to
82
+ // reach one.
83
+ bool advanceAnimation(facebook::react::Tag tag, double seconds);
84
+
85
+ // ScrollView's imperative commands: scrollTo, scrollToEnd. Returns false if
86
+ // the command is not one this handles.
87
+ bool dispatchCommand(facebook::react::Tag tag, const std::string &name, const folly::dynamic &args);
88
+
89
+ // A wheel notch carries no pixel distance of its own, so a step has to be
90
+ // chosen. Roughly three lines of 16pt text, which is what browsers and both
91
+ // other desktops settle on. Public because the test instrument resolves
92
+ // notches into pixels before calling `scrollAt`, and the two must use one
93
+ // number.
94
+ static constexpr double kWheelStepPixels = 53.0;
95
+
96
+ // A wheel over a point, in the surface root's coordinates, for
97
+ // BASALT_TEST_SCROLL. Returns false when nothing under it scrolls.
98
+ //
99
+ // It exists for the same reason every other instrument does: a real wheel
100
+ // event needs a display server that will deliver one to a window it
101
+ // considers focused, and an automated run on a headless compositor has no
102
+ // such thing. This enters where the GtkEventControllerScroll callback does,
103
+ // so it exercises the hit test, the offset, the clamp, the state write-back,
104
+ // onScroll and the pull gesture, and skips only GDK's delivery.
105
+ bool scrollAt(RnView *root, double x, double y, double dx, double dy);
106
+
107
+ // The pull past the top of the list, which is what a <RefreshControl> is
108
+ // waiting for. Called with how far past the top this scroll asked to go, or
109
+ // with zero when the gesture went the other way or the view actually moved --
110
+ // which re-arms it. See core/PullToRefresh.h for why a desktop has to count
111
+ // this rather than measure a rubber band.
112
+ void setOverscrollTopHandler(std::function<void(facebook::react::Tag, double)> handler) {
113
+ overscrollTop_ = std::move(handler);
114
+ }
115
+
116
+
117
+
118
+ private:
119
+ std::function<void(facebook::react::Tag, double)> overscrollTop_;
120
+
121
+ struct Entry {
122
+ RnView *view{nullptr};
123
+ facebook::react::Tag tag{0};
124
+ GtkScrollViewManager *owner{nullptr};
125
+
126
+ // The GTK callbacks below hold a pointer to this Entry. std::unordered_map
127
+ // never moves its elements, so that pointer stays valid for the life of the
128
+ // entry -- but erasing one would leave the controller pointing at freed
129
+ // memory, so remove() detaches the controller first.
130
+ GtkEventController *controller{nullptr};
131
+
132
+ std::shared_ptr<const facebook::react::ScrollViewShadowNode::ConcreteState> state;
133
+
134
+ facebook::react::Size contentSize{};
135
+ facebook::react::Size containerSize{};
136
+ // `contentInset` changes how far the view scrolls;
137
+ // `scrollIndicatorInsets` changes only where the bar is drawn. See
138
+ // core/ScrollBounds.h.
139
+ facebook::react::EdgeInsets contentInset{};
140
+ facebook::react::EdgeInsets indicatorInset{};
141
+
142
+ bool scrollEnabled{true};
143
+ // `showsVerticalScrollIndicator` / `showsHorizontalScrollIndicator`. Both
144
+ // default to true, as React Native's own props do.
145
+ bool showsVerticalIndicator{true};
146
+ bool showsHorizontalIndicator{true};
147
+ // Milliseconds, as React Native's prop is. Zero means every scroll.
148
+ double eventThrottleMs{0};
149
+
150
+ double offsetX{0};
151
+ double offsetY{0};
152
+ gint64 lastEmitMicros{0};
153
+ bool dragging{false};
154
+
155
+ // The fling after the fingers left. GTK hands over a velocity and stops, so
156
+ // the coasting is run here: a tick callback on the view, advancing the
157
+ // model, until it falls below a pixel a frame or hits an edge.
158
+ ScrollMomentum momentum;
159
+ guint momentumTickId{0};
160
+ // An animated `scrollTo`, which shares the frame clock with the fling above
161
+ // and never runs at the same time as one: each stops the other.
162
+ ScrollAnimation animation;
163
+ guint animationTickId{0};
164
+ gint64 animationLastMicros{0};
165
+ gint64 momentumLastMicros{0};
166
+ // React Native's decelerationRate prop, read off the ScrollView's props so
167
+ // an app that asks for a fast fling gets one.
168
+ double decelerationRate{ScrollMomentum::kNormalDeceleration};
169
+ // `pagingEnabled`, `snapToInterval`, `snapToOffsets`, `snapToAlignment`.
170
+ ScrollSnapConfig snap;
171
+ };
172
+
173
+ static gboolean onScroll(GtkEventControllerScroll *controller, double dx, double dy, gpointer userData);
174
+ // The part of onScroll that is not about GTK's event controller, so the test
175
+ // instrument and a real wheel go through the same code.
176
+ void scrollEntry(Entry &entry, double dx, double dy);
177
+ static void onScrollBegin(GtkEventControllerScroll *controller, gpointer userData);
178
+ static void onScrollEnd(GtkEventControllerScroll *controller, gpointer userData);
179
+ static void onDecelerate(GtkEventControllerScroll *controller,
180
+ double velocityX,
181
+ double velocityY,
182
+ gpointer userData);
183
+ static gboolean onMomentumTick(GtkWidget *widget, GdkFrameClock *clock, gpointer userData);
184
+ static gboolean onAnimationTick(GtkWidget *widget, GdkFrameClock *clock, gpointer userData);
185
+ void scrollTowards(Entry &entry, double x, double y, bool animated);
186
+ // Settles a drag or a fling on a snap point, if the props ask for one.
187
+ // Returns whether it took over.
188
+ bool settleOnSnapPoint(Entry &entry, double velocityY);
189
+
190
+ void stopAnimation(Entry &entry);
191
+
192
+ // Ends a fling, with or without telling JavaScript. `emitEnd` is false only
193
+ // when the ScrollView itself is going away, where the emitter is about to
194
+ // stop existing anyway.
195
+ void stopMomentum(Entry &entry, bool emitEnd);
196
+
197
+ void applyOffset(Entry &entry, double x, double y, bool emitEvent);
198
+ // Recomputes both overlay scrollbars and hands them to the view to draw.
199
+ // Called wherever the offset, the content size or the container size can
200
+ // have changed, which is more places than the offset alone.
201
+ void updateIndicators(const Entry &entry);
202
+ void emitScrollEvent(Entry &entry, const char *which);
203
+ void writeStateOffset(const Entry &entry);
204
+
205
+ EmitterLookup lookup_;
206
+ std::unordered_map<facebook::react::Tag, Entry> entries_;
207
+ };
208
+
209
+ } // namespace basalt
@@ -0,0 +1,248 @@
1
+ #include "GtkSkiaContext.h"
2
+
3
+ #include "ImageBytes.h"
4
+ #include "PlatformServices.h"
5
+
6
+ #include <gtk/gtk.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/SkFontMgr_fontconfig.h"
15
+ #include "include/ports/SkFontScanner_FreeType.h"
16
+
17
+ #include <glog/logging.h>
18
+
19
+ #include <cstdint>
20
+ #include <memory>
21
+ #include <stdexcept>
22
+ #include <string>
23
+ #include <thread>
24
+ #include <utility>
25
+ #include <vector>
26
+
27
+ namespace basalt {
28
+ namespace {
29
+
30
+ class GtkSkiaPlatformContext : public RNSkia::RNSkPlatformContext {
31
+ public:
32
+ GtkSkiaPlatformContext(std::shared_ptr<facebook::react::CallInvoker> callInvoker,
33
+ float pixelDensity)
34
+ : RNSkia::RNSkPlatformContext(std::move(callInvoker), pixelDensity) {}
35
+
36
+ // ---------------------------------------------------------------- threading
37
+
38
+ void runOnMainThread(std::function<void()> func) override {
39
+ // basalt's own, so Skia's idea of the main thread and this host's are the
40
+ // same one. Fire and forget, which is all `postToUiThread` offers and all
41
+ // this seam promises.
42
+ postToUiThread(std::move(func));
43
+ }
44
+
45
+ // ------------------------------------------------------------------- errors
46
+
47
+ void raiseError(const std::exception &err) override {
48
+ // Logged, not fatal, as on the other two hosts: the package's Apple version
49
+ // calls RCTFatal and takes the app down with a red box, and a shader that
50
+ // fails to compile is not a reason to end a session.
51
+ LOG(ERROR) << "react-native-skia: " << err.what();
52
+ }
53
+
54
+ // ----------------------------------------------------------------- surfaces
55
+
56
+ sk_sp<SkSurface> makeOffscreenSurface(int width, int height,
57
+ bool useP3ColorSpace = false) override {
58
+ // The colour space is accepted and not honoured, as on Windows: a Display
59
+ // P3 surface is a wide-gamut answer for a host that has no colour
60
+ // management of its own yet, and silently returning sRGB is the same
61
+ // picture on a machine that is not wide-gamut.
62
+ (void)useP3ColorSpace;
63
+ // CPU. See gtk/GtkSkiaContext.h for why this stage is raster even though
64
+ // the archives carry a GL backend.
65
+ return SkSurfaces::Raster(SkImageInfo::MakeN32Premul(width, height));
66
+ }
67
+
68
+ GrDirectContext *getDirectContext() override {
69
+ // Null is a documented answer rather than a failure: Skia's own call sites
70
+ // either skip the GPU path for a raster image or say "No GPU context
71
+ // available" by name. What must not happen is inventing a context that
72
+ // draws nowhere.
73
+ return nullptr;
74
+ }
75
+
76
+ // ------------------------------------------------------------------- images
77
+
78
+ sk_sp<SkImage> makeImageFromNativeBuffer(void *) override {
79
+ throw std::runtime_error(unsupportedNativeBuffers());
80
+ }
81
+
82
+ sk_sp<SkImage> makeImageFromNativeTexture(const RNSkia::TextureInfo &, int, int,
83
+ bool) override {
84
+ throw std::runtime_error(unsupportedTextures());
85
+ }
86
+
87
+ const RNSkia::TextureInfo getTexture(sk_sp<SkImage>) override {
88
+ throw std::runtime_error(unsupportedTextures());
89
+ }
90
+
91
+ const RNSkia::TextureInfo getTexture(sk_sp<SkSurface>) override {
92
+ throw std::runtime_error(unsupportedTextures());
93
+ }
94
+
95
+ // -------------------------------------------------------------------- fonts
96
+
97
+ sk_sp<SkFontMgr> createFontMgr() override {
98
+ // Fontconfig, through Skia's own port, which is what the Linux archives are
99
+ // built with -- the GN args turn it on where the Windows build turns it
100
+ // off. Same seam, each platform's real font system: CoreText on Apple,
101
+ // DirectWrite on Windows, this here.
102
+ //
103
+ // Two arguments, which is newer than most examples of this call. m152
104
+ // split the scanner out of the font manager -- `SkFontMgr_New_FontConfig`
105
+ // used to take a config alone and now takes the thing that reads a font
106
+ // file as well, because Skia has two of those to choose between: FreeType
107
+ // and Fontations, its Rust one. FreeType, to match what the archives were
108
+ // built with.
109
+ //
110
+ // A null FcConfig means "the current one", which is fontconfig's own
111
+ // default -- the machine's /etc/fonts plus the user's. Passing one of our
112
+ // own would mean owning font discovery, which is not this host's job.
113
+ //
114
+ // Made once and shared: this builds a fontconfig configuration each time
115
+ // it is called, which is a scan of every font directory on the machine,
116
+ // and createFontMgr is called per paragraph.
117
+ static sk_sp<SkFontMgr> manager =
118
+ SkFontMgr_New_FontConfig(nullptr, SkFontScanner_Make_FreeType());
119
+ return manager;
120
+ }
121
+
122
+ std::vector<std::string> getSystemFontFamilies() override {
123
+ std::vector<std::string> families;
124
+ const sk_sp<SkFontMgr> manager = createFontMgr();
125
+ if (manager == nullptr) {
126
+ return families;
127
+ }
128
+ const int count = manager->countFamilies();
129
+ families.reserve(static_cast<size_t>(count));
130
+ for (int i = 0; i < count; i++) {
131
+ SkString name;
132
+ manager->getFamilyName(i, &name);
133
+ families.emplace_back(name.c_str());
134
+ }
135
+ return families;
136
+ }
137
+
138
+ // -------------------------------------------------------------------- media
139
+
140
+ std::shared_ptr<RNSkia::RNSkVideo> createVideo(const std::string &) override {
141
+ // The package's video is AVFoundation on Apple and MediaCodec through JNI
142
+ // on Android; neither exists here. GStreamer is the GTK answer and is its
143
+ // own piece of work.
144
+ throw std::runtime_error(
145
+ "react-native-skia: video is not implemented on this host. It needs "
146
+ "GStreamer, which nothing here wraps yet.");
147
+ }
148
+
149
+ // ------------------------------------------------------------------ loading
150
+
151
+ void performStreamOperation(
152
+ const std::string &sourceUri,
153
+ const std::function<void(std::unique_ptr<SkStreamAsset>)> &op) override {
154
+ // On a thread, because callers treat this as asynchronous and the operation
155
+ // may read from the network. Detached for the same reason the package's is:
156
+ // the continuation is the completion.
157
+ //
158
+ // Through core's own fetcher rather than a second one written here, so a
159
+ // URI means to Skia what it means to <Image>: file, data and http, resolved
160
+ // the same way in both.
161
+ std::thread([sourceUri, op]() {
162
+ std::string bytes;
163
+ std::string error;
164
+ if (!fetchImageBytes(sourceUri, &bytes, &error)) {
165
+ LOG(WARNING) << "react-native-skia: could not read " << sourceUri << ": " << error;
166
+ // An empty stream, not a null one. The continuation in
167
+ // JsiSkDataFactory::fromURI calls `stream->getLength()` without
168
+ // checking, so a null here is a segfault rather than a missing image --
169
+ // and a 404 on one asset would take the app down. Apple's
170
+ // implementation reaches the same place by accident: a failed
171
+ // `dataWithContentsOfURL:` leaves `data` nil, and nil's bytes and
172
+ // length are null and zero, so it too hands over an empty stream.
173
+ op(SkMemoryStream::Make(SkData::MakeEmpty()));
174
+ return;
175
+ }
176
+ sk_sp<SkData> data = SkData::MakeWithCopy(bytes.data(), bytes.size());
177
+ op(SkMemoryStream::Make(std::move(data)));
178
+ }).detach();
179
+ }
180
+
181
+ // ------------------------------------------------- not implemented, and why
182
+
183
+ sk_sp<SkImage> takeScreenshotFromViewTag(size_t tag) override {
184
+ // Needs a tag-to-view lookup and a render of that widget into a surface.
185
+ // The host has the first half -- the mounting manager keeps a tag registry
186
+ // -- and nothing yet asks for the second. Named rather than blank: an empty
187
+ // image here would read as a broken renderer.
188
+ throw std::runtime_error(
189
+ "react-native-skia: makeImageSnapshot of a view is not implemented on this "
190
+ "host (tag " +
191
+ std::to_string(tag) + "). Render to an offscreen surface instead.");
192
+ }
193
+
194
+ uint64_t makeNativeBuffer(sk_sp<SkImage>) override {
195
+ throw std::runtime_error(unsupportedNativeBuffers());
196
+ }
197
+
198
+ uint64_t makeTestNativeBuffer(int, int) override {
199
+ throw std::runtime_error(unsupportedNativeBuffers());
200
+ }
201
+
202
+ void releaseNativeBuffer(uint64_t) override {
203
+ // Deliberately quiet: nothing here hands out a buffer, so nothing can
204
+ // return one, and throwing from a release path would turn a leak into a
205
+ // crash.
206
+ }
207
+
208
+ private:
209
+ static std::string unsupportedNativeBuffers() {
210
+ return "react-native-skia: native buffer interchange is not implemented on this "
211
+ "host. It is used for camera and video frame sharing, which nothing here "
212
+ "provides yet.";
213
+ }
214
+
215
+ static std::string unsupportedTextures() {
216
+ return "react-native-skia: this host has no GPU context yet, so there are no "
217
+ "textures to share. See gtk/GtkSkiaContext.h.";
218
+ }
219
+ };
220
+
221
+ } // namespace
222
+
223
+ std::shared_ptr<RNSkia::RNSkPlatformContext>
224
+ makeSkiaPlatformContext(std::shared_ptr<facebook::react::CallInvoker> callInvoker) {
225
+ // The default display's scale, and one number for the whole session: Skia
226
+ // asks for a density up front and there is no window yet when the module
227
+ // installs. A window dragged to a monitor of another scale is a thing to fix
228
+ // when something notices, which is the position all three hosts take.
229
+ //
230
+ // `gdk_display_get_monitors` needs a display, and there is one by the time a
231
+ // TurboModule installs -- but not in a test binary that never opened one, so
232
+ // a missing display is 1.0 rather than a crash.
233
+ float density = 1.0f;
234
+ if (GdkDisplay *display = gdk_display_get_default()) {
235
+ if (GListModel *monitors = gdk_display_get_monitors(display)) {
236
+ if (auto *monitor = static_cast<GdkMonitor *>(g_list_model_get_item(monitors, 0))) {
237
+ const int scale = gdk_monitor_get_scale_factor(monitor);
238
+ if (scale > 0) {
239
+ density = static_cast<float>(scale);
240
+ }
241
+ g_object_unref(monitor);
242
+ }
243
+ }
244
+ }
245
+ return std::make_shared<GtkSkiaPlatformContext>(std::move(callInvoker), density);
246
+ }
247
+
248
+ } // namespace basalt
@@ -0,0 +1,62 @@
1
+ // What @shopify/react-native-skia needs from the GTK 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 and
8
+ // win32/Win32SkiaContext.h are.
9
+ //
10
+ // ## Raster, for now
11
+ //
12
+ // AppKit's delegates to the package's `MetalContext` and has a GPU context to
13
+ // hand out. This one has none yet: `getDirectContext` answers null and
14
+ // `makeOffscreenSurface` returns a CPU surface.
15
+ //
16
+ // That is a deliberate first stage rather than an oversight, and it is the
17
+ // same stage Windows is at. It is enough for the imperative API -- `Skia.Path`,
18
+ // image decoding, typefaces -- and enough for `<Canvas>`, which renders into
19
+ // one of these surfaces and reaches the screen as a texture; see
20
+ // gtk/GtkSkiaPeer.h.
21
+ //
22
+ // A GPU stage has further to go here than it looks, and further than on the
23
+ // other two. The archives are built `skia_use_gl=true skia_use_egl=true`, so
24
+ // the backend is present, and GTK4 has GtkGLArea. What is missing is the piece
25
+ // in between: Ganesh wants a `GrDirectContext` made from a current GL context,
26
+ // and GtkGLArea only makes one current inside its own render signal -- so the
27
+ // context this class hands out would have to be created and used on GTK's
28
+ // terms rather than on Skia's. That is a real design question, not a port, and
29
+ // it is why this stage is raster.
30
+ //
31
+ // ## Fonts
32
+ //
33
+ // Fontconfig, through Skia's own `SkFontMgr_New_FontConfig`, which is what the
34
+ // Linux archives are built for -- `skia_use_fontconfig=true` and
35
+ // `skia_use_system_freetype2=true` in scripts/build_skia_linux.sh. System
36
+ // freetype rather than a bundled one on purpose: a GTK host already links pango
37
+ // and cairo, which drag in freetype, and two freetypes in one process is a
38
+ // crash rather than an untidiness. The same seam the other two hosts answer
39
+ // with CoreText and DirectWrite.
40
+ //
41
+ // ## No unicode adapter here
42
+ //
43
+ // win32/Win32SkiaUnicode.cpp exists because that host links the libgrapheme
44
+ // backend while the package's JsiSkParagraphBuilder asks for
45
+ // `SkUnicodes::ICU::Make` on anything that is not Apple. This host links
46
+ // skunicode_icu, which defines that name itself -- see the note in
47
+ // cmake/Skia.cmake for why Linux can have the ICU backend and Windows cannot.
48
+
49
+ #pragma once
50
+
51
+ #include "RNSkPlatformContext.h"
52
+
53
+ #include <memory>
54
+
55
+ namespace basalt {
56
+
57
+ // The platform context this host gives Skia. One per install; see
58
+ // gtk/GtkSkiaModule.h.
59
+ std::shared_ptr<RNSkia::RNSkPlatformContext>
60
+ makeSkiaPlatformContext(std::shared_ptr<facebook::react::CallInvoker> callInvoker);
61
+
62
+ } // namespace basalt
@@ -0,0 +1,80 @@
1
+ #include "GtkSkiaModule.h"
2
+
3
+ #include "GtkSkiaContext.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
+ GtkSkiaModule::GtkSkiaModule(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
+ GtkSkiaModule::~GtkSkiaModule() { skManager().reset(); }
40
+
41
+ RNSkia::RNSkManager *GtkSkiaModule::manager() { return skManager().get(); }
42
+
43
+ facebook::jsi::Value GtkSkiaModule::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 GtkSkiaModule 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<GtkSkiaModule &>(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 the other two hosts: the
69
+ // package's JavaScript checks the return and says something useful about
70
+ // it, whereas an exception crossing the JSI boundary during module
71
+ // initialisation 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,51 @@
1
+ // `RNSkiaModule`, the TurboModule @shopify/react-native-skia asks for by name.
2
+ //
3
+ // The third of these, and the same shape as appkit/AppKitSkiaModule.h and
4
+ // win32/Win32SkiaModule.h, for the same reason: the package's own module is
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.
14
+ //
15
+ // In the gtk package rather than in core, for the reason Skia.cmake gives about
16
+ // the Apple one: a platform context is a host's business. This one reaches
17
+ // fontconfig and GTK's own display, neither of which core has any business
18
+ // knowing about.
19
+
20
+ #pragma once
21
+
22
+ #include <ReactCommon/TurboModule.h>
23
+
24
+ #include "RNSkManager.h"
25
+
26
+ #include <memory>
27
+
28
+ namespace basalt {
29
+
30
+ class GtkSkiaModule : public facebook::react::TurboModule {
31
+ public:
32
+ static constexpr const char *kModuleName = "RNSkiaModule";
33
+
34
+ explicit GtkSkiaModule(std::shared_ptr<facebook::react::CallInvoker> jsInvoker);
35
+ ~GtkSkiaModule() override;
36
+
37
+ // The manager, or null before `install` has run. A `<Canvas>` registers
38
+ // itself with it by nativeId, and the lifetime is the runtime's rather than
39
+ // any one view's, which is why it is reachable here and not owned by a view.
40
+ static RNSkia::RNSkManager *manager();
41
+
42
+ private:
43
+ // A plain function, because `MethodMetadata::invoker` is a function pointer
44
+ // rather than a std::function -- a TurboModule's methods are static dispatch.
45
+ static facebook::jsi::Value install(facebook::jsi::Runtime &runtime,
46
+ facebook::react::TurboModule &module,
47
+ const facebook::jsi::Value *args,
48
+ size_t count);
49
+ };
50
+
51
+ } // namespace basalt