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.
- package/LICENSE +21 -0
- package/cli/runLinux.js +29 -0
- package/cli/shared.js +75 -0
- package/native/CMakeLists.txt +362 -0
- package/native/gtk/ColorSchemeGtk.cpp +58 -0
- package/native/gtk/ComponentRegistryGtk.cpp +101 -0
- package/native/gtk/FontRegistryFontconfig.cpp +127 -0
- package/native/gtk/GtkAnimationChoreographer.cpp +73 -0
- package/native/gtk/GtkAnimationChoreographer.h +49 -0
- package/native/gtk/GtkDropTarget.cpp +269 -0
- package/native/gtk/GtkDropTarget.h +40 -0
- package/native/gtk/GtkFocus.cpp +296 -0
- package/native/gtk/GtkFocus.h +113 -0
- package/native/gtk/GtkImageLoader.cpp +128 -0
- package/native/gtk/GtkImageLoader.h +87 -0
- package/native/gtk/GtkKeyEvents.cpp +91 -0
- package/native/gtk/GtkKeyEvents.h +21 -0
- package/native/gtk/GtkMenuBar.cpp +45 -0
- package/native/gtk/GtkMountingManager.cpp +1027 -0
- package/native/gtk/GtkMountingManager.h +146 -0
- package/native/gtk/GtkRunLoopObserver.cpp +77 -0
- package/native/gtk/GtkRunLoopObserver.h +41 -0
- package/native/gtk/GtkScrollView.cpp +626 -0
- package/native/gtk/GtkScrollView.h +209 -0
- package/native/gtk/GtkSkiaContext.cpp +248 -0
- package/native/gtk/GtkSkiaContext.h +62 -0
- package/native/gtk/GtkSkiaModule.cpp +80 -0
- package/native/gtk/GtkSkiaModule.h +51 -0
- package/native/gtk/GtkSkiaPeer.cpp +285 -0
- package/native/gtk/GtkSkiaPeer.h +63 -0
- package/native/gtk/GtkTextInput.cpp +428 -0
- package/native/gtk/GtkTextInput.h +123 -0
- package/native/gtk/GtkTextPeer.cpp +425 -0
- package/native/gtk/GtkTextPeer.h +87 -0
- package/native/gtk/GtkTitleBar.cpp +273 -0
- package/native/gtk/GtkTitleBar.h +96 -0
- package/native/gtk/GtkTitleBarLayout.cpp +45 -0
- package/native/gtk/GtkTitleBarLayout.h +36 -0
- package/native/gtk/GtkTouchDispatcher.cpp +671 -0
- package/native/gtk/GtkTouchDispatcher.h +139 -0
- package/native/gtk/GtkWindowControl.cpp +223 -0
- package/native/gtk/GtkWindowModule.cpp +194 -0
- package/native/gtk/GtkWindowModule.h +62 -0
- package/native/gtk/PangoTextLayout.cpp +295 -0
- package/native/gtk/PangoTextLayout.h +54 -0
- package/native/gtk/PangoTextLayoutManager.cpp +140 -0
- package/native/gtk/PlatformServicesGtk.cpp +583 -0
- package/native/gtk/RnView.cpp +1407 -0
- package/native/gtk/RnView.h +339 -0
- package/native/gtk/demo_layout_gtk.cpp +57 -0
- package/native/gtk/main_gtk.cpp +1981 -0
- package/native/gtk/mount_harness_gtk.cpp +143 -0
- package/native/tests/TestMainGtk.cpp +25 -0
- package/native/tests/notification_stub.cpp +103 -0
- package/native/tests/test_accessibility.cpp +251 -0
- package/native/tests/test_focus.cpp +194 -0
- package/native/tests/test_hittest.cpp +242 -0
- package/native/tests/test_image.cpp +135 -0
- package/native/tests/test_key_events_gtk.cpp +112 -0
- package/native/tests/test_mounting.cpp +415 -0
- package/native/tests/test_scrollview.cpp +258 -0
- package/native/tests/test_skia_texture.cpp +120 -0
- package/native/tests/test_text.cpp +147 -0
- package/native/tests/test_textinput.cpp +598 -0
- package/native/tests/test_titlebar.cpp +137 -0
- package/native/tests/test_titlebar_drag.cpp +138 -0
- package/native/tests/test_version.cpp +49 -0
- package/native/tests/test_view.cpp +162 -0
- package/native/tests/test_viewprops.cpp +254 -0
- package/package.json +26 -0
- 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
|