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,262 @@
1
+ #include "Win32Focus.h"
2
+
3
+ #include "Win32KeyEvents.h"
4
+
5
+ #include <algorithm>
6
+ #include <optional>
7
+ #include <vector>
8
+
9
+ #include <folly/dynamic.h>
10
+
11
+ #include <react/renderer/components/view/ViewEventEmitter.h>
12
+ #include <react/renderer/core/EventEmitter.h>
13
+
14
+ #include <windows.h>
15
+
16
+ namespace basalt {
17
+
18
+ using facebook::react::RawEvent;
19
+ using facebook::react::Tag;
20
+ using win32::RnWin32View;
21
+
22
+ namespace {
23
+
24
+ // Everything Tab should stop on, in tree order.
25
+ //
26
+ // Tree order rather than paint order: zIndex changes what is on top and must
27
+ // not change what Tab reaches next, which is what both other hosts' chains do.
28
+ void collectInto(RnWin32View *view, std::vector<RnWin32View *> &stops) {
29
+ if (view == nullptr || view->hidden()) {
30
+ return;
31
+ }
32
+ if (view->editablePeer() != nullptr || view->focusable()) {
33
+ stops.push_back(view);
34
+ }
35
+ for (RnWin32View *child : view->children()) {
36
+ collectInto(child, stops);
37
+ }
38
+ }
39
+
40
+ } // namespace
41
+
42
+ Win32FocusManager::Win32FocusManager(Win32MountingManager *mountingManager,
43
+ RnWin32View *surfaceRoot)
44
+ : mountingManager_(mountingManager), surfaceRoot_(surfaceRoot) {}
45
+
46
+ void Win32FocusManager::setSurfaceRoot(RnWin32View *surfaceRoot) {
47
+ surfaceRoot_ = surfaceRoot;
48
+ // Every tag in the old tree is about to stop existing, so the focus goes with
49
+ // it -- without an event, because the emitter is going too.
50
+ focusedTag_ = 0;
51
+ }
52
+
53
+ std::vector<RnWin32View *> Win32FocusManager::collectStops() const {
54
+ std::vector<RnWin32View *> stops;
55
+ collectInto(surfaceRoot_, stops);
56
+ return stops;
57
+ }
58
+
59
+ void Win32FocusManager::setFocusedView(RnWin32View *view) {
60
+ const Tag tag = view == nullptr ? 0 : static_cast<Tag>(view->tag());
61
+ if (tag == focusedTag_) {
62
+ return;
63
+ }
64
+ const Tag previous = focusedTag_;
65
+ if (previous != 0) {
66
+ if (RnWin32View *old = mountingManager_->viewForTag(previous)) {
67
+ old->setShowsFocusRing(false);
68
+ }
69
+ }
70
+ focusedTag_ = tag;
71
+ if (previous != 0) {
72
+ emitFocus(previous, false);
73
+ }
74
+ if (view != nullptr) {
75
+ view->setShowsFocusRing(true);
76
+ emitFocus(tag, true);
77
+ }
78
+ }
79
+
80
+ void Win32FocusManager::emitFocus(Tag tag, bool focused) {
81
+ const auto emitter = std::dynamic_pointer_cast<const facebook::react::ViewEventEmitter>(
82
+ mountingManager_->eventEmitterForTag(tag));
83
+ if (emitter == nullptr) {
84
+ return;
85
+ }
86
+ // `topFocus` and `topBlur`, which React Native registers as bubbling events
87
+ // with an empty payload. BaseViewEventEmitter has both, so nothing here has
88
+ // to know the names.
89
+ if (focused) {
90
+ emitter->onFocus();
91
+ } else {
92
+ emitter->onBlur();
93
+ }
94
+ }
95
+
96
+ void Win32FocusManager::textInputTookFocus() {
97
+ // A field's peer is a real window and holds real Win32 focus; a view's ring
98
+ // is this project's own idea of focus. Only one of the two can be true, and
99
+ // the peer reports its own onFocus, so this just gives up the ring.
100
+ setFocusedView(nullptr);
101
+ }
102
+
103
+ bool Win32FocusManager::moveFocus(bool forward) {
104
+ const std::vector<RnWin32View *> stops = collectStops();
105
+ if (stops.empty()) {
106
+ return false;
107
+ }
108
+
109
+ // Where focus is now. A <TextInput> holds it through its peer and reports no
110
+ // tag, so both kinds of stop have to be recognised.
111
+ const HWND focusedWindow = GetFocus();
112
+ std::size_t index = stops.size();
113
+ for (std::size_t i = 0; i < stops.size(); i++) {
114
+ const bool isPeer =
115
+ stops[i]->editablePeer() != nullptr && stops[i]->editablePeer() == focusedWindow;
116
+ const bool isView = focusedTag_ != 0 && static_cast<Tag>(stops[i]->tag()) == focusedTag_;
117
+ if (isPeer || isView) {
118
+ index = i;
119
+ break;
120
+ }
121
+ }
122
+
123
+ std::size_t next = 0;
124
+ if (index == stops.size()) {
125
+ next = forward ? 0 : stops.size() - 1;
126
+ } else if (forward) {
127
+ next = (index + 1) % stops.size();
128
+ } else {
129
+ next = (index + stops.size() - 1) % stops.size();
130
+ }
131
+
132
+ RnWin32View *target = stops[next];
133
+ if (HWND peer = target->editablePeer()) {
134
+ // Real Win32 focus for a real window. The ring goes out first: the peer
135
+ // draws its own focus, and two of them at once would say focus is in two
136
+ // places.
137
+ setFocusedView(nullptr);
138
+ SetFocus(peer);
139
+ return true;
140
+ }
141
+
142
+ // Taking focus away from a field is the other half of the same rule, and
143
+ // Windows will not do it on its own: the peer keeps focus until something
144
+ // else asks for it, and nothing here is a window that can.
145
+ if (focusedWindow != nullptr && focusedWindow != GetAncestor(focusedWindow, GA_ROOT)) {
146
+ SetFocus(GetAncestor(focusedWindow, GA_ROOT));
147
+ }
148
+ setFocusedView(target);
149
+ return true;
150
+ }
151
+
152
+ bool Win32FocusManager::activateFocused() {
153
+ if (focusedTag_ == 0) {
154
+ return false;
155
+ }
156
+ // True whether or not anything is listening: the key belongs to the focused
157
+ // view either way, and letting it travel on because a view between a Remove
158
+ // and its Delete has no emitter would deliver it somewhere else.
159
+ // A <Switch> is toggled from here too. It listens for no click -- React
160
+ // Native's is a controlled component driven by its own native control -- so
161
+ // without this a switch that Tab reaches is one Enter cannot work. The same
162
+ // call the touch path makes, and a no-op for every view that is not a switch.
163
+ mountingManager_->pressedView(focusedTag_);
164
+
165
+ const auto emitter = mountingManager_->eventEmitterForTag(focusedTag_);
166
+ if (emitter == nullptr) {
167
+ return true;
168
+ }
169
+ // `topClick` with an empty payload, which is exactly what React Native for
170
+ // Android sends from a focusable view's OnClickListener. It cannot go through
171
+ // TouchEventEmitter::onClick: that carries a PointerEvent, and Pressability
172
+ // ignores a click with a `pointerType` on it so that a real click does not
173
+ // fire onPress twice. See the header.
174
+ emitter->dispatchEvent("click", folly::dynamic::object(), RawEvent::Category::Discrete);
175
+ return true;
176
+ }
177
+
178
+ namespace {
179
+
180
+ // The ancestors of `target`, innermost first.
181
+ //
182
+ // Downwards from the root, because a RnWin32View has no parent pointer -- the
183
+ // same reason win32/Win32DropTarget.cpp walks down to find a drop target's
184
+ // ancestry. Two near-identical walks in two files is a smell; they stay apart
185
+ // because that one collects views from a view and this one collects tags from a
186
+ // tag, and merging them would mean a shared header for seventeen lines. A third
187
+ // caller should merge all three.
188
+ bool tagPathTo(win32::RnWin32View *view,
189
+ facebook::react::Tag target,
190
+ std::vector<facebook::react::Tag> &path) {
191
+ if (view == nullptr) {
192
+ return false;
193
+ }
194
+ path.push_back(static_cast<facebook::react::Tag>(view->tag()));
195
+ if (static_cast<facebook::react::Tag>(view->tag()) == target) {
196
+ return true;
197
+ }
198
+ for (win32::RnWin32View *child : view->children()) {
199
+ if (tagPathTo(child, target, path)) {
200
+ return true;
201
+ }
202
+ }
203
+ path.pop_back();
204
+ return false;
205
+ }
206
+
207
+ } // namespace
208
+
209
+ bool Win32FocusManager::handleDeclaredKey(unsigned int virtualKey) {
210
+ // Almost every app claims nothing and presses plenty.
211
+ if (handledKeyViewCount() == 0) {
212
+ return false;
213
+ }
214
+ const std::optional<KeyCombination> pressed = keyCombinationFrom(virtualKey);
215
+ if (!pressed.has_value()) {
216
+ return false;
217
+ }
218
+ return deliverTestKey(*pressed);
219
+ }
220
+
221
+ bool Win32FocusManager::deliverTestKey(const KeyCombination &pressed) {
222
+ // Root-first from the walk, so reversed to put the focused view first and let
223
+ // the innermost claim win -- the order core/KeyEvents.h expects.
224
+ std::vector<facebook::react::Tag> path;
225
+ if (focusedTag_ != 0) {
226
+ tagPathTo(surfaceRoot_, focusedTag_, path);
227
+ std::reverse(path.begin(), path.end());
228
+ } else if (surfaceRoot_ != nullptr) {
229
+ // Nothing focused. The root is still a candidate: a window-level shortcut
230
+ // holder is the common case and would otherwise never be asked.
231
+ path.push_back(static_cast<facebook::react::Tag>(surfaceRoot_->tag()));
232
+ }
233
+
234
+ std::optional<facebook::react::Tag> handler = handledBy(path, pressed);
235
+ if (!handler.has_value() && focusedTag_ == 0) {
236
+ // See the AppKit host and core/KeyEvents.h.
237
+ handler = handledByAny(pressed);
238
+ }
239
+ if (!handler.has_value()) {
240
+ return false;
241
+ }
242
+ reportKey(*handler, pressed);
243
+ return true;
244
+ }
245
+
246
+ bool Win32FocusManager::handleKeyDown(unsigned int virtualKey) {
247
+ switch (virtualKey) {
248
+ case VK_TAB:
249
+ // Shift-Tab goes backwards. GetKeyState rather than a message parameter,
250
+ // because WM_KEYDOWN carries no modifier state of its own.
251
+ return moveFocus((GetKeyState(VK_SHIFT) & 0x8000) == 0);
252
+ case VK_RETURN:
253
+ case VK_SPACE:
254
+ // Only when something is focused. Otherwise the key goes on, so a window
255
+ // with no focus does not swallow it.
256
+ return activateFocused();
257
+ default:
258
+ return false;
259
+ }
260
+ }
261
+
262
+ } // namespace basalt
@@ -0,0 +1,109 @@
1
+ // Keyboard focus, for everything that is not a <TextInput>.
2
+ //
3
+ // Deliberately the same shape as basalt-gtk's GtkFocusManager and
4
+ // the AppKit one, down to what makes a view focusable and what activating one
5
+ // dispatches, because none of that is about the toolkit: if the three diverge,
6
+ // an app's buttons will be reachable from the keyboard on some desktops and not
7
+ // others.
8
+ //
9
+ // ## What makes a view focusable
10
+ //
11
+ // `accessible`. React Native has a `focusable` prop and it does not reach this
12
+ // platform: ReactCommon parses it only into Android's and tvOS's
13
+ // `HostPlatformViewProps`, and the C++ host's is a bare alias of
14
+ // `BaseViewProps`. So the signal has to be something that does arrive, and
15
+ // `accessible` is both the nearest true thing and the right one -- it is what
16
+ // `<Pressable>` sets on everything it renders, and what a screen reader already
17
+ // stops on.
18
+ //
19
+ // ## What activating one does
20
+ //
21
+ // Dispatches `topClick` with an empty payload, which is what React Native for
22
+ // Android does from `ReactViewManager.setFocusable`. Pressability listens for
23
+ // it and calls `onPress`, but only when the payload has no `pointerType` -- a
24
+ // click that came from a pointer would fire `onPress` twice. So this cannot go
25
+ // through `TouchEventEmitter::onClick`, which always carries one.
26
+ //
27
+ // ## The chain, all of which is here
28
+ //
29
+ // The other two desktops have something to start from. GTK hands over its focus
30
+ // chain outright; AppKit has a key-view loop that turned out to be unusable for
31
+ // a window built without a nib, but at least has a first responder to ask about.
32
+ // Here a React Native view is not a window at all -- it is a C++ object painted
33
+ // with Direct2D -- so there is nothing for Windows to focus and nothing to ask.
34
+ // The order, the current stop, the ring and the key handling are all this file's.
35
+ //
36
+ // The exception is a <TextInput>, whose peer *is* a real child window. Focus
37
+ // there is Win32 focus, moved with SetFocus and reported by EN_SETFOCUS, so a
38
+ // field is a stop in the same order as the buttons around it and the two kinds
39
+ // of focus have to be kept from being held at once.
40
+
41
+ #pragma once
42
+
43
+ #include "KeyEvents.h"
44
+
45
+ #include "RnWin32View.h"
46
+ #include "Win32MountingManager.h"
47
+
48
+ #include <vector>
49
+
50
+ namespace basalt {
51
+
52
+ class Win32FocusManager {
53
+ public:
54
+ Win32FocusManager(Win32MountingManager *mountingManager, win32::RnWin32View *surfaceRoot);
55
+
56
+ Win32FocusManager(const Win32FocusManager &) = delete;
57
+ Win32FocusManager &operator=(const Win32FocusManager &) = delete;
58
+ Win32FocusManager(Win32FocusManager &&) = delete;
59
+ Win32FocusManager &operator=(Win32FocusManager &&) = delete;
60
+
61
+ // The surface root can be replaced after a surface restart; every tag in the
62
+ // old tree goes with it.
63
+ void setSurfaceRoot(win32::RnWin32View *surfaceRoot);
64
+
65
+ // Moves focus to the next or previous stop, wrapping, and reports whether
66
+ // anything took it.
67
+ bool moveFocus(bool forward);
68
+
69
+ // Activates whatever has focus, as Enter or space does.
70
+ bool activateFocused();
71
+
72
+ // WM_KEYDOWN, from the host's window procedure. True when the key was used,
73
+ // which is what keeps Tab from also reaching whatever else might want it.
74
+ bool handleKeyDown(unsigned int virtualKey);
75
+
76
+ // An app's own declared shortcuts, asked before the platform's own keys. True
77
+ // when a view from the focused one outwards claimed this combination, in which
78
+ // case it has already been reported; see core/KeyEvents.h.
79
+ bool handleDeclaredKey(unsigned int virtualKey);
80
+
81
+ // BASALT_TEST_KEY. The same delivery a real press gets, minus the virtual-key
82
+ // translation, which tests/test_win32_key_events.cpp covers instead. Named as
83
+ // the other two hosts name it, so the three read alike. See core/TestKeys.h.
84
+ bool deliverTestKey(const KeyCombination &pressed);
85
+
86
+ // Called when a <TextInput>'s peer takes Win32 focus, so that a view holding
87
+ // the ring gives it up. The two kinds of focus are different mechanisms and
88
+ // only one of them can be true at a time.
89
+ void textInputTookFocus();
90
+
91
+ // The tag of the focused view, or 0. Zero while a <TextInput> holds focus:
92
+ // that is the peer's, and the peer reports its own onFocus.
93
+ facebook::react::Tag focusedTag() const { return focusedTag_; }
94
+
95
+ private:
96
+ // Everything Tab should stop on, in tree order. Rebuilt per move rather than
97
+ // cached: views arrive and leave with every mutation, and a stale order is
98
+ // worse than a walk of a tree that is already in memory.
99
+ std::vector<win32::RnWin32View *> collectStops() const;
100
+
101
+ void setFocusedView(win32::RnWin32View *view);
102
+ void emitFocus(facebook::react::Tag tag, bool focused);
103
+
104
+ Win32MountingManager *mountingManager_;
105
+ win32::RnWin32View *surfaceRoot_;
106
+ facebook::react::Tag focusedTag_{0};
107
+ };
108
+
109
+ } // namespace basalt
@@ -0,0 +1,167 @@
1
+ #include "Win32ImageLoader.h"
2
+
3
+ #include "ImageBytes.h"
4
+ #include "PlatformServices.h"
5
+ #include "Win32UiThread.h"
6
+
7
+ #include <cstdint>
8
+ #include <thread>
9
+ #include <utility>
10
+
11
+ namespace basalt::win32 {
12
+
13
+ using basalt::hasUiThread;
14
+ using basalt::postToUiThread;
15
+
16
+ Win32ImageLoader::Win32ImageLoader() : state_(std::make_shared<State>()) {}
17
+
18
+ Win32ImageLoader::~Win32ImageLoader() {
19
+ // The state outlives this object whenever a load is in flight. Marking it
20
+ // dead is what makes the completion a no-op rather than a use-after-free.
21
+ const std::lock_guard<std::mutex> lock(state_->mutex);
22
+ state_->alive = false;
23
+ }
24
+
25
+ void Win32ImageLoader::clearCache() {
26
+ const std::lock_guard<std::mutex> lock(state_->mutex);
27
+ state_->cache.clear();
28
+ state_->policy.clear();
29
+ }
30
+
31
+ void Win32ImageLoader::load(const std::string &uri, Callback callback) {
32
+ if (!callback) {
33
+ return;
34
+ }
35
+ if (uri.empty()) {
36
+ callback(nullptr, "empty uri");
37
+ return;
38
+ }
39
+
40
+ // Answered synchronously, deliberately: an <Image> whose pixels are already
41
+ // decoded should not flicker through a frame of nothing on its way back to
42
+ // the screen. Both other platforms answer a cache hit inline too.
43
+ {
44
+ const std::lock_guard<std::mutex> lock(state_->mutex);
45
+ if (const auto it = state_->cache.find(uri); it != state_->cache.end()) {
46
+ const auto image = it->second;
47
+ state_->policy.noteUse(uri);
48
+ // Outside the lock would be tidier; inside is fine because the callback
49
+ // never re-enters the loader, and holding it across the call is what
50
+ // stops a concurrent clearCache from dropping the entry underneath.
51
+ callback(image, {});
52
+ return;
53
+ }
54
+ }
55
+
56
+ // Fetch and decode, and where they run depends on whether there is anywhere
57
+ // to come back to.
58
+ const auto work = [uri](std::shared_ptr<RnWin32Image> &image, std::string &error) {
59
+ std::string bytes;
60
+ if (!fetchImageBytes(uri, &bytes, &error)) {
61
+ if (error.empty()) {
62
+ error = "could not fetch " + uri;
63
+ }
64
+ return;
65
+ }
66
+ if (bytes.empty()) {
67
+ error = "no bytes for " + uri;
68
+ return;
69
+ }
70
+ image = RnWin32Image::fromEncodedBytes(
71
+ reinterpret_cast<const uint8_t *>(bytes.data()), bytes.size());
72
+ if (image == nullptr) {
73
+ error = "could not decode " + uri;
74
+ }
75
+ };
76
+
77
+ auto state = state_;
78
+ const auto deliver = [state, uri](const std::shared_ptr<RnWin32Image> &image,
79
+ const std::string &error,
80
+ const Callback &callback) {
81
+ // The loader may have gone while this was in flight -- a surface torn down,
82
+ // not only the process exiting. The state is still here, which is the point
83
+ // of it being shared, and there is simply nothing to deliver to.
84
+ {
85
+ const std::lock_guard<std::mutex> lock(state->mutex);
86
+ if (!state->alive) {
87
+ return;
88
+ }
89
+ if (image != nullptr) {
90
+ state->cache[uri] = image;
91
+
92
+ // Four bytes a pixel, premultiplied BGRA, which is what
93
+ // `fromEncodedBytes` decodes to and what RnWin32Image's own stride
94
+ // says. See core/ImageCache.h for which URI goes next.
95
+ const size_t bytes = static_cast<size_t>(image->width()) *
96
+ static_cast<size_t>(image->height()) * 4u;
97
+ for (const std::string &evicted : state->policy.insert(uri, bytes)) {
98
+ state->cache.erase(evicted);
99
+ }
100
+ }
101
+ }
102
+ callback(image, error);
103
+ };
104
+
105
+ // No message loop to post back to, so no worker either.
106
+ //
107
+ // This is the case the harness and the tests are in, and going asynchronous
108
+ // anyway would be actively wrong rather than merely pointless:
109
+ // `postToUiThread` runs its work inline when nothing is installed, so the
110
+ // completion would run *on the worker* and reach straight into the mounting
111
+ // manager's registry from the wrong thread. Staying synchronous keeps that
112
+ // configuration single-threaded, which is what it already assumes everywhere
113
+ // else.
114
+ if (!hasUiThread()) {
115
+ std::shared_ptr<RnWin32Image> image;
116
+ std::string error;
117
+ work(image, error);
118
+ deliver(image, error, callback);
119
+ return;
120
+ }
121
+
122
+ // With a host, both halves go to a worker. GTK reads on a worker and decodes
123
+ // back on the main thread, because a GdkTexture is a GObject and the
124
+ // expensive part there is the read; WIC has neither constraint -- its factory
125
+ // is agile and the decode is the expensive part -- so the whole job leaves
126
+ // the UI thread and only the delivery comes back.
127
+ //
128
+ // Detached rather than joined: nothing waits for an image, and everything the
129
+ // thread touches is either its own or the shared state above.
130
+ std::thread([work, deliver, callback = std::move(callback)]() mutable {
131
+ std::shared_ptr<RnWin32Image> image;
132
+ std::string error;
133
+ work(image, error);
134
+ postToUiThread([deliver, image, error, callback = std::move(callback)]() {
135
+ deliver(image, error, callback);
136
+ });
137
+ }).detach();
138
+ }
139
+
140
+
141
+ void Win32ImageLoader::loadImage(const std::string &uri,
142
+ const facebook::react::IImageLoaderOnLoadCallback &&onLoad) {
143
+ // Copied because the callback may outlive this call, as it does on the other
144
+ // two hosts; here a cache hit answers inline, and a miss does not.
145
+ auto callback = onLoad;
146
+ load(uri, [callback](std::shared_ptr<RnWin32Image> image, const std::string &error) {
147
+ if (image == nullptr) {
148
+ // The message rather than a size: `Image.getSize` rejects with it, and a
149
+ // zero-by-zero success would be indistinguishable from a 0x0 image.
150
+ callback(0.0, 0.0, error.empty() ? "could not load image" : error.c_str());
151
+ return;
152
+ }
153
+ callback(static_cast<double>(image->width()), static_cast<double>(image->height()), nullptr);
154
+ });
155
+ }
156
+
157
+ facebook::react::IImageLoader::CacheStatus Win32ImageLoader::getCacheStatus(
158
+ const std::string &uri) {
159
+ // Memory or nothing: there is no disk cache here, and saying `Disk` would be
160
+ // claiming a persistence this has not got.
161
+ const std::lock_guard<std::mutex> lock(state_->mutex);
162
+ return state_->cache.find(uri) != state_->cache.end()
163
+ ? facebook::react::IImageLoader::CacheStatus::Memory
164
+ : facebook::react::IImageLoader::CacheStatus::None;
165
+ }
166
+
167
+ } // namespace basalt::win32
@@ -0,0 +1,104 @@
1
+ // The pixels behind an <Image>, fetched and decoded off the UI thread.
2
+ //
3
+ // React Native's cxx `ImageManager` is a stub -- `requestImage` returns an
4
+ // `ImageRequest` with no observer coordinator, so no `ImageResponse` ever
5
+ // arrives and `ImageState` never carries anything to render. The platform view
6
+ // is expected to load its own pixels, which is what Android does too, from
7
+ // `ReactImageView` rather than from the shadow node. `docs/DECISIONS.md` records
8
+ // the same conclusion for GTK.
9
+ //
10
+ // Two rules fall out of the lifetimes, and both are the reason this is a class
11
+ // rather than a function.
12
+ //
13
+ // **A load must survive the view that asked for it.** A view can be deleted
14
+ // while its bytes are still in flight, so the completion may not capture a
15
+ // view; the mounting manager looks the tag up again when it runs. That is the
16
+ // same arrangement GTK settled on.
17
+ //
18
+ // **A load must survive the loader.** The worker thread and the queued
19
+ // completion both outlive any particular call, and a mounting manager can be
20
+ // destroyed between the two -- which happens on surface teardown, not only at
21
+ // exit. So the cache lives in a shared state object that the in-flight work
22
+ // holds a reference to, and a completion arriving after the loader is gone
23
+ // finds the state alive and the callback already neutered. The AppKit loader
24
+ // captures its `this` raw and has the bug this avoids.
25
+
26
+ #pragma once
27
+
28
+ #include "RnWin32Image.h"
29
+
30
+ #include <functional>
31
+ #include <mutex>
32
+ #include <memory>
33
+ #include <string>
34
+ #include <unordered_map>
35
+
36
+ #include "ImageCache.h"
37
+
38
+ #include <react/io/IImageLoader.h>
39
+
40
+ namespace basalt::win32 {
41
+
42
+ // Also React Native's own `IImageLoader`, which is what `Image.getSize` and
43
+ // `Image.prefetch` reach. Upstream's `ImageLoaderModule` takes one and
44
+ // `ReactCxxTurboModuleProvider` never supplies it -- see
45
+ // docs/backlog/upstream.md -- so the host registers the module itself with
46
+ // this as the loader.
47
+ class Win32ImageLoader : public facebook::react::IImageLoader {
48
+ public:
49
+ // `image` is null when the load failed, and `error` says why. Always called
50
+ // on the UI thread, and possibly before `load` has returned -- a cache hit is
51
+ // answered synchronously, because making it asynchronous would mean an
52
+ // <Image> that is already decoded still flickers through a frame of nothing.
53
+ using Callback = std::function<void(std::shared_ptr<RnWin32Image> image,
54
+ const std::string &error)>;
55
+
56
+ Win32ImageLoader();
57
+ ~Win32ImageLoader();
58
+
59
+ Win32ImageLoader(const Win32ImageLoader &) = delete;
60
+ Win32ImageLoader &operator=(const Win32ImageLoader &) = delete;
61
+
62
+ void load(const std::string &uri, Callback callback);
63
+
64
+ // --- IImageLoader ----------------------------------------------------------
65
+ //
66
+ // The same decode as `load`, reporting the size rather than the pixels. It
67
+ // goes through the same cache, so `Image.getSize` on something already on
68
+ // screen answers without touching the disk or the network.
69
+ void loadImage(const std::string &uri,
70
+ const facebook::react::IImageLoaderOnLoadCallback &&onLoad) override;
71
+
72
+ facebook::react::IImageLoader::CacheStatus getCacheStatus(const std::string &uri) override;
73
+
74
+ // Drops every decoded image at once, for a host shutting a surface down.
75
+ // Ordinary eviction is core/ImageCache.h's, which runs on every insert.
76
+ void clearCache();
77
+
78
+ private:
79
+ struct State {
80
+ // Locked, rather than relying on "everything happens on the UI thread".
81
+ //
82
+ // That reasoning was tried and is wrong here in a way worth recording,
83
+ // because it looks right: `load` is called on the UI thread and every
84
+ // completion is posted back to it, so a lock seems redundant. But
85
+ // `postToUiThread` runs its work *inline* when no host has installed a
86
+ // message loop, and inline on a worker thread is not the UI thread. The
87
+ // load below avoids that case entirely by not spawning a worker when there
88
+ // is nothing to marshal back to; this lock is the belt to that pair of
89
+ // braces, and it costs a hash lookup's worth of nothing.
90
+ std::mutex mutex;
91
+ std::unordered_map<std::string, std::shared_ptr<RnWin32Image>> cache;
92
+ // Which URI goes next, and when. Shared with the other two hosts; the
93
+ // images are this loader's. See core/ImageCache.h. Guarded by the mutex
94
+ // beside it, like the cache it decides for.
95
+ basalt::ImageCachePolicy policy;
96
+ // Cleared when the loader is destroyed, so a completion that outlives it
97
+ // delivers nowhere instead of into freed memory.
98
+ bool alive = true;
99
+ };
100
+
101
+ std::shared_ptr<State> state_;
102
+ };
103
+
104
+ } // namespace basalt::win32