basalt-win32 0.0.0-stage → 0.2.0
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/runWindows.js +35 -0
- package/cli/shared.js +75 -0
- package/native/CMakeLists.txt +429 -0
- package/native/tests/test_win32_accessibility.cpp +585 -0
- package/native/tests/test_win32_animated_image.cpp +351 -0
- package/native/tests/test_win32_clipboard.cpp +121 -0
- package/native/tests/test_win32_focus.cpp +195 -0
- package/native/tests/test_win32_hittest.cpp +750 -0
- package/native/tests/test_win32_image.cpp +370 -0
- package/native/tests/test_win32_imageloader.cpp +208 -0
- package/native/tests/test_win32_input.cpp +511 -0
- package/native/tests/test_win32_key_events.cpp +84 -0
- package/native/tests/test_win32_mounting.cpp +757 -0
- package/native/tests/test_win32_paint.cpp +1799 -0
- package/native/tests/test_win32_scrollview.cpp +647 -0
- package/native/tests/test_win32_selection.cpp +215 -0
- package/native/tests/test_win32_text.cpp +1456 -0
- package/native/tests/test_win32_textinput.cpp +1594 -0
- package/native/tests/test_win32_titlebar.cpp +111 -0
- package/native/tests/test_win32_titlebar_actions.cpp +62 -0
- package/native/tests/test_win32_view.cpp +222 -0
- package/native/win32/ColorSchemeWin32.cpp +56 -0
- package/native/win32/ComponentRegistryWin32.cpp +156 -0
- package/native/win32/DirectWriteLayout.cpp +485 -0
- package/native/win32/DirectWriteLayout.h +77 -0
- package/native/win32/DirectWriteLayoutManager.cpp +154 -0
- package/native/win32/FontRegistryDirectWrite.cpp +117 -0
- package/native/win32/PlatformServicesWin32.cpp +744 -0
- package/native/win32/RnWin32Accessible.cpp +343 -0
- package/native/win32/RnWin32Accessible.h +130 -0
- package/native/win32/RnWin32Image.cpp +588 -0
- package/native/win32/RnWin32Image.h +139 -0
- package/native/win32/RnWin32TextLayout.cpp +1379 -0
- package/native/win32/RnWin32TextLayout.h +390 -0
- package/native/win32/RnWin32View.cpp +2956 -0
- package/native/win32/RnWin32View.h +926 -0
- package/native/win32/Win32AnimationChoreographer.cpp +76 -0
- package/native/win32/Win32AnimationChoreographer.h +61 -0
- package/native/win32/Win32Clip.h +209 -0
- package/native/win32/Win32Cursors.cpp +309 -0
- package/native/win32/Win32Cursors.h +121 -0
- package/native/win32/Win32DropTarget.cpp +567 -0
- package/native/win32/Win32DropTarget.h +42 -0
- package/native/win32/Win32Focus.cpp +262 -0
- package/native/win32/Win32Focus.h +109 -0
- package/native/win32/Win32ImageLoader.cpp +199 -0
- package/native/win32/Win32ImageLoader.h +133 -0
- package/native/win32/Win32InputScopes.h +92 -0
- package/native/win32/Win32KeyEvents.cpp +126 -0
- package/native/win32/Win32KeyEvents.h +20 -0
- package/native/win32/Win32MenuBar.cpp +382 -0
- package/native/win32/Win32MenuBar.h +16 -0
- package/native/win32/Win32MountingManager.cpp +1282 -0
- package/native/win32/Win32MountingManager.h +243 -0
- package/native/win32/Win32Offscreen.h +53 -0
- package/native/win32/Win32Packaging.cpp +161 -0
- package/native/win32/Win32Packaging.h +56 -0
- package/native/win32/Win32RunLoopObserver.cpp +48 -0
- package/native/win32/Win32RunLoopObserver.h +42 -0
- package/native/win32/Win32ScrollView.cpp +718 -0
- package/native/win32/Win32ScrollView.h +256 -0
- package/native/win32/Win32SkiaContext.cpp +221 -0
- package/native/win32/Win32SkiaContext.h +53 -0
- package/native/win32/Win32SkiaModule.cpp +80 -0
- package/native/win32/Win32SkiaModule.h +50 -0
- package/native/win32/Win32SkiaPeer.cpp +358 -0
- package/native/win32/Win32SkiaPeer.h +66 -0
- package/native/win32/Win32SkiaUnicode.cpp +42 -0
- package/native/win32/Win32Snapshot.cpp +201 -0
- package/native/win32/Win32Snapshot.h +66 -0
- package/native/win32/Win32Strings.h +54 -0
- package/native/win32/Win32TextInput.cpp +1296 -0
- package/native/win32/Win32TextInput.h +341 -0
- package/native/win32/Win32TitleBar.cpp +562 -0
- package/native/win32/Win32TitleBar.h +146 -0
- package/native/win32/Win32TitleBarLayout.cpp +46 -0
- package/native/win32/Win32TitleBarLayout.h +58 -0
- package/native/win32/Win32TouchDispatcher.cpp +577 -0
- package/native/win32/Win32TouchDispatcher.h +201 -0
- package/native/win32/Win32UiThread.h +27 -0
- package/native/win32/Win32WindowControl.cpp +329 -0
- package/native/win32/Win32WindowModule.cpp +209 -0
- package/native/win32/Win32WindowModule.h +72 -0
- package/native/win32/demo_layout_win32.cpp +232 -0
- package/native/win32/main_win32.cpp +2722 -0
- package/native/win32/mount_harness_win32.cpp +316 -0
- package/package.json +25 -4
- package/react-native.config.js +22 -0
- package/README.md +0 -3
|
@@ -0,0 +1,256 @@
|
|
|
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 "ScrollVisiblePosition.h"
|
|
67
|
+
#include "RnWin32View.h"
|
|
68
|
+
|
|
69
|
+
#include <react/renderer/components/scrollview/ScrollViewShadowNode.h>
|
|
70
|
+
#include <react/renderer/core/EventEmitter.h>
|
|
71
|
+
#include <react/renderer/mounting/ShadowView.h>
|
|
72
|
+
|
|
73
|
+
#include <cstdint>
|
|
74
|
+
#include <functional>
|
|
75
|
+
#include <optional>
|
|
76
|
+
#include <string>
|
|
77
|
+
#include <unordered_map>
|
|
78
|
+
#include <vector>
|
|
79
|
+
|
|
80
|
+
namespace basalt {
|
|
81
|
+
|
|
82
|
+
class Win32ScrollViewManager {
|
|
83
|
+
public:
|
|
84
|
+
// Emitters live in the mounting manager's registry, which is also what owns
|
|
85
|
+
// this, so they are reached through a lookup rather than a second copy.
|
|
86
|
+
using EmitterLookup = std::function<facebook::react::EventEmitter::Shared(facebook::react::Tag)>;
|
|
87
|
+
|
|
88
|
+
explicit Win32ScrollViewManager(EmitterLookup lookup);
|
|
89
|
+
~Win32ScrollViewManager();
|
|
90
|
+
|
|
91
|
+
Win32ScrollViewManager(const Win32ScrollViewManager &) = delete;
|
|
92
|
+
Win32ScrollViewManager &operator=(const Win32ScrollViewManager &) = delete;
|
|
93
|
+
Win32ScrollViewManager(Win32ScrollViewManager &&) = delete;
|
|
94
|
+
Win32ScrollViewManager &operator=(Win32ScrollViewManager &&) = delete;
|
|
95
|
+
|
|
96
|
+
// Called for every mutation touching a ScrollView. Registers the view the
|
|
97
|
+
// first time, and refreshes the geometry and props after.
|
|
98
|
+
void update(win32::RnWin32View *view, const facebook::react::ShadowView &shadowView);
|
|
99
|
+
|
|
100
|
+
void remove(facebook::react::Tag tag);
|
|
101
|
+
// `maintainVisibleContentPosition`, in the two halves a mounting transaction
|
|
102
|
+
// needs: the children are measured before the mutations and the offset is
|
|
103
|
+
// moved after, so content under the eye stays where it is while items are
|
|
104
|
+
// inserted above it. The arithmetic is core/ScrollVisiblePosition.h's.
|
|
105
|
+
//
|
|
106
|
+
// Public because the mounting manager is what knows when a transaction starts
|
|
107
|
+
// and ends, which is upstream's division too: `mountingTransactionWillMount`
|
|
108
|
+
// and `...DidMount` on iOS.
|
|
109
|
+
void prepareMaintainVisiblePosition();
|
|
110
|
+
void adjustForMaintainVisiblePosition();
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
// ScrollView's imperative commands: scrollTo, scrollToEnd. Returns false if
|
|
114
|
+
// the command is not one this handles.
|
|
115
|
+
bool dispatchCommand(facebook::react::Tag tag,
|
|
116
|
+
const std::string &name,
|
|
117
|
+
const folly::dynamic &args);
|
|
118
|
+
|
|
119
|
+
// The wheel, in pixels already: a notch is resolved against kWheelStepPixels
|
|
120
|
+
// by the caller, because only the message knows how many notches it was. The
|
|
121
|
+
// point is in `root`'s coordinates. Returns false when nothing under it
|
|
122
|
+
// scrolls, which is what lets the host leave the message to DefWindowProc.
|
|
123
|
+
bool scrollAt(win32::RnWin32View *root, double x, double y, double dx, double dy);
|
|
124
|
+
|
|
125
|
+
// A wheel notch carries no pixel distance of its own, so a step has to be
|
|
126
|
+
// chosen. Roughly three lines of 16pt text, which is what browsers and both
|
|
127
|
+
// other desktops settle on -- and, at 96 DPI, close to what Windows' own
|
|
128
|
+
// three-line default works out to. Public because the host resolves notches
|
|
129
|
+
// into pixels before calling, and the two must use one number.
|
|
130
|
+
//
|
|
131
|
+
// Deliberately not `SPI_GETWHEELSCROLLLINES`. Honouring it would be the more
|
|
132
|
+
// native thing and would make one notch move a different distance here than
|
|
133
|
+
// on the other two desktops, which is the trade this project keeps making the
|
|
134
|
+
// other way: matching the other platform matters more than matching the
|
|
135
|
+
// toolkit's own default.
|
|
136
|
+
static constexpr double kWheelStepPixels = 53.0;
|
|
137
|
+
|
|
138
|
+
// The pull past the top of the list, which is what a <RefreshControl> is
|
|
139
|
+
// waiting for. Called with how far past the top this scroll asked to go, or
|
|
140
|
+
// with zero when the gesture went the other way or the view actually moved --
|
|
141
|
+
// which re-arms it. See core/PullToRefresh.h for why a desktop has to count
|
|
142
|
+
// this rather than measure a rubber band.
|
|
143
|
+
void setOverscrollTopHandler(std::function<void(facebook::react::Tag, double)> handler) {
|
|
144
|
+
overscrollTop_ = std::move(handler);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private:
|
|
148
|
+
std::function<void(facebook::react::Tag, double)> overscrollTop_;
|
|
149
|
+
|
|
150
|
+
struct Entry {
|
|
151
|
+
win32::RnWin32View *view{nullptr};
|
|
152
|
+
facebook::react::Tag tag{0};
|
|
153
|
+
|
|
154
|
+
// An animated `scrollTo`. Windows models no fling -- a wheel supplies no
|
|
155
|
+
// velocity -- but an animated scroll is given its target, so the argument
|
|
156
|
+
// that rules out momentum does not rule this out. Driven by a thread timer,
|
|
157
|
+
// which needs no window: the scroll manager has a tag, not an HWND.
|
|
158
|
+
ScrollSnapConfig snap;
|
|
159
|
+
ScrollAnimation animation;
|
|
160
|
+
UINT_PTR animationTimer{0};
|
|
161
|
+
unsigned long long animationLastMillis{0};
|
|
162
|
+
|
|
163
|
+
std::shared_ptr<const facebook::react::ScrollViewShadowNode::ConcreteState> state;
|
|
164
|
+
|
|
165
|
+
facebook::react::Size contentSize{};
|
|
166
|
+
facebook::react::Size containerSize{};
|
|
167
|
+
// `contentInset` changes how far the view scrolls;
|
|
168
|
+
// `scrollIndicatorInsets` changes only where the bar is drawn. See
|
|
169
|
+
// core/ScrollBounds.h.
|
|
170
|
+
facebook::react::EdgeInsets contentInset{};
|
|
171
|
+
facebook::react::EdgeInsets indicatorInset{};
|
|
172
|
+
|
|
173
|
+
bool scrollEnabled{true};
|
|
174
|
+
// `showsVerticalScrollIndicator` / `showsHorizontalScrollIndicator`. Both
|
|
175
|
+
// default to true, as React Native's own props do.
|
|
176
|
+
bool showsVerticalIndicator{true};
|
|
177
|
+
bool showsHorizontalIndicator{true};
|
|
178
|
+
// Milliseconds, as React Native's prop is. Zero means every scroll.
|
|
179
|
+
double eventThrottleMs{0};
|
|
180
|
+
|
|
181
|
+
double offsetX{0};
|
|
182
|
+
double offsetY{0};
|
|
183
|
+
|
|
184
|
+
// `maintainVisibleContentPosition`, as React Native hands it over: a
|
|
185
|
+
// minimum index and an optional threshold, or nothing at all when the list
|
|
186
|
+
// never asked.
|
|
187
|
+
std::optional<facebook::react::ScrollViewMaintainVisibleContentPosition> maintainVisible{};
|
|
188
|
+
// The child being held still across the current transaction, measured by
|
|
189
|
+
// `prepareMaintainVisiblePosition` and used by the adjust that follows.
|
|
190
|
+
ScrollPinnedChild pinned{};
|
|
191
|
+
// `indicatorStyle`: which colour the overlay thumb is drawn in. Held as
|
|
192
|
+
// core's enum rather than React Native's, the view layer below knowing
|
|
193
|
+
// nothing about either.
|
|
194
|
+
ScrollIndicatorStyle indicatorStyle{ScrollIndicatorStyle::Default};
|
|
195
|
+
|
|
196
|
+
// `centerContent`: whether content smaller than the container is centred in
|
|
197
|
+
// it. Acted on through the content inset, which is how iOS does it too --
|
|
198
|
+
// see `update`.
|
|
199
|
+
bool centerContent{false};
|
|
200
|
+
|
|
201
|
+
// The last `contentOffset` prop seen, and nothing when none has been.
|
|
202
|
+
//
|
|
203
|
+
// The *initial* value of that prop arrives through the state rather than
|
|
204
|
+
// here -- `ScrollViewShadowNode::initialStateData` seeds the state with it
|
|
205
|
+
// -- and is adopted below. What this is for is every later change, which
|
|
206
|
+
// arrives as an ordinary prop update and is applied when it *changes*,
|
|
207
|
+
// never merely when it differs from where the list is: an app that sets the
|
|
208
|
+
// prop once and re-renders for an unrelated reason must not have the list
|
|
209
|
+
// dragged back under the person reading it. The same rule a `<TextInput>`'s
|
|
210
|
+
// `text` is under, for the same reason.
|
|
211
|
+
std::optional<facebook::react::Point> lastPropContentOffset{};
|
|
212
|
+
double lastEmitMs{0};
|
|
213
|
+
|
|
214
|
+
bool dragging{false};
|
|
215
|
+
// Bumped on every wheel. An idle timer that wakes and finds a different
|
|
216
|
+
// value knows another notch arrived after it was scheduled, and leaves the
|
|
217
|
+
// drag alone.
|
|
218
|
+
std::uint64_t wheelGeneration{0};
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
bool scrollEntry(Entry &entry, double dx, double dy);
|
|
222
|
+
void endWheelDrag(facebook::react::Tag tag, std::uint64_t generation);
|
|
223
|
+
|
|
224
|
+
// The rows of the content view, on whichever axis scrolls, for
|
|
225
|
+
// `maintainVisibleContentPosition`. The one part of that prop a toolkit
|
|
226
|
+
// knows: core decides what to do with the boxes.
|
|
227
|
+
std::vector<ScrollChildBox> contentChildBoxes(const Entry &entry) const;
|
|
228
|
+
|
|
229
|
+
void applyOffset(Entry &entry, double x, double y, bool emitEvent);
|
|
230
|
+
// Recomputes both overlay scrollbars and hands them to the view to draw.
|
|
231
|
+
// Called wherever the offset, the content size or the container size can
|
|
232
|
+
// have changed, which is more places than the offset alone.
|
|
233
|
+
void updateIndicators(const Entry &entry);
|
|
234
|
+
void scrollTowards(Entry &entry, double x, double y, bool animated);
|
|
235
|
+
bool settleOnSnapPoint(Entry &entry, double velocityY);
|
|
236
|
+
void stopAnimation(Entry &entry);
|
|
237
|
+
void advanceAnimation(facebook::react::Tag tag, double seconds);
|
|
238
|
+
|
|
239
|
+
// The timer procedure cannot carry a pointer, so it looks the animation up by
|
|
240
|
+
// timer id. Static because `SetTimer` calls a plain function.
|
|
241
|
+
static void CALLBACK onAnimationTimer(HWND hwnd, UINT message, UINT_PTR id, DWORD now);
|
|
242
|
+
void emitScrollEvent(Entry &entry, const char *which);
|
|
243
|
+
void writeStateOffset(const Entry &entry);
|
|
244
|
+
|
|
245
|
+
EmitterLookup lookup_;
|
|
246
|
+
std::unordered_map<facebook::react::Tag, Entry> entries_;
|
|
247
|
+
|
|
248
|
+
// Cleared by the destructor, and held by every idle timer in flight. A timer
|
|
249
|
+
// that outlives this object -- a surface torn down inside the 150ms after the
|
|
250
|
+
// last notch -- finds the flag false and does nothing, instead of calling
|
|
251
|
+
// through a dangling `this`. The same arrangement Win32ImageLoader uses for
|
|
252
|
+
// the same reason, which is that `postDelayed` has no cancel.
|
|
253
|
+
std::shared_ptr<bool> alive_ = std::make_shared<bool>(true);
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
} // 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
|