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,727 @@
1
+ #include "Win32MountingManager.h"
2
+
3
+ #include "DirectWriteLayout.h"
4
+ #include "ImageBytes.h"
5
+ #include "PlatformServices.h"
6
+ #include "UIManagerAccess.h"
7
+
8
+ #ifdef BASALT_HAS_SKIA
9
+ #include "Win32SkiaPeer.h"
10
+ #endif
11
+
12
+ #include <react/renderer/components/image/ImageProps.h>
13
+ #include <react/renderer/components/text/ParagraphProps.h>
14
+ #include <react/renderer/components/text/ParagraphState.h>
15
+ #include <react/renderer/components/view/ViewProps.h>
16
+ #include <react/renderer/core/ConcreteState.h>
17
+ #include <react/renderer/graphics/Color.h>
18
+ #include <react/renderer/uimanager/UIManager.h>
19
+
20
+ #include <algorithm>
21
+ #include <memory>
22
+ #include <string_view>
23
+ #include <utility>
24
+
25
+ namespace basalt {
26
+
27
+ using facebook::react::ImageProps;
28
+ using facebook::react::ImageResizeMode;
29
+ using facebook::react::MountingTransaction;
30
+ using facebook::react::ParagraphState;
31
+ using facebook::react::ShadowView;
32
+ using facebook::react::SurfaceId;
33
+ using facebook::react::Tag;
34
+ using facebook::react::ViewProps;
35
+ using win32::RnImageFit;
36
+ using win32::RnWin32View;
37
+
38
+ Win32MountingManager::Win32MountingManager()
39
+ : scrollViews_([this](Tag tag) { return eventEmitterForTag(tag); }),
40
+ textInputs_([this](Tag tag) { return eventEmitterForTag(tag); }) {
41
+ // The pull past the top of a list, counted in core/PullToRefresh.h and fired
42
+ // at whichever <RefreshControl> that scroll view has. Identical on all three
43
+ // desktops, which is the point of the two of them being portable.
44
+ scrollViews_.setOverscrollTopHandler([this](Tag tag, double amount) {
45
+ if (amount <= 0.0) {
46
+ pullToRefresh().release(tag);
47
+ return;
48
+ }
49
+ if (pullToRefresh().pull(tag, amount)) {
50
+ fireRefresh(tag);
51
+ }
52
+ });
53
+ }
54
+
55
+ Win32MountingManager::~Win32MountingManager() noexcept {
56
+ // MountingWalk cannot do this from its own destructor: by the time a base
57
+ // destructor runs, the platform half that knows how to free a view is already
58
+ // gone. Its header says so, and every platform has to make this call.
59
+ releaseAllViews();
60
+ }
61
+
62
+ // ---------------------------------------------------------------------------
63
+ // IMountingManager
64
+ // ---------------------------------------------------------------------------
65
+
66
+ void Win32MountingManager::executeMount(SurfaceId surfaceId, MountingTransaction &&transaction) {
67
+ // This runs on the JS thread: Scheduler::uiManagerDidFinishTransaction queues
68
+ // the mount via RuntimeScheduler::scheduleRenderingUpdate, which drains in the
69
+ // event loop's "update the rendering" step with the jsi::Runtime live. A view
70
+ // here is not thread-affine the way a GtkWidget or an NSView is -- it is a
71
+ // plain C++ object -- but the *painting* is, and so is every HWND a later
72
+ // <TextInput> will own. So nothing here may touch a view either.
73
+ //
74
+ // Always post, never run inline even when already on the UI thread. The queue
75
+ // is FIFO, and running one transaction inline while another is already
76
+ // queued would reorder them -- so this is about ordering rather than about
77
+ // thread-safety, and it is why the condition is absent rather than merely
78
+ // unnecessary.
79
+ //
80
+ // That is this project's choice rather than everyone's, and worth being exact
81
+ // about: iOS takes the other side. RCTMountingManager::scheduleTransaction
82
+ // runs `initiateTransaction` inline when `RCTIsMainQueue()`, and only
83
+ // dispatches otherwise. It gets away with it because under Fabric the JS
84
+ // thread is not the main thread, so the fast path is reached only when
85
+ // everything is already on one thread and there is nothing to reorder against.
86
+ // GTK and AppKit both queue unconditionally here, and this matches them.
87
+ //
88
+ // None of this changed with the New Architecture. What JSI removed is the
89
+ // *bridge* -- the JSON serialisation and the asynchronous message queue
90
+ // between JavaScript and native, which is why a TurboModule call is now a
91
+ // direct C++ call and why `measure` can answer synchronously. Mounting was
92
+ // never on that path: Fabric computes layout off the UI thread, commits to
93
+ // the shadow tree, and the mutations still have to be applied where the
94
+ // platform permits. Thread affinity is USER32's rule, not the bridge's.
95
+ //
96
+ // MountingTransaction is move-only, so it travels in a shared_ptr the lambda
97
+ // can capture -- std::function requires its target to be copyable.
98
+ auto pending = std::make_shared<MountingTransaction>(std::move(transaction));
99
+ postToUiThread([this, surfaceId, pending] {
100
+ applyTransaction(surfaceId, std::move(*pending));
101
+ });
102
+ }
103
+
104
+ void Win32MountingManager::applyTransaction(SurfaceId surfaceId,
105
+ MountingTransaction &&transaction) {
106
+ // The walk itself is in core/MountingWalk.h and is shared with the other two
107
+ // desktops; what is Windows about mounting is below, in the operations it
108
+ // calls back into.
109
+ applyMutations(transaction.getMutations());
110
+
111
+ // Tell the UIManager the transaction is on screen. Anything registered as a
112
+ // mount hook -- Reanimated's is the one that matters -- is waiting for this,
113
+ // and without it an animated style is computed every frame, committed to the
114
+ // shadow tree, and never resumed, so nothing moves. ReactCxxPlatform calls it
115
+ // nowhere.
116
+ if (auto uiManager = sharedUIManager()) {
117
+ uiManager->reportMount(surfaceId);
118
+ }
119
+
120
+ // And ask the host to repaint, because on Windows nothing does that on its
121
+ // own. See setOnDidMount.
122
+ if (onDidMount_) {
123
+ onDidMount_();
124
+ }
125
+ }
126
+
127
+ void Win32MountingManager::setOnDidMount(std::function<void()> onDidMount) {
128
+ onDidMount_ = std::move(onDidMount);
129
+ }
130
+
131
+ void Win32MountingManager::dispatchCommand(const ShadowView &shadowView,
132
+ const std::string &commandName,
133
+ const folly::dynamic &args) {
134
+ // Same trip, for the same reason, and posted onto the same queue so it stays
135
+ // behind the transaction that created the view it names. A scrollTo that
136
+ // arrived before its ScrollView was mounted would find no entry and be
137
+ // dropped.
138
+ const Tag tag = shadowView.tag;
139
+ postToUiThread([this, tag, commandName, args] { applyCommand(tag, commandName, args); });
140
+ }
141
+
142
+ void Win32MountingManager::applyCommand(Tag tag,
143
+ const std::string &commandName,
144
+ const folly::dynamic &args) {
145
+ if (scrollViews_.dispatchCommand(tag, commandName, args)) {
146
+ return;
147
+ }
148
+ if (textInputs_.dispatchCommand(tag, commandName, args)) {
149
+ return;
150
+ }
151
+ // React DevTools' overlay, answered the same way on all three: see
152
+ // core/DebuggingOverlay.h.
153
+ if (applyOverlayCommand(tag, commandName, args)) {
154
+ return;
155
+ }
156
+ // Dropped silently rather than logged. Every command the other two desktops
157
+ // implement is now implemented here, so what reaches this line is a command
158
+ // for a component this platform does not claim -- which is expected rather
159
+ // than exceptional, and would otherwise be logged on every frame of a list.
160
+ }
161
+
162
+ facebook::react::ComponentRegistryFactory Win32MountingManager::getComponentRegistryFactory() {
163
+ return facebook::react::getDefaultComponentRegistryFactory();
164
+ }
165
+
166
+ bool Win32MountingManager::hasComponent(const std::string &name) {
167
+ // Only what actually mounts. Claiming more would be worse than admitting the
168
+ // gap: the registry would build shadow nodes nothing can put on screen, and
169
+ // the app would render blank rectangles instead of failing somewhere legible.
170
+ //
171
+ // Paragraph is the mountable half of <Text>; Text and RawText exist only in
172
+ // the shadow tree, folded into the Paragraph's AttributedString.
173
+ //
174
+ // This list and ComponentRegistryWin32.cpp are two statements of one fact and
175
+ // must agree.
176
+ return name == "View" || name == "RootView" || name == "Paragraph" ||
177
+ name == "ScrollView" || name == "Image" || name == "TextInput" ||
178
+ name == "ActivityIndicatorView" || name == "Switch" || name == "ModalHostView" ||
179
+ name == "PullToRefreshView" || name == "UnimplementedNativeView" ||
180
+ name == "DebuggingOverlay";
181
+ }
182
+
183
+ void Win32MountingManager::setUIManager(
184
+ std::weak_ptr<facebook::react::UIManager> uiManager) noexcept {
185
+ setSharedUIManager(std::move(uiManager));
186
+ }
187
+
188
+ // ---------------------------------------------------------------------------
189
+ // What MountingWalk asks of a platform
190
+ // ---------------------------------------------------------------------------
191
+
192
+ RnWin32View *Win32MountingManager::createView(const ShadowView &shadowView) {
193
+ return new RnWin32View(shadowView.tag);
194
+ }
195
+
196
+ RnWin32View *Win32MountingManager::createRootView(Tag tag) {
197
+ return new RnWin32View(tag);
198
+ }
199
+
200
+ void Win32MountingManager::destroyView(RnWin32View *view) {
201
+ // The one operation that is genuinely different here. On GTK the registry
202
+ // holds a g_object_ref_sink and this is an unref; under ARC it is nothing at
203
+ // all, because erasing the map entry releases the last strong reference. A
204
+ // plain C++ object has neither, so the registry's ownership is written out:
205
+ // this is where a view stops existing.
206
+ //
207
+ // Safe against a view that is still parented, because ~RnWin32View detaches
208
+ // itself -- though Fabric guarantees a Remove before every Delete, so that
209
+ // should never be the path taken.
210
+ delete view;
211
+ }
212
+
213
+ void Win32MountingManager::insertChild(RnWin32View *parent, RnWin32View *child, int index) {
214
+ parent->insertChild(child, index);
215
+ }
216
+
217
+ void Win32MountingManager::removeChild(RnWin32View *parent, RnWin32View *child) {
218
+ parent->removeChild(child);
219
+ }
220
+
221
+ void Win32MountingManager::forgetTag(Tag tag) {
222
+ scrollViews_.remove(tag);
223
+ textInputs_.remove(tag);
224
+ imageUris_.erase(tag);
225
+ switchValues_.erase(tag);
226
+ #ifdef BASALT_HAS_SKIA
227
+ // Unregisters the canvas. This is the one moment a view is known to be
228
+ // finished with, and a canvas left registered is a surface the package will
229
+ // keep rendering into for a tag nothing will ever draw again.
230
+ forgetSkiaCanvas(tag);
231
+ #endif
232
+ }
233
+
234
+ // ---------------------------------------------------------------------------
235
+ // Applying a ShadowView to a view
236
+ // ---------------------------------------------------------------------------
237
+
238
+ void Win32MountingManager::updateView(RnWin32View *view, const ShadowView &shadowView) {
239
+ applyProps(view, shadowView);
240
+ applyText(view, shadowView);
241
+ applyImage(view, shadowView);
242
+ applyAccessibility(view, shadowView);
243
+ #ifdef BASALT_HAS_SKIA
244
+ // After applyProps, which is what puts nativeID on the view -- a canvas
245
+ // registers itself by that id and cannot attach before it is there. The size
246
+ // it reads comes from the shadow view rather than from the frame
247
+ // applyLayoutMetrics is about to set, so being before that one is only
248
+ // incidental.
249
+ //
250
+ // onDidMount_ is handed over as the way to say "the screen is stale": a
251
+ // canvas renders between transactions, when nothing else will invalidate the
252
+ // window. See Win32SkiaPeer.h.
253
+ applySkiaCanvas(view, shadowView, onDidMount_);
254
+ #endif
255
+ applyLayoutMetrics(view, shadowView);
256
+ // Last: the scroll manager clamps its offset against the frame that was just
257
+ // applied, and forces the clip that applyProps may have read as `visible`.
258
+ applyScrollView(view, shadowView);
259
+ applyTextInput(view, shadowView);
260
+ // After layout: a control is drawn centred in the frame it was just given,
261
+ // and a <Modal> commits that frame's size back into its own state.
262
+ applyControls(view, shadowView);
263
+ }
264
+
265
+ void Win32MountingManager::applyProps(RnWin32View *view, const ShadowView &shadowView) {
266
+ const auto props = std::dynamic_pointer_cast<const ViewProps>(shadowView.props);
267
+ if (props == nullptr) {
268
+ return;
269
+ }
270
+
271
+ if (props->backgroundColor) {
272
+ const auto components = facebook::react::colorComponentsFromColor(props->backgroundColor);
273
+ view->setBackgroundColor(
274
+ components.red, components.green, components.blue, components.alpha, true);
275
+ } else {
276
+ // Not transparent black: a view with no background does not paint at all.
277
+ view->setBackgroundColor(0, 0, 0, 0, false);
278
+ }
279
+
280
+ view->setOpacity(props->opacity);
281
+
282
+ // overflow: 'hidden'. React Native's default is 'visible'.
283
+ view->setClipsChildren(props->getClipsContentToBounds());
284
+
285
+ // Radii depend on the frame -- percentage radii, and the clamping that stops
286
+ // opposite corners overlapping -- so they are resolved against the layout
287
+ // metrics rather than read raw.
288
+ const auto borders = props->resolveBorderMetrics(shadowView.layoutMetrics);
289
+ // All four corners, each with its own horizontal and vertical radius, in the
290
+ // order GtkMountingManager passes them and describeTree prints them. This
291
+ // used to hand over the top-left horizontal radius alone, which was right for
292
+ // a single `borderRadius` and visibly wrong for anything else.
293
+ const float radii[8] = {
294
+ static_cast<float>(borders.borderRadii.topLeft.horizontal),
295
+ static_cast<float>(borders.borderRadii.topLeft.vertical),
296
+ static_cast<float>(borders.borderRadii.topRight.horizontal),
297
+ static_cast<float>(borders.borderRadii.topRight.vertical),
298
+ static_cast<float>(borders.borderRadii.bottomRight.horizontal),
299
+ static_cast<float>(borders.borderRadii.bottomRight.vertical),
300
+ static_cast<float>(borders.borderRadii.bottomLeft.horizontal),
301
+ static_cast<float>(borders.borderRadii.bottomLeft.vertical),
302
+ };
303
+ view->setCornerRadii(radii);
304
+
305
+ // Top, right, bottom, left, as GTK takes them -- the order CSS names the
306
+ // edges in, and not the order RectangleEdges stores them. An edge without a
307
+ // colour is transparent, so it draws nothing whatever its width.
308
+ const float widths[4] = {
309
+ static_cast<float>(borders.borderWidths.top),
310
+ static_cast<float>(borders.borderWidths.right),
311
+ static_cast<float>(borders.borderWidths.bottom),
312
+ static_cast<float>(borders.borderWidths.left),
313
+ };
314
+ float colours[16] = {};
315
+ const facebook::react::SharedColor *edges[4] = {
316
+ &borders.borderColors.top,
317
+ &borders.borderColors.right,
318
+ &borders.borderColors.bottom,
319
+ &borders.borderColors.left,
320
+ };
321
+ for (int i = 0; i < 4; i++) {
322
+ if (*edges[i]) {
323
+ const auto components = facebook::react::colorComponentsFromColor(*edges[i]);
324
+ colours[i * 4 + 0] = components.red;
325
+ colours[i * 4 + 1] = components.green;
326
+ colours[i * 4 + 2] = components.blue;
327
+ colours[i * 4 + 3] = components.alpha;
328
+ }
329
+ }
330
+ view->setBorders(widths, colours);
331
+
332
+ view->setZIndex(static_cast<int>(props->zIndex.value_or(0)));
333
+
334
+ // resolveTransform folds in transformOrigin, but only when one was set: the
335
+ // default anchor is the view's centre, which is what RnWin32View::paint
336
+ // already assumes.
337
+ const auto transform = props->resolveTransform(shadowView.layoutMetrics);
338
+ if (transform == facebook::react::Transform::Identity()) {
339
+ view->setTransform(nullptr);
340
+ } else {
341
+ view->setTransform(transform.matrix.data());
342
+ }
343
+
344
+ // `backfaceVisibility: 'hidden'`, which stops the back of a card being drawn
345
+ // mirrored halfway through a flip. Whether a transform has turned away is
346
+ // core/Backface.h's to decide, so all three hide the same face.
347
+ //
348
+ // Applied after the transform, because the view has to know the matrix
349
+ // before it can say which way it is facing.
350
+ view->setHidesBackFace(props->backfaceVisibility ==
351
+ facebook::react::BackfaceVisibility::Hidden);
352
+
353
+ // Only a hidden title bar reads this, to find the drag regions an app marked
354
+ // with <TitleBar.DragRegion>.
355
+ view->setNativeId(props->nativeId);
356
+
357
+ // Hit testing only. `hitTest` reads it; nothing about painting does.
358
+ switch (props->pointerEvents) {
359
+ case facebook::react::PointerEventsMode::None:
360
+ view->setPointerEvents(win32::RnWin32View::PointerEvents::None);
361
+ break;
362
+ case facebook::react::PointerEventsMode::BoxNone:
363
+ view->setPointerEvents(win32::RnWin32View::PointerEvents::BoxNone);
364
+ break;
365
+ case facebook::react::PointerEventsMode::BoxOnly:
366
+ view->setPointerEvents(win32::RnWin32View::PointerEvents::BoxOnly);
367
+ break;
368
+ case facebook::react::PointerEventsMode::Auto:
369
+ view->setPointerEvents(win32::RnWin32View::PointerEvents::Auto);
370
+ break;
371
+ }
372
+ }
373
+
374
+ // A <Paragraph> carries its text in state, not props: ParagraphShadowNode
375
+ // resolves the whole <Text> subtree into one AttributedString and commits it as
376
+ // ParagraphState, which is why nothing here walks child shadow nodes.
377
+ void Win32MountingManager::applyText(RnWin32View *view, const ShadowView &shadowView) {
378
+ if (shadowView.componentName == nullptr ||
379
+ std::string_view(shadowView.componentName) != "Paragraph") {
380
+ return;
381
+ }
382
+
383
+ const auto state =
384
+ std::dynamic_pointer_cast<const facebook::react::ConcreteState<ParagraphState>>(
385
+ shadowView.state);
386
+ if (state == nullptr) {
387
+ return;
388
+ }
389
+
390
+ const auto &data = state->getData();
391
+ // Built through the same function the measurement seam uses, which is what
392
+ // makes the painted lines break where the measured ones did.
393
+ view->setTextLayout(
394
+ win32::buildTextLayout(data.attributedString, data.paragraphAttributes));
395
+ }
396
+
397
+ namespace {
398
+
399
+ RnImageFit toImageFit(ImageResizeMode mode) {
400
+ switch (mode) {
401
+ case ImageResizeMode::Contain:
402
+ return RnImageFit::Contain;
403
+ case ImageResizeMode::Stretch:
404
+ return RnImageFit::Stretch;
405
+ case ImageResizeMode::Center:
406
+ case ImageResizeMode::None:
407
+ return RnImageFit::Center;
408
+ case ImageResizeMode::Repeat:
409
+ return RnImageFit::Repeat;
410
+ case ImageResizeMode::Cover:
411
+ break;
412
+ }
413
+ return RnImageFit::Cover;
414
+ }
415
+
416
+ } // namespace
417
+
418
+ void Win32MountingManager::applyImage(RnWin32View *view, const ShadowView &shadowView) {
419
+ if (shadowView.componentName == nullptr ||
420
+ std::string_view(shadowView.componentName) != "Image") {
421
+ return;
422
+ }
423
+
424
+ const auto props = std::dynamic_pointer_cast<const ImageProps>(shadowView.props);
425
+ if (props == nullptr) {
426
+ return;
427
+ }
428
+
429
+ const RnImageFit fit = toImageFit(props->resizeMode);
430
+ const std::string uri =
431
+ props->sources.empty() ? std::string{} : props->sources.front().uri;
432
+
433
+ // `tintColor`, applied before the load rather than in its callback: the tint
434
+ // is a prop and the image is a loader's answer, and either can arrive first.
435
+ if (props->tintColor) {
436
+ const auto components = facebook::react::colorComponentsFromColor(*props->tintColor);
437
+ const float rgba[4] = {components.red, components.green, components.blue, components.alpha};
438
+ view->setImageTint(true, rgba);
439
+ } else {
440
+ view->setImageTint(false, nullptr);
441
+ }
442
+
443
+ // A mutation that changed only layout must not restart the load, or an
444
+ // <Image> flickers whenever its parent resizes. The fit is applied every time
445
+ // regardless, because changing it is cheap and does not touch the pixels.
446
+ const auto previous = imageUris_.find(shadowView.tag);
447
+ if (previous != imageUris_.end() && previous->second == uri) {
448
+ view->setImage(view->image(), fit);
449
+ return;
450
+ }
451
+ imageUris_[shadowView.tag] = uri;
452
+
453
+ if (uri.empty()) {
454
+ view->setImage(nullptr, fit);
455
+ return;
456
+ }
457
+
458
+ // Clear whatever was showing: the source changed, and leaving the old picture
459
+ // up while the new one loads is worse than a blank box, because it looks like
460
+ // the change did not take.
461
+ view->setImage(nullptr, fit);
462
+
463
+ // The fetch is in the shared half, which knows file, data: and http URIs --
464
+ // a URI means the same thing on every desktop -- and the loader puts both it
465
+ // and the decode on a worker thread.
466
+ //
467
+ // The completion captures the *tag*, not the view. A view can be deleted
468
+ // while its bytes are in flight, and capturing the pointer would be a
469
+ // use-after-free on a slow network; looking it up again is the arrangement
470
+ // GTK settled on for the same reason. `this` is safe to capture because the
471
+ // loader is a member and cannot outlive the manager -- and the loader itself
472
+ // guards the case where the manager goes first.
473
+ const Tag tag = shadowView.tag;
474
+ imageLoader_->load(uri, [this, tag, uri, fit](std::shared_ptr<win32::RnWin32Image> image,
475
+ const std::string &error) {
476
+ (void)error;
477
+ RnWin32View *target = viewForTag(tag);
478
+ if (target == nullptr) {
479
+ // Deleted while loading. Not an error, and not worth logging: scrolling a
480
+ // list past an image faster than it arrives does exactly this.
481
+ return;
482
+ }
483
+ // And the source may have changed *again* while this one was loading, in
484
+ // which case a later load owns the view and this result is stale.
485
+ const auto current = imageUris_.find(tag);
486
+ if (current == imageUris_.end() || current->second != uri) {
487
+ return;
488
+ }
489
+ target->setImage(std::move(image), fit);
490
+ });
491
+ }
492
+
493
+ namespace {
494
+
495
+ win32::RnAccessibleFlag toFlag(bool value) {
496
+ return value ? win32::RnAccessibleFlag::True : win32::RnAccessibleFlag::False;
497
+ }
498
+
499
+ // The role React Native effectively means for this view: what the app asked
500
+ // for, or what the component implies.
501
+ //
502
+ // A <Text> is a label and an <Image> is an image whether or not the app said
503
+ // so, which is what makes an ordinary screen navigable without every developer
504
+ // having annotated it. Both other platforms infer the same two.
505
+ std::string effectiveRole(const ShadowView &shadowView) {
506
+ if (const auto props =
507
+ std::dynamic_pointer_cast<const facebook::react::AccessibilityProps>(shadowView.props)) {
508
+ if (!props->accessibilityRole.empty()) {
509
+ return props->accessibilityRole;
510
+ }
511
+ }
512
+ if (shadowView.componentName != nullptr) {
513
+ const std::string_view name(shadowView.componentName);
514
+ if (name == "Paragraph") {
515
+ return "text";
516
+ }
517
+ if (name == "Image") {
518
+ return "image";
519
+ }
520
+ }
521
+ return {};
522
+ }
523
+
524
+ } // namespace
525
+
526
+ void Win32MountingManager::applyAccessibility(RnWin32View *view, const ShadowView &shadowView) {
527
+ const auto props =
528
+ std::dynamic_pointer_cast<const facebook::react::AccessibilityProps>(shadowView.props);
529
+ if (props == nullptr) {
530
+ return;
531
+ }
532
+
533
+ win32::RnAccessibleInfo info;
534
+ info.role = effectiveRole(shadowView);
535
+
536
+ // A label given in props wins. Falling back to a Paragraph's own text means a
537
+ // plain <Text> announces itself without the app having repeated the string in
538
+ // an accessibilityLabel, which is what the other two hosts do.
539
+ info.label = props->accessibilityLabel;
540
+ if (info.label.empty() && view->textLayout() != nullptr) {
541
+ info.label = view->textLayout()->text();
542
+ }
543
+ info.hint = props->accessibilityHint;
544
+
545
+ const auto &state = props->accessibilityState;
546
+ if (state.has_value()) {
547
+ info.state.disabled = toFlag(state->disabled);
548
+ info.state.selected = toFlag(state->selected);
549
+ info.state.busy = toFlag(state->busy);
550
+ if (state->checked != facebook::react::AccessibilityState::Unchecked ||
551
+ !info.role.empty()) {
552
+ // Tri-state on purpose: `checked` unset is not the same as false, and a
553
+ // view that never mentions being checked must not report ToggleState_Off.
554
+ // React Native's own enum has no "unset", so Unchecked on a view with no
555
+ // role is treated as "did not say".
556
+ info.state.checked = state->checked == facebook::react::AccessibilityState::Checked
557
+ ? win32::RnAccessibleFlag::True
558
+ : win32::RnAccessibleFlag::False;
559
+ }
560
+ if (state->expanded.has_value()) {
561
+ info.state.expanded = toFlag(*state->expanded);
562
+ }
563
+ }
564
+
565
+ // accessible={false} and accessibilityElementsHidden both mean "not for a
566
+ // screen reader".
567
+ info.hidden = !props->accessible ||
568
+ props->importantForAccessibility == facebook::react::ImportantForAccessibility::NoHideDescendants;
569
+
570
+ view->setAccessibleInfo(info);
571
+
572
+ // Keyboard focus. `accessible` is the signal because React Native's
573
+ // `focusable` prop never reaches this platform -- see Win32Focus.h -- and
574
+ // because it is what <Pressable> sets on everything it renders. A hidden view
575
+ // is not focusable whatever it says: Tab stopping on something nobody can see
576
+ // is worse than Tab skipping it.
577
+ // A control is focusable whether or not the app said `accessible`.
578
+ //
579
+ // React Native's `focusable` prop never reaches this platform -- ReactCommon
580
+ // parses it only into Android's and tvOS's props -- so `accessible` is the
581
+ // signal, and <Pressable> sets it. <Switch> does not: on a phone the native
582
+ // control is focusable by being a control, and React Native never had to say
583
+ // so. On a desktop a switch that Tab skips is simply broken, so being a
584
+ // control is the signal here too.
585
+ // Disabled is excluded, which is what every desktop does: Tab skips a control
586
+ // that cannot be operated rather than stopping on one that does nothing.
587
+ const basalt::ControlState control = basalt::controlStateOf(shadowView);
588
+ const bool isControl = control.kind == basalt::ControlKind::Switch && !control.disabled;
589
+ view->setFocusable((props->accessible || isControl) && !props->accessibilityElementsHidden);
590
+ }
591
+
592
+ void Win32MountingManager::applyLayoutMetrics(RnWin32View *view, const ShadowView &shadowView) {
593
+ const auto &frame = shadowView.layoutMetrics.frame;
594
+ view->setFrame(frame.origin.x, frame.origin.y, frame.size.width, frame.size.height);
595
+
596
+ // display: 'none' keeps the node in the shadow tree but takes it out of
597
+ // layout and painting -- and, here, out of hit testing too.
598
+ view->setHidden(shadowView.layoutMetrics.displayType == facebook::react::DisplayType::None);
599
+
600
+ // TODO(layout): pointScaleFactor, once a high-DPI backing store is involved.
601
+ }
602
+
603
+ void Win32MountingManager::applyScrollView(RnWin32View *view, const ShadowView &shadowView) {
604
+ if (std::string_view(shadowView.componentName) != "ScrollView") {
605
+ return;
606
+ }
607
+ scrollViews_.update(view, shadowView);
608
+ }
609
+
610
+ // ---------------------------------------------------------------------------
611
+ // The wheel
612
+ // ---------------------------------------------------------------------------
613
+
614
+ bool Win32MountingManager::scrollAt(
615
+ RnWin32View *root, double x, double y, double deltaX, double deltaY) {
616
+ return scrollViews_.scrollAt(root, x, y, deltaX, deltaY);
617
+ }
618
+
619
+ // ---------------------------------------------------------------------------
620
+ // <TextInput>
621
+ // ---------------------------------------------------------------------------
622
+
623
+
624
+ // ---------------------------------------------------------------------------
625
+ // Controls
626
+ // ---------------------------------------------------------------------------
627
+
628
+ void Win32MountingManager::applyControlPeer(RnWin32View *view, const basalt::ControlState &state) {
629
+ RnWin32View::ControlStyle style;
630
+ style.on = state.on;
631
+ style.disabled = state.disabled;
632
+ style.large = state.large;
633
+ style.hidesWhenStopped = state.hidesWhenStopped;
634
+ style.hasThumb = state.hasForeground;
635
+ style.hasTrackOn = state.hasTrackOn;
636
+ style.hasTrackOff = state.hasTrackOff;
637
+ std::copy(std::begin(state.foreground), std::end(state.foreground), std::begin(style.thumb));
638
+ std::copy(std::begin(state.trackOn), std::end(state.trackOn), std::begin(style.trackOn));
639
+ std::copy(std::begin(state.trackOff), std::end(state.trackOff), std::begin(style.trackOff));
640
+
641
+ if (state.kind == basalt::ControlKind::Switch) {
642
+ switchValues_[static_cast<Tag>(view->tag())] = state.on;
643
+ }
644
+
645
+ view->setControl(state.kind == basalt::ControlKind::Switch ? RnWin32View::Control::Switch
646
+ : RnWin32View::Control::Spinner,
647
+ style,
648
+ basalt::describeControl(state));
649
+ }
650
+
651
+ void Win32MountingManager::setHighlights(RnWin32View *view,
652
+ const std::vector<basalt::Highlight> &highlights) {
653
+ std::vector<RnWin32View::Highlight> converted;
654
+ converted.reserve(highlights.size());
655
+ for (const basalt::Highlight &highlight : highlights) {
656
+ RnWin32View::Highlight entry;
657
+ entry.x = highlight.x;
658
+ entry.y = highlight.y;
659
+ entry.width = highlight.width;
660
+ entry.height = highlight.height;
661
+ std::copy(std::begin(highlight.color), std::end(highlight.color), std::begin(entry.color));
662
+ entry.filled = highlight.filled;
663
+ converted.push_back(entry);
664
+ }
665
+ view->setHighlights(std::move(converted));
666
+ // Nothing else asks for a repaint: an overlay command arrives outside a mount
667
+ // transaction, so the window would otherwise show the highlight at whatever
668
+ // moment something else happened to invalidate it.
669
+ if (onDidMount_) {
670
+ onDidMount_();
671
+ }
672
+ }
673
+
674
+ void Win32MountingManager::pressedView(Tag tag) {
675
+ RnWin32View *view = viewForTag(tag);
676
+ if (view == nullptr || view->control() != RnWin32View::Control::Switch) {
677
+ return;
678
+ }
679
+ if (view->controlStyle().disabled) {
680
+ return;
681
+ }
682
+ const auto known = switchValues_.find(tag);
683
+ if (known == switchValues_.end()) {
684
+ return;
685
+ }
686
+
687
+ // Told, and then left alone. React Native's <Switch> is a controlled
688
+ // component: the app's `value` prop is the only thing that moves it, and a
689
+ // switch that flipped itself would show a state its props do not agree with
690
+ // -- which is exactly what an app that ignores onValueChange is supposed to
691
+ // look like. The other two hosts have to put their real widget back for the
692
+ // same reason; here there is nothing to put back, because nothing moved.
693
+ basalt::emitSwitchChange(eventEmitterForTag(tag), tag, !known->second);
694
+ }
695
+
696
+ void Win32MountingManager::applyTextInput(RnWin32View *view, const ShadowView &shadowView) {
697
+ if (std::string_view(shadowView.componentName) != "TextInput") {
698
+ return;
699
+ }
700
+ textInputs_.update(view, shadowView);
701
+ }
702
+
703
+ void Win32MountingManager::setHostWindow(HWND window) {
704
+ textInputs_.setHostWindow(window);
705
+ }
706
+
707
+ void Win32MountingManager::syncTextInputBounds(RnWin32View *root) {
708
+ textInputs_.syncBounds(root);
709
+ }
710
+
711
+ bool Win32MountingManager::handleControlCommand(WPARAM wparam, LPARAM lparam) {
712
+ return textInputs_.handleControlCommand(wparam, lparam);
713
+ }
714
+
715
+ HBRUSH Win32MountingManager::controlColor(HDC deviceContext, HWND control) {
716
+ return textInputs_.controlColor(deviceContext, control);
717
+ }
718
+
719
+ bool Win32MountingManager::typeIntoFocusedTextInput(const std::string &text) {
720
+ return textInputs_.typeIntoFocused(text);
721
+ }
722
+
723
+ bool Win32MountingManager::focusTextInputAt(RnWin32View *root, double x, double y) {
724
+ return textInputs_.focusAt(root, x, y);
725
+ }
726
+
727
+ } // namespace basalt