basalt-win32 0.0.0-rc5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/LICENSE +21 -0
  2. package/cli/runWindows.js +35 -0
  3. package/cli/shared.js +75 -0
  4. package/native/CMakeLists.txt +398 -0
  5. package/native/tests/test_win32_accessibility.cpp +271 -0
  6. package/native/tests/test_win32_clipboard.cpp +121 -0
  7. package/native/tests/test_win32_focus.cpp +195 -0
  8. package/native/tests/test_win32_hittest.cpp +410 -0
  9. package/native/tests/test_win32_image.cpp +239 -0
  10. package/native/tests/test_win32_imageloader.cpp +208 -0
  11. package/native/tests/test_win32_input.cpp +361 -0
  12. package/native/tests/test_win32_key_events.cpp +84 -0
  13. package/native/tests/test_win32_mounting.cpp +410 -0
  14. package/native/tests/test_win32_paint.cpp +505 -0
  15. package/native/tests/test_win32_scrollview.cpp +385 -0
  16. package/native/tests/test_win32_text.cpp +258 -0
  17. package/native/tests/test_win32_textinput.cpp +609 -0
  18. package/native/tests/test_win32_titlebar.cpp +111 -0
  19. package/native/tests/test_win32_titlebar_actions.cpp +62 -0
  20. package/native/tests/test_win32_view.cpp +222 -0
  21. package/native/win32/ColorSchemeWin32.cpp +56 -0
  22. package/native/win32/ComponentRegistryWin32.cpp +124 -0
  23. package/native/win32/DirectWriteLayout.cpp +116 -0
  24. package/native/win32/DirectWriteLayout.h +36 -0
  25. package/native/win32/DirectWriteLayoutManager.cpp +124 -0
  26. package/native/win32/FontRegistryDirectWrite.cpp +117 -0
  27. package/native/win32/PlatformServicesWin32.cpp +643 -0
  28. package/native/win32/RnWin32Accessible.cpp +258 -0
  29. package/native/win32/RnWin32Accessible.h +91 -0
  30. package/native/win32/RnWin32Image.cpp +265 -0
  31. package/native/win32/RnWin32Image.h +93 -0
  32. package/native/win32/RnWin32TextLayout.cpp +310 -0
  33. package/native/win32/RnWin32TextLayout.h +160 -0
  34. package/native/win32/RnWin32View.cpp +1281 -0
  35. package/native/win32/RnWin32View.h +506 -0
  36. package/native/win32/Win32AnimationChoreographer.cpp +76 -0
  37. package/native/win32/Win32AnimationChoreographer.h +61 -0
  38. package/native/win32/Win32Clip.h +173 -0
  39. package/native/win32/Win32DropTarget.cpp +567 -0
  40. package/native/win32/Win32DropTarget.h +42 -0
  41. package/native/win32/Win32Focus.cpp +262 -0
  42. package/native/win32/Win32Focus.h +109 -0
  43. package/native/win32/Win32ImageLoader.cpp +167 -0
  44. package/native/win32/Win32ImageLoader.h +104 -0
  45. package/native/win32/Win32KeyEvents.cpp +126 -0
  46. package/native/win32/Win32KeyEvents.h +20 -0
  47. package/native/win32/Win32MenuBar.cpp +352 -0
  48. package/native/win32/Win32MenuBar.h +16 -0
  49. package/native/win32/Win32MountingManager.cpp +727 -0
  50. package/native/win32/Win32MountingManager.h +203 -0
  51. package/native/win32/Win32Packaging.cpp +161 -0
  52. package/native/win32/Win32Packaging.h +56 -0
  53. package/native/win32/Win32RunLoopObserver.cpp +48 -0
  54. package/native/win32/Win32RunLoopObserver.h +42 -0
  55. package/native/win32/Win32ScrollView.cpp +515 -0
  56. package/native/win32/Win32ScrollView.h +208 -0
  57. package/native/win32/Win32SkiaContext.cpp +221 -0
  58. package/native/win32/Win32SkiaContext.h +53 -0
  59. package/native/win32/Win32SkiaModule.cpp +80 -0
  60. package/native/win32/Win32SkiaModule.h +50 -0
  61. package/native/win32/Win32SkiaPeer.cpp +358 -0
  62. package/native/win32/Win32SkiaPeer.h +66 -0
  63. package/native/win32/Win32SkiaUnicode.cpp +42 -0
  64. package/native/win32/Win32Snapshot.cpp +201 -0
  65. package/native/win32/Win32Snapshot.h +66 -0
  66. package/native/win32/Win32Strings.h +54 -0
  67. package/native/win32/Win32TextInput.cpp +811 -0
  68. package/native/win32/Win32TextInput.h +232 -0
  69. package/native/win32/Win32TitleBar.cpp +562 -0
  70. package/native/win32/Win32TitleBar.h +146 -0
  71. package/native/win32/Win32TitleBarLayout.cpp +46 -0
  72. package/native/win32/Win32TitleBarLayout.h +58 -0
  73. package/native/win32/Win32TouchDispatcher.cpp +443 -0
  74. package/native/win32/Win32TouchDispatcher.h +153 -0
  75. package/native/win32/Win32UiThread.h +27 -0
  76. package/native/win32/Win32WindowControl.cpp +329 -0
  77. package/native/win32/Win32WindowModule.cpp +209 -0
  78. package/native/win32/Win32WindowModule.h +72 -0
  79. package/native/win32/demo_layout_win32.cpp +232 -0
  80. package/native/win32/main_win32.cpp +2481 -0
  81. package/native/win32/mount_harness_win32.cpp +316 -0
  82. package/package.json +26 -0
  83. package/react-native.config.js +22 -0
@@ -0,0 +1,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