basalt-win32 0.0.0-rc5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/cli/runWindows.js +35 -0
- package/cli/shared.js +75 -0
- package/native/CMakeLists.txt +398 -0
- package/native/tests/test_win32_accessibility.cpp +271 -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 +410 -0
- package/native/tests/test_win32_image.cpp +239 -0
- package/native/tests/test_win32_imageloader.cpp +208 -0
- package/native/tests/test_win32_input.cpp +361 -0
- package/native/tests/test_win32_key_events.cpp +84 -0
- package/native/tests/test_win32_mounting.cpp +410 -0
- package/native/tests/test_win32_paint.cpp +505 -0
- package/native/tests/test_win32_scrollview.cpp +385 -0
- package/native/tests/test_win32_text.cpp +258 -0
- package/native/tests/test_win32_textinput.cpp +609 -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 +124 -0
- package/native/win32/DirectWriteLayout.cpp +116 -0
- package/native/win32/DirectWriteLayout.h +36 -0
- package/native/win32/DirectWriteLayoutManager.cpp +124 -0
- package/native/win32/FontRegistryDirectWrite.cpp +117 -0
- package/native/win32/PlatformServicesWin32.cpp +643 -0
- package/native/win32/RnWin32Accessible.cpp +258 -0
- package/native/win32/RnWin32Accessible.h +91 -0
- package/native/win32/RnWin32Image.cpp +265 -0
- package/native/win32/RnWin32Image.h +93 -0
- package/native/win32/RnWin32TextLayout.cpp +310 -0
- package/native/win32/RnWin32TextLayout.h +160 -0
- package/native/win32/RnWin32View.cpp +1281 -0
- package/native/win32/RnWin32View.h +506 -0
- package/native/win32/Win32AnimationChoreographer.cpp +76 -0
- package/native/win32/Win32AnimationChoreographer.h +61 -0
- package/native/win32/Win32Clip.h +173 -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 +167 -0
- package/native/win32/Win32ImageLoader.h +104 -0
- package/native/win32/Win32KeyEvents.cpp +126 -0
- package/native/win32/Win32KeyEvents.h +20 -0
- package/native/win32/Win32MenuBar.cpp +352 -0
- package/native/win32/Win32MenuBar.h +16 -0
- package/native/win32/Win32MountingManager.cpp +727 -0
- package/native/win32/Win32MountingManager.h +203 -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 +515 -0
- package/native/win32/Win32ScrollView.h +208 -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 +811 -0
- package/native/win32/Win32TextInput.h +232 -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 +443 -0
- package/native/win32/Win32TouchDispatcher.h +153 -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 +2481 -0
- package/native/win32/mount_harness_win32.cpp +316 -0
- package/package.json +26 -0
- package/react-native.config.js +22 -0
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
#include "Win32SkiaPeer.h"
|
|
2
|
+
|
|
3
|
+
#include "PlatformServices.h"
|
|
4
|
+
#include "RnWin32View.h"
|
|
5
|
+
#include "SkiaPictureViewComponent.h"
|
|
6
|
+
#include "Win32SkiaModule.h"
|
|
7
|
+
|
|
8
|
+
#include <windows.h>
|
|
9
|
+
|
|
10
|
+
#include <d2d1.h>
|
|
11
|
+
#include <wrl/client.h>
|
|
12
|
+
|
|
13
|
+
#include "include/core/SkCanvas.h"
|
|
14
|
+
#include "include/core/SkColor.h"
|
|
15
|
+
#include "include/core/SkImageInfo.h"
|
|
16
|
+
#include "include/core/SkPixmap.h"
|
|
17
|
+
#include "include/core/SkSurface.h"
|
|
18
|
+
|
|
19
|
+
#include "RNSkPictureView.h"
|
|
20
|
+
#include "RNSkView.h"
|
|
21
|
+
|
|
22
|
+
#include <glog/logging.h>
|
|
23
|
+
|
|
24
|
+
#include <algorithm>
|
|
25
|
+
#include <cmath>
|
|
26
|
+
#include <cstdint>
|
|
27
|
+
#include <functional>
|
|
28
|
+
#include <memory>
|
|
29
|
+
#include <string>
|
|
30
|
+
#include <string_view>
|
|
31
|
+
#include <unordered_map>
|
|
32
|
+
#include <utility>
|
|
33
|
+
|
|
34
|
+
namespace basalt {
|
|
35
|
+
namespace {
|
|
36
|
+
|
|
37
|
+
using Microsoft::WRL::ComPtr;
|
|
38
|
+
|
|
39
|
+
// The surface a `<Canvas>` renders into, and the thing that draws it.
|
|
40
|
+
//
|
|
41
|
+
// Two interfaces on one object because they are two views of one buffer: the
|
|
42
|
+
// package writes through `RNSkCanvasProvider` and the paint walk reads through
|
|
43
|
+
// `RnWin32Painter`, and an object between them would only mean keeping two
|
|
44
|
+
// pointers to the same pixels in step.
|
|
45
|
+
//
|
|
46
|
+
// Both halves run on the UI thread -- `renderToCanvas` arrives through the
|
|
47
|
+
// platform context's `runOnMainThread`, `draw` from WM_PAINT -- so the buffer
|
|
48
|
+
// needs no lock. That is checked rather than assumed; see renderToCanvas,
|
|
49
|
+
// which is also where the package's Metal provider checks it.
|
|
50
|
+
class Win32SkiaCanvas final : public RNSkia::RNSkCanvasProvider,
|
|
51
|
+
public win32::RnWin32Painter {
|
|
52
|
+
public:
|
|
53
|
+
Win32SkiaCanvas(std::function<void()> requestRedraw,
|
|
54
|
+
std::shared_ptr<RNSkia::RNSkPlatformContext> context,
|
|
55
|
+
std::function<void()> requestRepaint)
|
|
56
|
+
: RNSkia::RNSkCanvasProvider(std::move(requestRedraw)),
|
|
57
|
+
context_(std::move(context)),
|
|
58
|
+
requestRepaint_(std::move(requestRepaint)) {}
|
|
59
|
+
|
|
60
|
+
// Replaced on every mutation rather than kept from the first one. A canvas
|
|
61
|
+
// that mounted before the host installed its repaint callback would
|
|
62
|
+
// otherwise hold an empty function for as long as it lives, render every
|
|
63
|
+
// frame perfectly and show none of them.
|
|
64
|
+
void setRequestRepaint(std::function<void()> requestRepaint) {
|
|
65
|
+
requestRepaint_ = std::move(requestRepaint);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// --- RNSkCanvasProvider ---------------------------------------------------
|
|
69
|
+
//
|
|
70
|
+
// "Scaled", in the package's vocabulary, means pixels rather than points:
|
|
71
|
+
// the Metal provider multiplies by the pixel density in setSize and reports
|
|
72
|
+
// the product here. Same here, so that `makeImageSnapshot` -- which builds
|
|
73
|
+
// an offscreen provider out of these two numbers -- gets the resolution it
|
|
74
|
+
// would have got on any other host.
|
|
75
|
+
|
|
76
|
+
int getWidth() override { return pixelWidth_; }
|
|
77
|
+
|
|
78
|
+
int getHeight() override { return pixelHeight_; }
|
|
79
|
+
|
|
80
|
+
bool renderToCanvas(const std::function<void(SkCanvas *)> &callback) override {
|
|
81
|
+
if (!isUiThread()) {
|
|
82
|
+
// Off the UI thread, a paint may already be reading the buffer, and
|
|
83
|
+
// drawing into it now would tear. Ask for a redraw and let it arrive on
|
|
84
|
+
// the thread that owns the pixels -- the answer the package's Metal
|
|
85
|
+
// provider gives when it finds itself off the main thread, and for the
|
|
86
|
+
// same reason.
|
|
87
|
+
_requestRedraw();
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
if (surface_ == nullptr) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
callback(surface_->getCanvas());
|
|
94
|
+
// The pixels changed, so the Direct2D copy of them is stale and the window
|
|
95
|
+
// is showing the frame before this one.
|
|
96
|
+
generation_++;
|
|
97
|
+
if (requestRepaint_) {
|
|
98
|
+
requestRepaint_();
|
|
99
|
+
}
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// In points, as the package's own providers take it: the density is applied
|
|
104
|
+
// here rather than by the caller.
|
|
105
|
+
void setSize(int width, int height) {
|
|
106
|
+
const float density = context_->getPixelDensity();
|
|
107
|
+
const int pixelWidth =
|
|
108
|
+
std::max(0, static_cast<int>(std::lround(static_cast<float>(width) * density)));
|
|
109
|
+
const int pixelHeight =
|
|
110
|
+
std::max(0, static_cast<int>(std::lround(static_cast<float>(height) * density)));
|
|
111
|
+
if (pixelWidth == pixelWidth_ && pixelHeight == pixelHeight_) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
pixelWidth_ = pixelWidth;
|
|
115
|
+
pixelHeight_ = pixelHeight;
|
|
116
|
+
surface_ = nullptr;
|
|
117
|
+
// A device bitmap is made for one size; the next draw makes another.
|
|
118
|
+
releaseDeviceBitmap();
|
|
119
|
+
if (pixelWidth_ > 0 && pixelHeight_ > 0) {
|
|
120
|
+
// N32Premul is premultiplied BGRA8888 on Windows, which is also the one
|
|
121
|
+
// pixel format a Direct2D bitmap takes here. The copy in `draw` is
|
|
122
|
+
// therefore a memcpy per row rather than a conversion -- worth stating,
|
|
123
|
+
// because the day that stops being true it will show up as a quietly
|
|
124
|
+
// halved frame rate and not as a compile error.
|
|
125
|
+
surface_ = SkSurfaces::Raster(SkImageInfo::MakeN32Premul(pixelWidth_, pixelHeight_));
|
|
126
|
+
if (surface_ == nullptr) {
|
|
127
|
+
LOG(WARNING) << "react-native-skia: could not make a " << pixelWidth_ << "x"
|
|
128
|
+
<< pixelHeight_ << " surface for a <Canvas>";
|
|
129
|
+
} else {
|
|
130
|
+
// Cleared here and not only by the renderer. A raster surface is not
|
|
131
|
+
// promised zeroed pixels, and there is a window between this and the
|
|
132
|
+
// first picture -- a mount, a layout, a paint -- in which the canvas
|
|
133
|
+
// is drawn. Uninitialised memory is a striking thing to put on screen.
|
|
134
|
+
surface_->getCanvas()->clear(SK_ColorTRANSPARENT);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// The picture drawn at the old size has to be drawn again at this one, and
|
|
138
|
+
// nothing else would ask.
|
|
139
|
+
_requestRedraw();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// --- RnWin32Painter -------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
void draw(ID2D1RenderTarget *target, float boxWidth, float boxHeight) override {
|
|
145
|
+
if (surface_ == nullptr || target == nullptr || pixelWidth_ <= 0 || pixelHeight_ <= 0) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (boxWidth <= 0.0f || boxHeight <= 0.0f) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (!ensureDeviceBitmap(target)) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
// The destination is the view's box in points and the bitmap is in pixels,
|
|
155
|
+
// which is where the density applied in setSize comes back out. On a
|
|
156
|
+
// display at 1x the two are the same number and the draw is one to one.
|
|
157
|
+
target->DrawBitmap(deviceBitmap_.Get(),
|
|
158
|
+
D2D1::RectF(0.0f, 0.0f, boxWidth, boxHeight),
|
|
159
|
+
1.0f,
|
|
160
|
+
D2D1_BITMAP_INTERPOLATION_MODE_LINEAR);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private:
|
|
164
|
+
void releaseDeviceBitmap() {
|
|
165
|
+
deviceBitmap_.Reset();
|
|
166
|
+
deviceTarget_ = nullptr;
|
|
167
|
+
deviceGeneration_ = 0;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// The Direct2D copy of the surface: made once, refilled per frame.
|
|
171
|
+
//
|
|
172
|
+
// Keyed on the target the way RnWin32Image's cache is, and for the same
|
|
173
|
+
// reason -- a bitmap belongs to the target that made it, and this host draws
|
|
174
|
+
// into a second one only for the offscreen snapshot. Refilled rather than
|
|
175
|
+
// remade because `CopyFromMemory` reuses the allocation, which is the
|
|
176
|
+
// difference between a copy per frame and a copy plus an allocation.
|
|
177
|
+
bool ensureDeviceBitmap(ID2D1RenderTarget *target) {
|
|
178
|
+
if (deviceBitmap_ == nullptr || deviceTarget_ != target) {
|
|
179
|
+
deviceBitmap_.Reset();
|
|
180
|
+
deviceTarget_ = nullptr;
|
|
181
|
+
const D2D1_BITMAP_PROPERTIES properties = D2D1::BitmapProperties(
|
|
182
|
+
D2D1::PixelFormat(DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED));
|
|
183
|
+
ComPtr<ID2D1Bitmap> created;
|
|
184
|
+
if (FAILED(target->CreateBitmap(
|
|
185
|
+
D2D1::SizeU(static_cast<UINT32>(pixelWidth_), static_cast<UINT32>(pixelHeight_)),
|
|
186
|
+
properties,
|
|
187
|
+
created.GetAddressOf()))) {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
deviceBitmap_ = std::move(created);
|
|
191
|
+
deviceTarget_ = target;
|
|
192
|
+
// Nothing has been copied into it yet, whatever generation the surface
|
|
193
|
+
// happens to be on.
|
|
194
|
+
deviceGeneration_ = 0;
|
|
195
|
+
}
|
|
196
|
+
if (deviceGeneration_ == generation_) {
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
SkPixmap pixels;
|
|
200
|
+
if (!surface_->peekPixels(&pixels)) {
|
|
201
|
+
// Only a GPU-backed surface cannot be peeked, and this one is raster by
|
|
202
|
+
// construction. A line rather than a silent blank frame, for the day
|
|
203
|
+
// that changes.
|
|
204
|
+
LOG(WARNING) << "react-native-skia: a <Canvas> surface could not be read back";
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
if (FAILED(deviceBitmap_->CopyFromMemory(
|
|
208
|
+
nullptr, pixels.addr(), static_cast<UINT32>(pixels.rowBytes())))) {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
deviceGeneration_ = generation_;
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
std::shared_ptr<RNSkia::RNSkPlatformContext> context_;
|
|
216
|
+
std::function<void()> requestRepaint_;
|
|
217
|
+
|
|
218
|
+
sk_sp<SkSurface> surface_;
|
|
219
|
+
int pixelWidth_ = 0;
|
|
220
|
+
int pixelHeight_ = 0;
|
|
221
|
+
|
|
222
|
+
// Bumped by every render. The device bitmap remembers which generation it
|
|
223
|
+
// holds, so a window that repaints for some other reason -- a resize, a
|
|
224
|
+
// sibling changing -- redraws the canvas without copying its pixels again.
|
|
225
|
+
uint64_t generation_ = 1;
|
|
226
|
+
uint64_t deviceGeneration_ = 0;
|
|
227
|
+
|
|
228
|
+
ComPtr<ID2D1Bitmap> deviceBitmap_;
|
|
229
|
+
ID2D1RenderTarget *deviceTarget_ = nullptr;
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
// The package's view, holding the canvas above.
|
|
233
|
+
//
|
|
234
|
+
// `RNSkAppleView.h` is this in template form and cannot be included here: its
|
|
235
|
+
// first import is the Apple platform context, which is Objective-C++ over an
|
|
236
|
+
// RCTBridge. A `<Canvas>` needs exactly one RNSkView subclass, so this is that
|
|
237
|
+
// one rather than a template over the one.
|
|
238
|
+
class Win32SkiaPictureView final : public RNSkia::RNSkPictureView {
|
|
239
|
+
public:
|
|
240
|
+
Win32SkiaPictureView(const std::shared_ptr<RNSkia::RNSkPlatformContext> &context,
|
|
241
|
+
const std::function<void()> &requestRepaint)
|
|
242
|
+
: RNSkia::RNSkPictureView(
|
|
243
|
+
context,
|
|
244
|
+
std::make_shared<Win32SkiaCanvas>(
|
|
245
|
+
// Bound before the base class is constructed, exactly as the
|
|
246
|
+
// package's own views bind it: nothing calls it until a
|
|
247
|
+
// picture arrives, by which time this object is whole.
|
|
248
|
+
std::bind(&RNSkia::RNSkView::requestRedraw, this),
|
|
249
|
+
context,
|
|
250
|
+
requestRepaint)) {}
|
|
251
|
+
|
|
252
|
+
// `getCanvasProvider` is protected on RNSkView, which is why this is a
|
|
253
|
+
// member rather than a cast at the call site.
|
|
254
|
+
std::shared_ptr<Win32SkiaCanvas> canvas() {
|
|
255
|
+
return std::static_pointer_cast<Win32SkiaCanvas>(getCanvasProvider());
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
struct Attached {
|
|
260
|
+
std::shared_ptr<Win32SkiaPictureView> view;
|
|
261
|
+
// The id it was registered under, kept so the unregister matches: a view
|
|
262
|
+
// whose nativeID changed would otherwise leave the old registration behind,
|
|
263
|
+
// pointing at a surface nothing draws into.
|
|
264
|
+
size_t nativeId = 0;
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
std::unordered_map<facebook::react::Tag, Attached> &attached() {
|
|
268
|
+
static std::unordered_map<facebook::react::Tag, Attached> map;
|
|
269
|
+
return map;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// `<Canvas>` stringifies a counter into nativeID. Anything else on a
|
|
273
|
+
// SkiaPictureView is not ours to interpret.
|
|
274
|
+
bool parseNativeId(const std::string &text, size_t &out) {
|
|
275
|
+
if (text.empty()) {
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
try {
|
|
279
|
+
size_t consumed = 0;
|
|
280
|
+
const unsigned long long value = std::stoull(text, &consumed);
|
|
281
|
+
if (consumed != text.size()) {
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
out = static_cast<size_t>(value);
|
|
285
|
+
return true;
|
|
286
|
+
} catch (const std::exception &) {
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
} // namespace
|
|
292
|
+
|
|
293
|
+
bool applySkiaCanvas(win32::RnWin32View *view,
|
|
294
|
+
const facebook::react::ShadowView &shadowView,
|
|
295
|
+
const std::function<void()> &requestRepaint) {
|
|
296
|
+
if (view == nullptr || shadowView.componentName == nullptr ||
|
|
297
|
+
std::string_view(shadowView.componentName) !=
|
|
298
|
+
facebook::react::SkiaPictureViewComponentName) {
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const auto tag = shadowView.tag;
|
|
303
|
+
// In points, which is what the canvas wants: it applies the pixel density
|
|
304
|
+
// itself, and passing pixels would square it on a scaled display.
|
|
305
|
+
const auto &frame = shadowView.layoutMetrics.frame;
|
|
306
|
+
const int width = static_cast<int>(frame.size.width);
|
|
307
|
+
const int height = static_cast<int>(frame.size.height);
|
|
308
|
+
|
|
309
|
+
size_t nativeId = 0;
|
|
310
|
+
if (!parseNativeId(view->nativeId(), nativeId)) {
|
|
311
|
+
// Not an error: a SkiaPictureView can mount before its nativeID has been
|
|
312
|
+
// applied, and the next updateView carries it.
|
|
313
|
+
return true;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
RNSkia::RNSkManager *manager = Win32SkiaModule::manager();
|
|
317
|
+
if (manager == nullptr) {
|
|
318
|
+
LOG(WARNING) << "SkiaPictureView tag " << tag
|
|
319
|
+
<< " mounted before RNSkiaModule.install ran, so it has no canvas";
|
|
320
|
+
return true;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
auto &entry = attached()[tag];
|
|
324
|
+
if (entry.view == nullptr) {
|
|
325
|
+
entry.view =
|
|
326
|
+
std::make_shared<Win32SkiaPictureView>(manager->getPlatformContext(), requestRepaint);
|
|
327
|
+
entry.nativeId = nativeId;
|
|
328
|
+
// The view draws it; this file only keeps it alive and in step.
|
|
329
|
+
view->setPainter(entry.view->canvas());
|
|
330
|
+
manager->setSkiaView(nativeId, entry.view);
|
|
331
|
+
LOG(INFO) << "SkiaPictureView tag " << tag << " registered as Skia view " << nativeId;
|
|
332
|
+
} else if (entry.nativeId != nativeId) {
|
|
333
|
+
// The id moved. Re-register rather than leave the manager pointing the old
|
|
334
|
+
// surface at the new id's pictures.
|
|
335
|
+
manager->unregisterSkiaView(entry.nativeId);
|
|
336
|
+
entry.nativeId = nativeId;
|
|
337
|
+
manager->setSkiaView(nativeId, entry.view);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
entry.view->canvas()->setRequestRepaint(requestRepaint);
|
|
341
|
+
if (width > 0 && height > 0) {
|
|
342
|
+
entry.view->canvas()->setSize(width, height);
|
|
343
|
+
}
|
|
344
|
+
return true;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
void forgetSkiaCanvas(facebook::react::Tag tag) {
|
|
348
|
+
auto it = attached().find(tag);
|
|
349
|
+
if (it == attached().end()) {
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
if (RNSkia::RNSkManager *manager = Win32SkiaModule::manager()) {
|
|
353
|
+
manager->unregisterSkiaView(it->second.nativeId);
|
|
354
|
+
}
|
|
355
|
+
attached().erase(it);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
} // namespace basalt
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// The Win32 half of a `<Canvas>`: a raster surface drawn in the paint walk.
|
|
2
|
+
//
|
|
3
|
+
// The other two hosts give the package a layer of its own -- a CAMetalLayer on
|
|
4
|
+
// macOS, a GL surface on GTK -- and let it present straight to the compositor.
|
|
5
|
+
// This host has neither a layer tree nor a view that is a window (see the note
|
|
6
|
+
// at the top of RnWin32View.h for why), so a canvas cannot present itself. What
|
|
7
|
+
// it can do is render into memory and let the one Direct2D walk that draws
|
|
8
|
+
// everything else draw it too, at the rect the shadow tree already resolved.
|
|
9
|
+
//
|
|
10
|
+
// That is a CPU renderer, and saying so plainly: every frame is drawn by Skia
|
|
11
|
+
// into a raster surface and copied once into a Direct2D bitmap. It is the same
|
|
12
|
+
// choice Win32SkiaContext.h already made for offscreen surfaces, for the same
|
|
13
|
+
// reason -- there is no GL or Vulkan context in this host to render into yet --
|
|
14
|
+
// and it is what makes the whole thing about two hundred lines instead of a
|
|
15
|
+
// WGL bring-up. A GPU path would replace `Win32SkiaCanvas`, and nothing else
|
|
16
|
+
// here: the seam it plugs into, `RnWin32Painter`, says nothing about pixels.
|
|
17
|
+
//
|
|
18
|
+
// What the walk buys, and why this is not a worse arrangement than a layer:
|
|
19
|
+
// the canvas is clipped by `overflow: hidden`, moved by a ScrollView's offset,
|
|
20
|
+
// faded by `opacity` and turned by `transform` because the view above it did
|
|
21
|
+
// those things before calling down -- none of which a sibling layer gets for
|
|
22
|
+
// free. It also appears in the offscreen snapshot, because the snapshot is the
|
|
23
|
+
// same walk against a WIC target.
|
|
24
|
+
//
|
|
25
|
+
// What it costs: a frame is a copy, and the window repaints whole. Neither is
|
|
26
|
+
// free and both are measured in the size of the canvas rather than the size of
|
|
27
|
+
// the app, which is the part that matters for a preview pane.
|
|
28
|
+
|
|
29
|
+
#pragma once
|
|
30
|
+
|
|
31
|
+
#include <react/renderer/core/ReactPrimitives.h>
|
|
32
|
+
#include <react/renderer/mounting/ShadowView.h>
|
|
33
|
+
|
|
34
|
+
#include <functional>
|
|
35
|
+
|
|
36
|
+
namespace basalt {
|
|
37
|
+
|
|
38
|
+
namespace win32 {
|
|
39
|
+
class RnWin32View;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Gives `view` a Skia canvas if `shadowView` is a SkiaPictureView, updating its
|
|
43
|
+
// size if it already has one. False when the component is something else, so
|
|
44
|
+
// the caller can treat this as a filter rather than asking twice.
|
|
45
|
+
//
|
|
46
|
+
// `requestRepaint` is how a picture pushed from JavaScript reaches the screen.
|
|
47
|
+
// Windows repaints on demand, so rendering a frame into the surface changes
|
|
48
|
+
// nothing until something invalidates the window -- and a canvas renders
|
|
49
|
+
// between mount transactions, which is exactly when nothing else will. It is
|
|
50
|
+
// the mounting manager's own `onDidMount` callback, for want of a second way
|
|
51
|
+
// to say "the screen is stale".
|
|
52
|
+
//
|
|
53
|
+
// Safe to call before `install` has run: without a manager there is nothing to
|
|
54
|
+
// register with, so it says so in the log and leaves the view alone rather
|
|
55
|
+
// than building a surface no picture could ever reach.
|
|
56
|
+
bool applySkiaCanvas(win32::RnWin32View *view,
|
|
57
|
+
const facebook::react::ShadowView &shadowView,
|
|
58
|
+
const std::function<void()> &requestRepaint);
|
|
59
|
+
|
|
60
|
+
// Unregisters the canvas for `tag`, if it had one. Called when the mounting
|
|
61
|
+
// walk forgets a tag, which is after the view itself has been deleted -- so
|
|
62
|
+
// this is only about the manager's registration and this file's own map. The
|
|
63
|
+
// surface goes with the view, which held the last reference to it.
|
|
64
|
+
void forgetSkiaCanvas(facebook::react::Tag tag);
|
|
65
|
+
|
|
66
|
+
} // namespace basalt
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// `SkUnicodes::ICU::Make`, answered with libgrapheme.
|
|
2
|
+
//
|
|
3
|
+
// One symbol, and it exists because of a disagreement between two reasonable
|
|
4
|
+
// choices rather than a mistake by either.
|
|
5
|
+
//
|
|
6
|
+
// @shopify/react-native-skia picks its unicode backend by platform, in
|
|
7
|
+
// cpp/api/JsiSkParagraphBuilder.h:
|
|
8
|
+
//
|
|
9
|
+
// #ifdef __APPLE__
|
|
10
|
+
// unicode = SkUnicodes::Libgrapheme::Make();
|
|
11
|
+
// #else
|
|
12
|
+
// unicode = SkUnicodes::ICU::Make();
|
|
13
|
+
// #endif
|
|
14
|
+
//
|
|
15
|
+
// Windows is not __APPLE__, so it asks for ICU. This host cannot give it the
|
|
16
|
+
// ICU-backed skunicode: basalt links the operating system's own ICU -- see
|
|
17
|
+
// cmake/ThirdParty.cmake, which declines to vendor a second copy -- and Skia's
|
|
18
|
+
// ICU-backed skunicode carries a static one, so the two collide on every
|
|
19
|
+
// unsuffixed ICU symbol. The archives here are therefore built with
|
|
20
|
+
// libgrapheme, exactly as the package's own published Apple archives are.
|
|
21
|
+
//
|
|
22
|
+
// Which leaves the package calling a factory nobody defines. Defining it here
|
|
23
|
+
// is the smallest honest answer: the same backend Apple gets, under the name
|
|
24
|
+
// this platform's branch of that #ifdef asks for. The alternative is patching
|
|
25
|
+
// the package in every app that uses it, which is a worse version of this.
|
|
26
|
+
//
|
|
27
|
+
// It is not a stub. `SkUnicodes::Libgrapheme::Make()` is the real
|
|
28
|
+
// implementation, shipped in skunicode_libgrapheme.lib, and paragraph layout
|
|
29
|
+
// on this host is the same code Apple runs. What differs from an ICU build is
|
|
30
|
+
// what libgrapheme does not do -- ICU's full line-breaking and bidi tables --
|
|
31
|
+
// which is a difference Apple already lives with.
|
|
32
|
+
|
|
33
|
+
#include "modules/skunicode/include/SkUnicode_icu.h"
|
|
34
|
+
#include "modules/skunicode/include/SkUnicode_libgrapheme.h"
|
|
35
|
+
|
|
36
|
+
namespace SkUnicodes::ICU {
|
|
37
|
+
|
|
38
|
+
sk_sp<SkUnicode> Make() {
|
|
39
|
+
return SkUnicodes::Libgrapheme::Make();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
} // namespace SkUnicodes::ICU
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
#include "Win32Snapshot.h"
|
|
2
|
+
|
|
3
|
+
#include "RnWin32View.h"
|
|
4
|
+
|
|
5
|
+
#include <windows.h>
|
|
6
|
+
|
|
7
|
+
#include <d2d1.h>
|
|
8
|
+
#include <wincodec.h>
|
|
9
|
+
#include <wrl/client.h>
|
|
10
|
+
|
|
11
|
+
#include <cmath>
|
|
12
|
+
#include <string>
|
|
13
|
+
|
|
14
|
+
using Microsoft::WRL::ComPtr;
|
|
15
|
+
|
|
16
|
+
namespace basalt::win32 {
|
|
17
|
+
namespace {
|
|
18
|
+
|
|
19
|
+
// COM, initialised once per thread and deliberately never uninitialised.
|
|
20
|
+
//
|
|
21
|
+
// A snapshot can be taken from a thread that already called CoInitializeEx --
|
|
22
|
+
// the host's main thread will have -- and RPC_E_CHANGED_MODE says exactly that
|
|
23
|
+
// and is not a failure. Balancing this with CoUninitialize would tear down the
|
|
24
|
+
// apartment the caller set up, so it is left alone: nothing here owns the
|
|
25
|
+
// thread it is called on.
|
|
26
|
+
bool ensureCom() {
|
|
27
|
+
const HRESULT hr = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
|
|
28
|
+
return SUCCEEDED(hr) || hr == RPC_E_CHANGED_MODE;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
std::wstring widen(const std::string &text) {
|
|
32
|
+
if (text.empty()) {
|
|
33
|
+
return {};
|
|
34
|
+
}
|
|
35
|
+
const int needed =
|
|
36
|
+
MultiByteToWideChar(CP_UTF8, 0, text.c_str(), static_cast<int>(text.size()), nullptr, 0);
|
|
37
|
+
if (needed <= 0) {
|
|
38
|
+
return {};
|
|
39
|
+
}
|
|
40
|
+
std::wstring wide(static_cast<size_t>(needed), L'\0');
|
|
41
|
+
MultiByteToWideChar(
|
|
42
|
+
CP_UTF8, 0, text.c_str(), static_cast<int>(text.size()), wide.data(), needed);
|
|
43
|
+
return wide;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Renders the tree into a WIC bitmap. The one place the render is described, so
|
|
47
|
+
// the pixels a test reads and the pixels a PNG carries cannot come out
|
|
48
|
+
// different -- the same reason the text layout is built in one place and shared
|
|
49
|
+
// by measurement and painting.
|
|
50
|
+
bool renderToBitmap(const RnWin32View &root,
|
|
51
|
+
ComPtr<IWICImagingFactory> &wic,
|
|
52
|
+
ComPtr<IWICBitmap> &bitmap,
|
|
53
|
+
UINT &width,
|
|
54
|
+
UINT &height) {
|
|
55
|
+
const RnRect frame = root.frame();
|
|
56
|
+
width = static_cast<UINT>(std::lround(frame.width));
|
|
57
|
+
height = static_cast<UINT>(std::lround(frame.height));
|
|
58
|
+
if (width == 0 || height == 0) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (!ensureCom()) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (FAILED(CoCreateInstance(
|
|
67
|
+
CLSID_WICImagingFactory, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&wic)))) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Premultiplied BGRA, which is what Direct2D's B8G8R8A8_UNORM target expects
|
|
72
|
+
// and what the PNG encoder can take straight back.
|
|
73
|
+
if (FAILED(wic->CreateBitmap(
|
|
74
|
+
width, height, GUID_WICPixelFormat32bppPBGRA, WICBitmapCacheOnLoad, bitmap.GetAddressOf()))) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
ComPtr<ID2D1Factory> d2d;
|
|
79
|
+
if (FAILED(D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, d2d.GetAddressOf()))) {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 96 dpi explicitly, so a render is the size the frames say regardless of
|
|
84
|
+
// what the machine taking it is set to. Every coordinate in this project is
|
|
85
|
+
// in React Native's density-independent pixels, and a snapshot that silently
|
|
86
|
+
// came out at 150% on one developer's laptop would compare as a difference
|
|
87
|
+
// against every other host.
|
|
88
|
+
const D2D1_RENDER_TARGET_PROPERTIES properties = D2D1::RenderTargetProperties(
|
|
89
|
+
D2D1_RENDER_TARGET_TYPE_DEFAULT,
|
|
90
|
+
D2D1::PixelFormat(DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED),
|
|
91
|
+
96.0f,
|
|
92
|
+
96.0f);
|
|
93
|
+
|
|
94
|
+
ComPtr<ID2D1RenderTarget> target;
|
|
95
|
+
if (FAILED(d2d->CreateWicBitmapRenderTarget(bitmap.Get(), properties, target.GetAddressOf()))) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
target->BeginDraw();
|
|
100
|
+
target->Clear(D2D1::ColorF(0.0f, 0.0f, 0.0f, 0.0f));
|
|
101
|
+
target->SetTransform(D2D1::Matrix3x2F::Identity());
|
|
102
|
+
// The root paints at its own frame's origin like any other view, and a
|
|
103
|
+
// surface root's origin is (0,0), so nothing here has to compensate.
|
|
104
|
+
root.paint(target.Get());
|
|
105
|
+
return SUCCEEDED(target->EndDraw());
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
} // namespace
|
|
109
|
+
|
|
110
|
+
RnPixel RnPixels::at(unsigned x, unsigned y) const {
|
|
111
|
+
if (x >= width_ || y >= height_) {
|
|
112
|
+
return {};
|
|
113
|
+
}
|
|
114
|
+
const size_t offset = (static_cast<size_t>(y) * width_ + x) * 4;
|
|
115
|
+
if (offset + 3 >= bgra_.size()) {
|
|
116
|
+
return {};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const uint8_t blue = bgra_[offset + 0];
|
|
120
|
+
const uint8_t green = bgra_[offset + 1];
|
|
121
|
+
const uint8_t red = bgra_[offset + 2];
|
|
122
|
+
const uint8_t alpha = bgra_[offset + 3];
|
|
123
|
+
if (alpha == 0) {
|
|
124
|
+
return {};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Unpremultiplied, so a test compares against the components it set rather
|
|
128
|
+
// than against those components times an alpha it has to work out.
|
|
129
|
+
const auto straight = [alpha](uint8_t component) {
|
|
130
|
+
const int value = (static_cast<int>(component) * 255 + alpha / 2) / alpha;
|
|
131
|
+
return static_cast<uint8_t>(value > 255 ? 255 : value);
|
|
132
|
+
};
|
|
133
|
+
return RnPixel{straight(red), straight(green), straight(blue), alpha};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
RnPixels renderToPixels(const RnWin32View &root) {
|
|
137
|
+
ComPtr<IWICImagingFactory> wic;
|
|
138
|
+
ComPtr<IWICBitmap> bitmap;
|
|
139
|
+
UINT width = 0;
|
|
140
|
+
UINT height = 0;
|
|
141
|
+
if (!renderToBitmap(root, wic, bitmap, width, height)) {
|
|
142
|
+
return RnPixels{0, 0, {}};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const UINT stride = width * 4;
|
|
146
|
+
std::vector<uint8_t> buffer(static_cast<size_t>(stride) * height);
|
|
147
|
+
if (FAILED(bitmap->CopyPixels(
|
|
148
|
+
nullptr, stride, static_cast<UINT>(buffer.size()), buffer.data()))) {
|
|
149
|
+
return RnPixels{0, 0, {}};
|
|
150
|
+
}
|
|
151
|
+
return RnPixels{width, height, std::move(buffer)};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
bool writeSnapshot(const RnWin32View &root, const std::string &path) {
|
|
155
|
+
ComPtr<IWICImagingFactory> wic;
|
|
156
|
+
ComPtr<IWICBitmap> bitmap;
|
|
157
|
+
UINT width = 0;
|
|
158
|
+
UINT height = 0;
|
|
159
|
+
if (!renderToBitmap(root, wic, bitmap, width, height)) {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const std::wstring widePath = widen(path);
|
|
164
|
+
if (widePath.empty()) {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
ComPtr<IWICStream> stream;
|
|
169
|
+
if (FAILED(wic->CreateStream(stream.GetAddressOf())) ||
|
|
170
|
+
FAILED(stream->InitializeFromFilename(widePath.c_str(), GENERIC_WRITE))) {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
ComPtr<IWICBitmapEncoder> encoder;
|
|
175
|
+
if (FAILED(wic->CreateEncoder(GUID_ContainerFormatPng, nullptr, encoder.GetAddressOf())) ||
|
|
176
|
+
FAILED(encoder->Initialize(stream.Get(), WICBitmapEncoderNoCache))) {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
ComPtr<IWICBitmapFrameEncode> frameEncode;
|
|
181
|
+
if (FAILED(encoder->CreateNewFrame(frameEncode.GetAddressOf(), nullptr)) ||
|
|
182
|
+
FAILED(frameEncode->Initialize(nullptr)) || FAILED(frameEncode->SetSize(width, height))) {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// The encoder is allowed to answer with a format it prefers; PNG takes this
|
|
187
|
+
// one, and asking is the documented way rather than an optimisation.
|
|
188
|
+
WICPixelFormatGUID format = GUID_WICPixelFormat32bppPBGRA;
|
|
189
|
+
if (FAILED(frameEncode->SetPixelFormat(&format))) {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (FAILED(frameEncode->WriteSource(bitmap.Get(), nullptr)) || FAILED(frameEncode->Commit()) ||
|
|
194
|
+
FAILED(encoder->Commit())) {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
} // namespace basalt::win32
|