basalt-win32 0.0.0-rc5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/cli/runWindows.js +35 -0
- package/cli/shared.js +75 -0
- package/native/CMakeLists.txt +398 -0
- package/native/tests/test_win32_accessibility.cpp +271 -0
- package/native/tests/test_win32_clipboard.cpp +121 -0
- package/native/tests/test_win32_focus.cpp +195 -0
- package/native/tests/test_win32_hittest.cpp +410 -0
- package/native/tests/test_win32_image.cpp +239 -0
- package/native/tests/test_win32_imageloader.cpp +208 -0
- package/native/tests/test_win32_input.cpp +361 -0
- package/native/tests/test_win32_key_events.cpp +84 -0
- package/native/tests/test_win32_mounting.cpp +410 -0
- package/native/tests/test_win32_paint.cpp +505 -0
- package/native/tests/test_win32_scrollview.cpp +385 -0
- package/native/tests/test_win32_text.cpp +258 -0
- package/native/tests/test_win32_textinput.cpp +609 -0
- package/native/tests/test_win32_titlebar.cpp +111 -0
- package/native/tests/test_win32_titlebar_actions.cpp +62 -0
- package/native/tests/test_win32_view.cpp +222 -0
- package/native/win32/ColorSchemeWin32.cpp +56 -0
- package/native/win32/ComponentRegistryWin32.cpp +124 -0
- package/native/win32/DirectWriteLayout.cpp +116 -0
- package/native/win32/DirectWriteLayout.h +36 -0
- package/native/win32/DirectWriteLayoutManager.cpp +124 -0
- package/native/win32/FontRegistryDirectWrite.cpp +117 -0
- package/native/win32/PlatformServicesWin32.cpp +643 -0
- package/native/win32/RnWin32Accessible.cpp +258 -0
- package/native/win32/RnWin32Accessible.h +91 -0
- package/native/win32/RnWin32Image.cpp +265 -0
- package/native/win32/RnWin32Image.h +93 -0
- package/native/win32/RnWin32TextLayout.cpp +310 -0
- package/native/win32/RnWin32TextLayout.h +160 -0
- package/native/win32/RnWin32View.cpp +1281 -0
- package/native/win32/RnWin32View.h +506 -0
- package/native/win32/Win32AnimationChoreographer.cpp +76 -0
- package/native/win32/Win32AnimationChoreographer.h +61 -0
- package/native/win32/Win32Clip.h +173 -0
- package/native/win32/Win32DropTarget.cpp +567 -0
- package/native/win32/Win32DropTarget.h +42 -0
- package/native/win32/Win32Focus.cpp +262 -0
- package/native/win32/Win32Focus.h +109 -0
- package/native/win32/Win32ImageLoader.cpp +167 -0
- package/native/win32/Win32ImageLoader.h +104 -0
- package/native/win32/Win32KeyEvents.cpp +126 -0
- package/native/win32/Win32KeyEvents.h +20 -0
- package/native/win32/Win32MenuBar.cpp +352 -0
- package/native/win32/Win32MenuBar.h +16 -0
- package/native/win32/Win32MountingManager.cpp +727 -0
- package/native/win32/Win32MountingManager.h +203 -0
- package/native/win32/Win32Packaging.cpp +161 -0
- package/native/win32/Win32Packaging.h +56 -0
- package/native/win32/Win32RunLoopObserver.cpp +48 -0
- package/native/win32/Win32RunLoopObserver.h +42 -0
- package/native/win32/Win32ScrollView.cpp +515 -0
- package/native/win32/Win32ScrollView.h +208 -0
- package/native/win32/Win32SkiaContext.cpp +221 -0
- package/native/win32/Win32SkiaContext.h +53 -0
- package/native/win32/Win32SkiaModule.cpp +80 -0
- package/native/win32/Win32SkiaModule.h +50 -0
- package/native/win32/Win32SkiaPeer.cpp +358 -0
- package/native/win32/Win32SkiaPeer.h +66 -0
- package/native/win32/Win32SkiaUnicode.cpp +42 -0
- package/native/win32/Win32Snapshot.cpp +201 -0
- package/native/win32/Win32Snapshot.h +66 -0
- package/native/win32/Win32Strings.h +54 -0
- package/native/win32/Win32TextInput.cpp +811 -0
- package/native/win32/Win32TextInput.h +232 -0
- package/native/win32/Win32TitleBar.cpp +562 -0
- package/native/win32/Win32TitleBar.h +146 -0
- package/native/win32/Win32TitleBarLayout.cpp +46 -0
- package/native/win32/Win32TitleBarLayout.h +58 -0
- package/native/win32/Win32TouchDispatcher.cpp +443 -0
- package/native/win32/Win32TouchDispatcher.h +153 -0
- package/native/win32/Win32UiThread.h +27 -0
- package/native/win32/Win32WindowControl.cpp +329 -0
- package/native/win32/Win32WindowModule.cpp +209 -0
- package/native/win32/Win32WindowModule.h +72 -0
- package/native/win32/demo_layout_win32.cpp +232 -0
- package/native/win32/main_win32.cpp +2481 -0
- package/native/win32/mount_harness_win32.cpp +316 -0
- package/package.json +26 -0
- package/react-native.config.js +22 -0
|
@@ -0,0 +1,2481 @@
|
|
|
1
|
+
// basalt-core — the Windows host process.
|
|
2
|
+
//
|
|
3
|
+
// Constructs a ReactHost, loads a script into Hermes, and runs one surface in
|
|
4
|
+
// an HWND. Everything on screen from here on is produced by Fabric: the
|
|
5
|
+
// mutation stream arrives through Win32MountingManager exactly as it does on
|
|
6
|
+
// iOS and Android, and nothing hand-builds a ShadowViewMutation.
|
|
7
|
+
//
|
|
8
|
+
// The counterpart of gtk/main_gtk.cpp and appkit/main_appkit.mm, and
|
|
9
|
+
// deliberately the same shape. The pieces a host has to supply, and where each
|
|
10
|
+
// comes from:
|
|
11
|
+
//
|
|
12
|
+
// IMountingManager Win32MountingManager (this repo)
|
|
13
|
+
// RunLoopObserverManager ReactCxxPlatform (event beat)
|
|
14
|
+
// the beat itself Win32RunLoopObserver (this repo, the loop)
|
|
15
|
+
// AnimationChoreographer Win32AnimationChoreographer (this repo, a timer)
|
|
16
|
+
// ContextContainer http + websocket client factories, below
|
|
17
|
+
// ComponentRegistryFactory ComponentRegistryWin32 (via the mounting manager)
|
|
18
|
+
// FontRegistry FontRegistryDirectWrite (this repo, link-time seam)
|
|
19
|
+
// input Win32TouchDispatcher (this repo, hostProc)
|
|
20
|
+
//
|
|
21
|
+
// Threading: this thread owns the window and the message loop. ReactHost spins
|
|
22
|
+
// up its own JS thread, and every mount is marshalled back here by
|
|
23
|
+
// Win32MountingManager through postToUiThread. Nothing below touches a view off
|
|
24
|
+
// this thread.
|
|
25
|
+
//
|
|
26
|
+
// What this host can render is what Win32MountingManager can mount: <View>,
|
|
27
|
+
// <Text>, <Image>, <ScrollView> and <TextInput> -- the same five as the other
|
|
28
|
+
// two desktops. `js/demo.js` -- a surface driven straight through
|
|
29
|
+
// nativeFabricUIManager, with no React and no react-native JavaScript -- is
|
|
30
|
+
// still the bundle this runs by default, because it is what an argumentless run
|
|
31
|
+
// has always meant here; an ordinary React app runs from a bundle and a module
|
|
32
|
+
// name.
|
|
33
|
+
//
|
|
34
|
+
// One thing below is not like the other hosts, and it is <TextInput>. Its peer
|
|
35
|
+
// is a real EDIT control, which is a child *window*, so the host has to place
|
|
36
|
+
// it, forward its notifications, and answer its colour questions. See
|
|
37
|
+
// win32/Win32TextInput.h.
|
|
38
|
+
|
|
39
|
+
#include "Win32AnimationChoreographer.h"
|
|
40
|
+
#include "AppIdentity.h"
|
|
41
|
+
#include "AppPaths.h"
|
|
42
|
+
#include "DevMenu.h"
|
|
43
|
+
#include "DialogModule.h"
|
|
44
|
+
#include "MenuModel.h"
|
|
45
|
+
#include "MenuModule.h"
|
|
46
|
+
#include "WindowsModule.h"
|
|
47
|
+
#include "TestKeys.h"
|
|
48
|
+
#include "TestQuitFile.h"
|
|
49
|
+
#include "TestSettle.h"
|
|
50
|
+
#include "Win32MountingManager.h"
|
|
51
|
+
|
|
52
|
+
#include <react/io/ImageLoaderModule.h>
|
|
53
|
+
#include "Win32MenuBar.h"
|
|
54
|
+
#ifdef BASALT_HAS_SKIA
|
|
55
|
+
#include "Win32SkiaModule.h"
|
|
56
|
+
#endif
|
|
57
|
+
#include "Win32Packaging.h"
|
|
58
|
+
#include "PointerButtons.h"
|
|
59
|
+
#include "WindowControl.h"
|
|
60
|
+
#include "WindowHost.h"
|
|
61
|
+
#include "Win32RunLoopObserver.h"
|
|
62
|
+
#include "Win32Snapshot.h"
|
|
63
|
+
#include "Win32Strings.h"
|
|
64
|
+
#include "LogBoxSurface.h"
|
|
65
|
+
|
|
66
|
+
#include <react/renderer/animated/NativeAnimatedNodesManagerProvider.h>
|
|
67
|
+
#include "Win32Focus.h"
|
|
68
|
+
#include "Win32TouchDispatcher.h"
|
|
69
|
+
#include "Win32UiThread.h"
|
|
70
|
+
#include "DragAndDrop.h"
|
|
71
|
+
#include "Win32DropTarget.h"
|
|
72
|
+
#include "Win32TitleBar.h"
|
|
73
|
+
#include "Win32WindowModule.h"
|
|
74
|
+
#include "RnWin32View.h"
|
|
75
|
+
|
|
76
|
+
#include "AppearanceModule.h"
|
|
77
|
+
#include "BlobModule.h"
|
|
78
|
+
#include "ColorScheme.h"
|
|
79
|
+
#include "CoreModules.h"
|
|
80
|
+
#include "DevBundle.h"
|
|
81
|
+
#include "ExpoModules.h"
|
|
82
|
+
#include "ExpoRuntime.h"
|
|
83
|
+
#include "TurboModuleProxy.h"
|
|
84
|
+
#include "GestureHandlerModule.h"
|
|
85
|
+
#include "PlatformConstantsModule.h"
|
|
86
|
+
#include "PlatformServices.h"
|
|
87
|
+
#include "ReanimatedModule.h"
|
|
88
|
+
#include "SourceCodeModule.h"
|
|
89
|
+
#include "StatusBarModule.h"
|
|
90
|
+
#include "JsRuntimeAccess.h"
|
|
91
|
+
#include "PackageModules.h"
|
|
92
|
+
#include "UIManagerAccess.h"
|
|
93
|
+
#include "WorkletsModule.h"
|
|
94
|
+
|
|
95
|
+
#include <glog/logging.h>
|
|
96
|
+
#include <jsi/jsi.h>
|
|
97
|
+
#include <logger/react_native_log.h>
|
|
98
|
+
#include <react/featureflags/ReactNativeFeatureFlags.h>
|
|
99
|
+
#include <react/featureflags/ReactNativeFeatureFlagsDefaults.h>
|
|
100
|
+
#include <react/http/IHttpClient.h>
|
|
101
|
+
#include <react/http/IWebSocketClient.h>
|
|
102
|
+
#include <react/logging/DefaultOnJsErrorHandler.h>
|
|
103
|
+
#include <react/renderer/core/LayoutConstraints.h>
|
|
104
|
+
#include <react/renderer/core/LayoutContext.h>
|
|
105
|
+
#include <react/runtime/ReactHost.h>
|
|
106
|
+
#include <react/runtime/ReactInstanceConfig.h>
|
|
107
|
+
#include <react/utils/ContextContainer.h>
|
|
108
|
+
#include <react/utils/RunLoopObserverManager.h>
|
|
109
|
+
|
|
110
|
+
#include <windows.h>
|
|
111
|
+
// GET_X_LPARAM. Its own header, and not pulled in by WIN32_LEAN_AND_MEAN.
|
|
112
|
+
#include <windowsx.h>
|
|
113
|
+
|
|
114
|
+
#include <d2d1.h>
|
|
115
|
+
#include <wrl/client.h>
|
|
116
|
+
|
|
117
|
+
#include <algorithm>
|
|
118
|
+
#include <cstdio>
|
|
119
|
+
#include <cstdlib>
|
|
120
|
+
#include <cwchar>
|
|
121
|
+
#include <exception>
|
|
122
|
+
#include <memory>
|
|
123
|
+
#include <string>
|
|
124
|
+
#include <vector>
|
|
125
|
+
|
|
126
|
+
using Microsoft::WRL::ComPtr;
|
|
127
|
+
using facebook::react::ContextContainer;
|
|
128
|
+
using facebook::react::Float;
|
|
129
|
+
using facebook::react::LayoutConstraints;
|
|
130
|
+
using facebook::react::LayoutContext;
|
|
131
|
+
using facebook::react::LayoutDirection;
|
|
132
|
+
using facebook::react::ReactHost;
|
|
133
|
+
using facebook::react::ReactInstanceConfig;
|
|
134
|
+
using facebook::react::RunLoopObserverManager;
|
|
135
|
+
using facebook::react::SurfaceId;
|
|
136
|
+
using basalt::win32::RnWin32View;
|
|
137
|
+
|
|
138
|
+
namespace {
|
|
139
|
+
|
|
140
|
+
// In Fabric a SurfaceId *is* the root shadow node's tag, which is what lets the
|
|
141
|
+
// surface root live in the mounting manager's registry like any other view.
|
|
142
|
+
constexpr SurfaceId kSurfaceId = 1;
|
|
143
|
+
|
|
144
|
+
// React Native's error inspector, which is a surface of its own -- registered
|
|
145
|
+
// by AppRegistry under the name "LogBox", exactly as an app registers its own
|
|
146
|
+
// component. Started over the app's when NativeLogBox.show() asks; see
|
|
147
|
+
// core/LogBoxSurface.h.
|
|
148
|
+
constexpr SurfaceId kLogBoxSurfaceId = 2;
|
|
149
|
+
|
|
150
|
+
constexpr int kInitialWidth = 900;
|
|
151
|
+
constexpr int kInitialHeight = 700;
|
|
152
|
+
|
|
153
|
+
// The entry point for the *scriptless* mode, where the bundle is a hand-written
|
|
154
|
+
// script talking to nativeFabricUIManager directly rather than a React app.
|
|
155
|
+
// See js/demo.js. Only used when no module name is given.
|
|
156
|
+
constexpr const char *kRenderFunctionName = "basaltRender";
|
|
157
|
+
|
|
158
|
+
constexpr UINT_PTR kSecondTreeTimer = 100;
|
|
159
|
+
|
|
160
|
+
// BASALT_QUIT_AFTER_MS's timer. A constant rather than the literal 101 it used
|
|
161
|
+
// to be written as at its one call site: the spinner timer below was given 101
|
|
162
|
+
// too, and `SetTimer` with an id that is already in use *replaces* the timer
|
|
163
|
+
// rather than failing. An app with an <ActivityIndicator> in it therefore
|
|
164
|
+
// silently cancelled its own shutdown, and the only symptom was a host that
|
|
165
|
+
// never exited.
|
|
166
|
+
constexpr UINT_PTR kQuitAfterTimer = 101;
|
|
167
|
+
|
|
168
|
+
// One window, and the surface in it.
|
|
169
|
+
//
|
|
170
|
+
// A window is a surface: a surface is what has a size, a layout context and a
|
|
171
|
+
// root shadow node, and two windows sharing one would be two windows sharing a
|
|
172
|
+
// layout. So the id a window is known by *is* its surface id. See
|
|
173
|
+
// core/WindowHost.h.
|
|
174
|
+
//
|
|
175
|
+
// The render target is here rather than on Host because a
|
|
176
|
+
// ID2D1HwndRenderTarget belongs to one HWND -- painting a second window through
|
|
177
|
+
// the first one's would draw into the first one.
|
|
178
|
+
struct HostWindow {
|
|
179
|
+
facebook::react::SurfaceId surfaceId{0};
|
|
180
|
+
HWND window{nullptr};
|
|
181
|
+
RnWin32View *root{nullptr};
|
|
182
|
+
// Whether this press has already been offered as a drag out; see
|
|
183
|
+
// WM_MOUSEMOVE.
|
|
184
|
+
bool dragAsked{false};
|
|
185
|
+
ComPtr<ID2D1HwndRenderTarget> target;
|
|
186
|
+
std::unique_ptr<basalt::Win32TouchDispatcher> touchDispatcher;
|
|
187
|
+
std::unique_ptr<basalt::Win32FocusManager> focusManager;
|
|
188
|
+
// The app is closing this window, so WM_DESTROY is not news.
|
|
189
|
+
//
|
|
190
|
+
// Both closes end in `DestroyWindow` and so both raise WM_DESTROY, and the
|
|
191
|
+
// handler there is the one that tells JavaScript a window closed *itself* and
|
|
192
|
+
// tears the record down. Without this, an app closing a window ran that too:
|
|
193
|
+
// a second `stopSurface`, a `basaltWindowClosed` nobody closed, and a second
|
|
194
|
+
// deferred teardown that destroyed an already-destroyed root -- which is an
|
|
195
|
+
// access violation at exit rather than a warning.
|
|
196
|
+
//
|
|
197
|
+
// AppKit has the same hazard and answers it the same way, by nilling the
|
|
198
|
+
// delegate before `close`. Windows has no delegate to nil.
|
|
199
|
+
bool closingFromApp{false};
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
struct Host {
|
|
203
|
+
// Every window, main one first. Never empty once the app has started.
|
|
204
|
+
std::vector<std::unique_ptr<HostWindow>> windows;
|
|
205
|
+
|
|
206
|
+
// The window the app was started in. Shorthand for windows.front(), which is
|
|
207
|
+
// what every part of this host that is not yet per-window uses.
|
|
208
|
+
// An empty window with null fields before there is a real one, rather than
|
|
209
|
+
// dereferencing an empty vector. Several things here run before the window
|
|
210
|
+
// exists -- a repaint request, the spinner timer -- and each one already
|
|
211
|
+
// guards on `window == nullptr`, which is exactly what this keeps true.
|
|
212
|
+
HostWindow &main() {
|
|
213
|
+
static HostWindow none;
|
|
214
|
+
return windows.empty() ? none : *windows.front();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
HostWindow *windowFor(facebook::react::SurfaceId surfaceId) {
|
|
218
|
+
for (const auto &candidate : windows) {
|
|
219
|
+
if (candidate->surfaceId == surfaceId) {
|
|
220
|
+
return candidate.get();
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return nullptr;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Which window a message is for. `hostProc` is one procedure for every window
|
|
227
|
+
// this host makes, so every message starts by asking this.
|
|
228
|
+
HostWindow *windowFor(HWND hwnd) {
|
|
229
|
+
for (const auto &candidate : windows) {
|
|
230
|
+
if (candidate->window == hwnd) {
|
|
231
|
+
return candidate.get();
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return nullptr;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
ComPtr<ID2D1Factory> d2dFactory;
|
|
238
|
+
|
|
239
|
+
std::shared_ptr<basalt::Win32MountingManager> mountingManager;
|
|
240
|
+
std::shared_ptr<RunLoopObserverManager> runLoopObserverManager;
|
|
241
|
+
std::shared_ptr<basalt::Win32AnimationChoreographer> choreographer;
|
|
242
|
+
std::unique_ptr<ReactHost> reactHost;
|
|
243
|
+
// The error inspector's own surface root, or null when it is not showing.
|
|
244
|
+
// Painted after the app's and hit-tested before it, which is the whole of
|
|
245
|
+
// what "on top" means on a platform where a view is not a window.
|
|
246
|
+
RnWin32View *logBoxRoot{nullptr};
|
|
247
|
+
|
|
248
|
+
std::string bundlePath;
|
|
249
|
+
// Empty means the bundle is a raw Fabric script rather than a React app.
|
|
250
|
+
std::string moduleName;
|
|
251
|
+
// Metro's entry point, without the extension. Only meaningful in dev mode.
|
|
252
|
+
std::string sourcePath;
|
|
253
|
+
bool surfaceStarted{false};
|
|
254
|
+
// Whether this run is a development one, which is the only thing that decides
|
|
255
|
+
// whether Ctrl+D opens anything. Kept because the ReactInstanceConfig it came
|
|
256
|
+
// from is not.
|
|
257
|
+
bool devMode{false};
|
|
258
|
+
int scaleFactor{1};
|
|
259
|
+
// Whether TrackMouseEvent is armed. Windows sends WM_MOUSELEAVE once and then
|
|
260
|
+
// forgets, so it has to be re-armed on every move; the flag keeps that to one
|
|
261
|
+
// call per entry rather than one per motion message.
|
|
262
|
+
bool trackingMouseLeave{false};
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
Host gHost;
|
|
266
|
+
|
|
267
|
+
// ---------------------------------------------------------------------------
|
|
268
|
+
// Logging
|
|
269
|
+
// ---------------------------------------------------------------------------
|
|
270
|
+
|
|
271
|
+
// ReactHost's Logger is where console.log and RN's own logging come out.
|
|
272
|
+
// stderr rather than OutputDebugString: this is a console subsystem binary, and
|
|
273
|
+
// a host whose logs are only visible in a debugger is a host nobody can run
|
|
274
|
+
// from a terminal.
|
|
275
|
+
void logToConsole(const std::string &message, unsigned int logLevel) {
|
|
276
|
+
const char *prefix = "[js]";
|
|
277
|
+
switch (logLevel) {
|
|
278
|
+
case ReactNativeLogLevelFatal:
|
|
279
|
+
case ReactNativeLogLevelError:
|
|
280
|
+
prefix = "[js error]";
|
|
281
|
+
break;
|
|
282
|
+
case ReactNativeLogLevelWarning:
|
|
283
|
+
prefix = "[js warn]";
|
|
284
|
+
break;
|
|
285
|
+
default:
|
|
286
|
+
break;
|
|
287
|
+
}
|
|
288
|
+
std::fprintf(stderr, "%s %s\n", prefix, message.c_str());
|
|
289
|
+
std::fflush(stderr);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ---------------------------------------------------------------------------
|
|
293
|
+
// Layout constraints
|
|
294
|
+
// ---------------------------------------------------------------------------
|
|
295
|
+
|
|
296
|
+
// Minimum == maximum pins the root to the window's client box, which is what a
|
|
297
|
+
// full-window surface wants: the root fills, and flex children divide it.
|
|
298
|
+
LayoutConstraints constraintsFor(int width, int height) {
|
|
299
|
+
const facebook::react::Size size{static_cast<Float>(width), static_cast<Float>(height)};
|
|
300
|
+
return LayoutConstraints{
|
|
301
|
+
.minimumSize = size,
|
|
302
|
+
.maximumSize = size,
|
|
303
|
+
.layoutDirection = LayoutDirection::LeftToRight,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
LayoutContext layoutContextFor(int scaleFactor) {
|
|
308
|
+
return LayoutContext{.pointScaleFactor = static_cast<Float>(scaleFactor)};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// ---------------------------------------------------------------------------
|
|
312
|
+
// Painting
|
|
313
|
+
// ---------------------------------------------------------------------------
|
|
314
|
+
|
|
315
|
+
// Per window: an ID2D1HwndRenderTarget belongs to one HWND, so each window has
|
|
316
|
+
// its own and a second window cannot borrow the first's.
|
|
317
|
+
bool ensureTarget(HostWindow *made) {
|
|
318
|
+
if (made == nullptr) {
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
if (made->target) {
|
|
322
|
+
return true;
|
|
323
|
+
}
|
|
324
|
+
if (!gHost.d2dFactory || made->window == nullptr) {
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
RECT client{};
|
|
328
|
+
GetClientRect(made->window, &client);
|
|
329
|
+
const D2D1_SIZE_U size = D2D1::SizeU(static_cast<UINT32>(client.right - client.left),
|
|
330
|
+
static_cast<UINT32>(client.bottom - client.top));
|
|
331
|
+
if (size.width == 0 || size.height == 0) {
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
return SUCCEEDED(gHost.d2dFactory->CreateHwndRenderTarget(
|
|
335
|
+
D2D1::RenderTargetProperties(),
|
|
336
|
+
D2D1::HwndRenderTargetProperties(made->window, size),
|
|
337
|
+
&made->target));
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Asked for after every mounted transaction. Windows repaints on demand rather
|
|
341
|
+
// than continuously, so without this a mutation changes the view tree and
|
|
342
|
+
// nothing on screen moves until the window happens to be invalidated by
|
|
343
|
+
// something else -- which reads as "mounting is broken" and is not.
|
|
344
|
+
void requestRepaint() {
|
|
345
|
+
// Every window. This asked for the app's own and only the app's own, which
|
|
346
|
+
// was the whole truth when there was only ever one -- and afterwards meant a
|
|
347
|
+
// second window whose content changed and did not redraw until Windows
|
|
348
|
+
// happened to invalidate it for some other reason.
|
|
349
|
+
//
|
|
350
|
+
// All of them rather than the one the transaction was for, because this is
|
|
351
|
+
// called from the mount path with no surface in hand. Invalidating a window
|
|
352
|
+
// that did not change costs one WM_PAINT and is what the main window has
|
|
353
|
+
// always done for a transaction in the error inspector.
|
|
354
|
+
for (const auto &window : gHost.windows) {
|
|
355
|
+
if (window->window != nullptr) {
|
|
356
|
+
InvalidateRect(window->window, nullptr, FALSE);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// After every transaction, and after every scroll. Both move views, and a
|
|
362
|
+
// <TextInput>'s peer is a child window that does not move with one: nothing in
|
|
363
|
+
// the view tree owns an HWND, so its placement is recomputed from the tree
|
|
364
|
+
// rather than following it. GTK and AppKit need no equivalent, because there a
|
|
365
|
+
// text field is a widget inside the widget that is the view.
|
|
366
|
+
// Whether the window has to keep repainting because a spinner is turning, and
|
|
367
|
+
// the timer that makes it.
|
|
368
|
+
//
|
|
369
|
+
// An <ActivityIndicator> is painted here rather than mounted -- Windows has no
|
|
370
|
+
// spinner control; see RnWin32View.h -- so nothing invalidates the window
|
|
371
|
+
// between mounts, and without this a spinner is a still picture. GtkSpinner and
|
|
372
|
+
// NSProgressIndicator each drive their own redraw, which is why neither other
|
|
373
|
+
// host needs an equivalent.
|
|
374
|
+
//
|
|
375
|
+
// Started and stopped rather than left running, for the reason
|
|
376
|
+
// Win32AnimationChoreographer.h gives about its own timer: a timer that runs
|
|
377
|
+
// whether or not anything is animating wakes the process sixty times a second
|
|
378
|
+
// forever, which matters more on a laptop than the frame interval does.
|
|
379
|
+
constexpr UINT_PTR kSpinnerTimer = 102;
|
|
380
|
+
|
|
381
|
+
// BASALT_TEST_QUIT_FILE's poll. Repeating, unlike kQuitAfterTimer's one shot:
|
|
382
|
+
// it looks for the file until it finds it. See core/TestQuitFile.h.
|
|
383
|
+
constexpr UINT_PTR kQuitFileTimer = 103;
|
|
384
|
+
bool gSpinnerTimerRunning = false;
|
|
385
|
+
|
|
386
|
+
void updateSpinnerTimer() {
|
|
387
|
+
if (gHost.main().window == nullptr || gHost.main().root == nullptr) {
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
const bool wanted = gHost.main().root->hasAnimatingSpinner() ||
|
|
391
|
+
(gHost.logBoxRoot != nullptr && gHost.logBoxRoot->hasAnimatingSpinner());
|
|
392
|
+
if (wanted == gSpinnerTimerRunning) {
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
gSpinnerTimerRunning = wanted;
|
|
396
|
+
if (wanted) {
|
|
397
|
+
SetTimer(gHost.main().window, kSpinnerTimer, 16, nullptr);
|
|
398
|
+
} else {
|
|
399
|
+
KillTimer(gHost.main().window, kSpinnerTimer);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
void syncPeersAndRepaint() {
|
|
404
|
+
if (gHost.mountingManager != nullptr) {
|
|
405
|
+
gHost.mountingManager->syncTextInputBounds(gHost.main().root);
|
|
406
|
+
}
|
|
407
|
+
updateSpinnerTimer();
|
|
408
|
+
requestRepaint();
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// ---------------------------------------------------------------------------
|
|
412
|
+
// Calling into JS
|
|
413
|
+
// ---------------------------------------------------------------------------
|
|
414
|
+
|
|
415
|
+
// Runs on the JS thread. `step` lets the script commit a second, different tree
|
|
416
|
+
// so an Update/Remove diff can be seen coming out of Fabric rather than only an
|
|
417
|
+
// initial mount.
|
|
418
|
+
void callRenderFunction(int step) {
|
|
419
|
+
if (gHost.reactHost == nullptr) {
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
gHost.reactHost->runOnRuntimeScheduler([step](facebook::jsi::Runtime &runtime) {
|
|
423
|
+
try {
|
|
424
|
+
auto value = runtime.global().getProperty(runtime, kRenderFunctionName);
|
|
425
|
+
if (!value.isObject() || !value.getObject(runtime).isFunction(runtime)) {
|
|
426
|
+
std::fprintf(stderr, "script defines no global %s()\n", kRenderFunctionName);
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
value.getObject(runtime).getFunction(runtime).call(
|
|
430
|
+
runtime,
|
|
431
|
+
facebook::jsi::Value(static_cast<int>(kSurfaceId)),
|
|
432
|
+
facebook::jsi::Value(step));
|
|
433
|
+
} catch (const facebook::jsi::JSError &error) {
|
|
434
|
+
std::fprintf(stderr,
|
|
435
|
+
"%s() threw: %s\n%s\n",
|
|
436
|
+
kRenderFunctionName,
|
|
437
|
+
error.getMessage().c_str(),
|
|
438
|
+
error.getStack().c_str());
|
|
439
|
+
} catch (const std::exception &error) {
|
|
440
|
+
std::fprintf(stderr, "%s() failed: %s\n", kRenderFunctionName, error.what());
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// ---------------------------------------------------------------------------
|
|
446
|
+
// Startup
|
|
447
|
+
// ---------------------------------------------------------------------------
|
|
448
|
+
|
|
449
|
+
// Feature flags this platform states explicitly. Anything not listed keeps
|
|
450
|
+
// React Native's own default.
|
|
451
|
+
class DesktopFeatureFlags : public facebook::react::ReactNativeFeatureFlagsDefaults {
|
|
452
|
+
public:
|
|
453
|
+
bool enableBridgelessArchitecture() override {
|
|
454
|
+
return true;
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
// TurboModules this platform supplies itself. ReactCxxTurboModuleProvider
|
|
459
|
+
// consults these before its own, so naming a module it also provides replaces
|
|
460
|
+
// it. Every one comes from core/ and is shared with the other two hosts --
|
|
461
|
+
// which is the whole argument of phase 17 arriving as a list of things this
|
|
462
|
+
// file did not have to write.
|
|
463
|
+
facebook::react::TurboModuleProviders makeTurboModuleProviders(
|
|
464
|
+
std::string scriptURL,
|
|
465
|
+
bool devMode,
|
|
466
|
+
std::weak_ptr<basalt::win32::Win32ImageLoader> imageLoader) {
|
|
467
|
+
facebook::react::TurboModuleProviders providers;
|
|
468
|
+
// Whatever the discovered capability packages supply, first: a package naming
|
|
469
|
+
// a module this host also provides is replacing it deliberately, and the same
|
|
470
|
+
// rule already applies to this list against ReactCxxPlatform's own. Answers
|
|
471
|
+
// nullptr for every name when no package supplies one, which is most builds.
|
|
472
|
+
// See core/PackageModules.h.
|
|
473
|
+
providers.emplace_back(basalt::packageTurboModule);
|
|
474
|
+
providers.emplace_back(
|
|
475
|
+
[scriptURL = std::move(scriptURL), devMode, imageLoader = std::move(imageLoader)](
|
|
476
|
+
const std::string &name,
|
|
477
|
+
const std::shared_ptr<facebook::react::CallInvoker> &jsInvoker)
|
|
478
|
+
-> std::shared_ptr<facebook::react::TurboModule> {
|
|
479
|
+
// `Image.getSize` and `Image.prefetch`. Upstream builds this module
|
|
480
|
+
// with no loader at all and offers no way to supply one, so the answer
|
|
481
|
+
// is to build it here with the loader this host already has -- which
|
|
482
|
+
// is also the one holding the decoded images, so a size asked for
|
|
483
|
+
// something on screen costs nothing. See docs/backlog/upstream.md.
|
|
484
|
+
if (name == facebook::react::ImageLoaderModule::kModuleName) {
|
|
485
|
+
return std::make_shared<facebook::react::ImageLoaderModule>(jsInvoker, imageLoader);
|
|
486
|
+
}
|
|
487
|
+
if (name == facebook::react::PlatformConstantsModule::kModuleName) {
|
|
488
|
+
return std::make_shared<basalt::DesktopPlatformConstantsModule>(jsInvoker);
|
|
489
|
+
}
|
|
490
|
+
if (name == basalt::DesktopAppModule::kModuleName) {
|
|
491
|
+
return std::make_shared<basalt::DesktopAppModule>(jsInvoker);
|
|
492
|
+
}
|
|
493
|
+
if (name == basalt::DesktopAppearanceModule::kModuleName) {
|
|
494
|
+
return std::make_shared<basalt::DesktopAppearanceModule>(jsInvoker);
|
|
495
|
+
}
|
|
496
|
+
#ifdef BASALT_HAS_SKIA
|
|
497
|
+
// @shopify/react-native-skia, when the build was pointed at an app that
|
|
498
|
+
// has it. Its `install` is what puts Skia's whole JavaScript API on the
|
|
499
|
+
// global object, and an app that imports Skia does not start without
|
|
500
|
+
// it; see win32/Win32SkiaModule.h.
|
|
501
|
+
if (name == basalt::Win32SkiaModule::kModuleName) {
|
|
502
|
+
return std::make_shared<basalt::Win32SkiaModule>(jsInvoker);
|
|
503
|
+
}
|
|
504
|
+
#endif
|
|
505
|
+
if (name == basalt::DesktopBlobModule::kModuleName) {
|
|
506
|
+
return std::make_shared<basalt::DesktopBlobModule>(jsInvoker);
|
|
507
|
+
}
|
|
508
|
+
if (name == basalt::DesktopFileReaderModule::kModuleName) {
|
|
509
|
+
return std::make_shared<basalt::DesktopFileReaderModule>(jsInvoker);
|
|
510
|
+
}
|
|
511
|
+
if (name == basalt::DesktopClipboardModule::kModuleName) {
|
|
512
|
+
return std::make_shared<basalt::DesktopClipboardModule>(jsInvoker);
|
|
513
|
+
}
|
|
514
|
+
if (name == basalt::DesktopVibrationModule::kModuleName) {
|
|
515
|
+
return std::make_shared<basalt::DesktopVibrationModule>(jsInvoker);
|
|
516
|
+
}
|
|
517
|
+
if (name == basalt::DesktopAlertModule::kModuleName) {
|
|
518
|
+
return std::make_shared<basalt::DesktopAlertModule>(jsInvoker);
|
|
519
|
+
}
|
|
520
|
+
if (name == basalt::DesktopLinkingModule::kModuleName) {
|
|
521
|
+
return std::make_shared<basalt::DesktopLinkingModule>(jsInvoker);
|
|
522
|
+
}
|
|
523
|
+
if (name == basalt::DesktopShareModule::kModuleName) {
|
|
524
|
+
return std::make_shared<basalt::DesktopShareModule>(jsInvoker);
|
|
525
|
+
}
|
|
526
|
+
// The native file dialogs, which React Native has no API for and which
|
|
527
|
+
// are the first thing a desktop app reaches for. See core/DialogModule.h.
|
|
528
|
+
if (name == basalt::DesktopDialogModule::kModuleName) {
|
|
529
|
+
return std::make_shared<basalt::DesktopDialogModule>(jsInvoker);
|
|
530
|
+
}
|
|
531
|
+
// The application menu, which on macOS is also what makes Cmd-C reach
|
|
532
|
+
// a text field. See core/MenuModel.h.
|
|
533
|
+
if (name == basalt::DesktopMenuModule::kModuleName) {
|
|
534
|
+
return std::make_shared<basalt::DesktopMenuModule>(jsInvoker);
|
|
535
|
+
}
|
|
536
|
+
// More than one window, which is more than one surface. See
|
|
537
|
+
// core/WindowHost.h.
|
|
538
|
+
if (name == basalt::DesktopWindowsModule::kModuleName) {
|
|
539
|
+
return std::make_shared<basalt::DesktopWindowsModule>(jsInvoker);
|
|
540
|
+
}
|
|
541
|
+
if (name == basalt::DesktopI18nManagerModule::kModuleName) {
|
|
542
|
+
return std::make_shared<basalt::DesktopI18nManagerModule>(jsInvoker);
|
|
543
|
+
}
|
|
544
|
+
if (name == basalt::DesktopAccessibilityManagerModule::kModuleName) {
|
|
545
|
+
return std::make_shared<basalt::DesktopAccessibilityManagerModule>(jsInvoker);
|
|
546
|
+
}
|
|
547
|
+
if (name == basalt::DesktopAccessibilityInfoModule::kModuleName) {
|
|
548
|
+
return std::make_shared<basalt::DesktopAccessibilityInfoModule>(jsInvoker);
|
|
549
|
+
}
|
|
550
|
+
if (name == basalt::DesktopToastModule::kModuleName) {
|
|
551
|
+
return std::make_shared<basalt::DesktopToastModule>(jsInvoker);
|
|
552
|
+
}
|
|
553
|
+
// react-native-gesture-handler's module. Offered whether or not the app
|
|
554
|
+
// has the library: it costs a name comparison, and the alternative is a
|
|
555
|
+
// host that has to be rebuilt to run an app that uses gestures.
|
|
556
|
+
if (name == basalt::DesktopGestureHandlerModule::kModuleName) {
|
|
557
|
+
return std::make_shared<basalt::DesktopGestureHandlerModule>(jsInvoker);
|
|
558
|
+
}
|
|
559
|
+
#ifdef BASALT_HAS_WORKLETS
|
|
560
|
+
if (name == basalt::DesktopWorkletsModule::kModuleName) {
|
|
561
|
+
return std::make_shared<basalt::DesktopWorkletsModule>(jsInvoker);
|
|
562
|
+
}
|
|
563
|
+
#endif
|
|
564
|
+
#ifdef BASALT_HAS_REANIMATED
|
|
565
|
+
if (name == basalt::DesktopReanimatedModule::kModuleName) {
|
|
566
|
+
return std::make_shared<basalt::DesktopReanimatedModule>(jsInvoker);
|
|
567
|
+
}
|
|
568
|
+
#endif
|
|
569
|
+
// Only outside dev mode. ReactCxxPlatform provides a real DevSettings
|
|
570
|
+
// when a dev server exists, and this provider is consulted first -- so
|
|
571
|
+
// offering ours unconditionally would shadow the one that works.
|
|
572
|
+
if (!devMode && name == basalt::DesktopDevSettingsModule::kModuleName) {
|
|
573
|
+
return std::make_shared<basalt::DesktopDevSettingsModule>(jsInvoker);
|
|
574
|
+
}
|
|
575
|
+
if (name == basalt::DesktopStatusBarModule::kModuleName) {
|
|
576
|
+
return std::make_shared<basalt::DesktopStatusBarModule>(jsInvoker);
|
|
577
|
+
}
|
|
578
|
+
// The window's title bar. This host's own rather than core's, because it
|
|
579
|
+
// needs the window; see win32/Win32WindowModule.h.
|
|
580
|
+
if (name == basalt::Win32WindowModule::kModuleName) {
|
|
581
|
+
return std::make_shared<basalt::Win32WindowModule>(jsInvoker);
|
|
582
|
+
}
|
|
583
|
+
// Only outside dev mode, for the same reason DevSettings is above.
|
|
584
|
+
//
|
|
585
|
+
// This module's scriptURL is what HMRClient registers with Metro as its
|
|
586
|
+
// entry point, and Metro resolves that URL to a *graph*. The URL
|
|
587
|
+
// synthesised here is not the one DevServerHelper actually fetched --
|
|
588
|
+
// that one carries lazy, minify, runModule and app -- so it names a
|
|
589
|
+
// graph nobody built, Metro answers GraphNotFoundError, and Fast
|
|
590
|
+
// Refresh silently never starts. ReactCxxPlatform's own SourceCode
|
|
591
|
+
// module reports the URL the bundle really came from, which is the one
|
|
592
|
+
// whose graph exists.
|
|
593
|
+
if (!devMode && name == basalt::DesktopSourceCodeModule::kModuleName) {
|
|
594
|
+
return std::make_shared<basalt::DesktopSourceCodeModule>(jsInvoker, scriptURL);
|
|
595
|
+
}
|
|
596
|
+
return nullptr;
|
|
597
|
+
});
|
|
598
|
+
return providers;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// Runs against the JavaScript runtime before the bundle is evaluated, which is
|
|
602
|
+
// the only moment early enough for what goes in here.
|
|
603
|
+
void installBindings(facebook::jsi::Runtime &runtime) {
|
|
604
|
+
// Before Expo's, and before anything else that might look a module up: this is
|
|
605
|
+
// what makes TurboModuleRegistry's first question answerable on a bridgeless
|
|
606
|
+
// runtime. See core/TurboModuleProxy.h.
|
|
607
|
+
basalt::installTurboModuleProxy(runtime);
|
|
608
|
+
basalt::installExpoRuntime(runtime);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
std::shared_ptr<const ContextContainer> makeContextContainer() {
|
|
612
|
+
auto contextContainer = std::make_shared<ContextContainer>();
|
|
613
|
+
|
|
614
|
+
// ReactHost throws without these two. Both are real implementations --
|
|
615
|
+
// curl-backed http from core/HttpClient.cpp, and React Native's own
|
|
616
|
+
// boost::beast websocket client -- and neither is about a toolkit, which is
|
|
617
|
+
// why this host gets them for free.
|
|
618
|
+
contextContainer->insert(facebook::react::HttpClientFactoryKey,
|
|
619
|
+
facebook::react::getHttpClientFactory());
|
|
620
|
+
contextContainer->insert(facebook::react::WebSocketClientFactoryKey,
|
|
621
|
+
facebook::react::getWebSocketClientFactory());
|
|
622
|
+
|
|
623
|
+
// MessageQueueThreadFactoryKey is deliberately left unset: ReactHost then
|
|
624
|
+
// installs MessageQueueThreadImpl, a real threaded queue.
|
|
625
|
+
return contextContainer;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// BASALT_DUMP_MENU: write the application menu that is actually installed to a
|
|
629
|
+
// file on the way out.
|
|
630
|
+
//
|
|
631
|
+
// Read back from the platform rather than from the model that was sent, which
|
|
632
|
+
// is the point: it says a description became a real menu -- and it is the only
|
|
633
|
+
// way an automated run can see a menu bar at all, since a menu cannot be opened
|
|
634
|
+
// without a person. Written even when it is empty, because "this platform has
|
|
635
|
+
// no menu bar" is exactly what a test on Linux is asserting.
|
|
636
|
+
void dumpMenuIfRequested() {
|
|
637
|
+
const char *path = std::getenv("BASALT_DUMP_MENU");
|
|
638
|
+
if (path == nullptr) {
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
const std::string described = basalt::describeApplicationMenu();
|
|
642
|
+
std::FILE *file = std::fopen(path, "wb");
|
|
643
|
+
if (file == nullptr) {
|
|
644
|
+
std::fprintf(stderr, "could not write the menu dump to %s\n", path);
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
std::fwrite(described.data(), 1, described.size(), file);
|
|
648
|
+
std::fclose(file);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// BASALT_DUMP_TREE: write the view tree to a file on the way out, so an
|
|
652
|
+
// automated run can assert on what React actually produced rather than on a
|
|
653
|
+
// screenshot.
|
|
654
|
+
void dumpTreeIfRequested() {
|
|
655
|
+
const char *path = std::getenv("BASALT_DUMP_TREE");
|
|
656
|
+
if (path == nullptr || gHost.main().root == nullptr) {
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
if (FILE *file = std::fopen(path, "wb")) {
|
|
660
|
+
std::string described = gHost.main().root->describeTree();
|
|
661
|
+
// Every other window, each under a header naming its surface. Appended
|
|
662
|
+
// rather than merged for the same reason the inspector is: they are
|
|
663
|
+
// separate trees on screen, and nesting one inside another would say
|
|
664
|
+
// something untrue.
|
|
665
|
+
for (const auto &other : gHost.windows) {
|
|
666
|
+
if (other->surfaceId == kSurfaceId || other->root == nullptr) {
|
|
667
|
+
continue;
|
|
668
|
+
}
|
|
669
|
+
described += "--- window " + std::to_string(static_cast<int>(other->surfaceId)) + " ---\n";
|
|
670
|
+
described += other->root->describeTree();
|
|
671
|
+
}
|
|
672
|
+
// The error inspector is a second surface with a root of its own, so it is
|
|
673
|
+
// invisible to a dump of the app's. Appended rather than merged, because
|
|
674
|
+
// the two are siblings on screen and nesting one inside the other would say
|
|
675
|
+
// something untrue about the tree. Same separator the other two hosts use.
|
|
676
|
+
if (gHost.logBoxRoot != nullptr) {
|
|
677
|
+
described += "--- LogBox ---\n";
|
|
678
|
+
described += gHost.logBoxRoot->describeTree();
|
|
679
|
+
}
|
|
680
|
+
std::fwrite(described.data(), 1, described.size(), file);
|
|
681
|
+
std::fclose(file);
|
|
682
|
+
std::fprintf(stderr, "wrote view tree to %s\n", path);
|
|
683
|
+
} else {
|
|
684
|
+
std::fprintf(stderr, "could not write %s\n", path);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// BASALT_SNAPSHOT: render the surface to a PNG on the way out.
|
|
689
|
+
//
|
|
690
|
+
// The tree dump says which mutations arrived; it says nothing about whether any
|
|
691
|
+
// of them reached the screen, and on Windows those are genuinely separate --
|
|
692
|
+
// painting only happens when something invalidates. This is offscreen rather
|
|
693
|
+
// than a screen grab, so it works on a machine with no one watching and needs
|
|
694
|
+
// no window to be visible.
|
|
695
|
+
void snapshotIfRequested() {
|
|
696
|
+
const char *path = std::getenv("BASALT_SNAPSHOT");
|
|
697
|
+
if (path == nullptr || gHost.main().root == nullptr) {
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
if (basalt::win32::writeSnapshot(*gHost.main().root, path)) {
|
|
701
|
+
std::fprintf(stderr, "wrote snapshot to %s\n", path);
|
|
702
|
+
} else {
|
|
703
|
+
std::fprintf(stderr, "could not write %s\n", path);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// ---------------------------------------------------------------------------
|
|
708
|
+
// Scripted input
|
|
709
|
+
// ---------------------------------------------------------------------------
|
|
710
|
+
|
|
711
|
+
// BASALT_TEST_TAP and BASALT_TEST_DRAG, the same two escape hatches both other
|
|
712
|
+
// hosts have and for the same reason: the input path is otherwise untestable in
|
|
713
|
+
// automation. Synthesising a real click on Windows means SendInput, which moves
|
|
714
|
+
// the actual cursor and so cannot run beside anything else on the machine --
|
|
715
|
+
// the same objection CGEvent raises on macOS.
|
|
716
|
+
//
|
|
717
|
+
// They enter at the dispatcher rather than at the window procedure, so what
|
|
718
|
+
// they prove is hit testing, emitter lookup and delivery through the event
|
|
719
|
+
// beat, and what they leave unproven is that Windows routes WM_LBUTTONDOWN to
|
|
720
|
+
// `hostProc` at all. A person clicking the window is still the only check on
|
|
721
|
+
// that half, on all three platforms.
|
|
722
|
+
struct ScriptedInput {
|
|
723
|
+
enum class Kind { Tap, Hover, Drag, Wheel, Type, Focus, Close, Quit, Drop, Key };
|
|
724
|
+
|
|
725
|
+
Kind kind{Kind::Tap};
|
|
726
|
+
double fromX{0};
|
|
727
|
+
double fromY{0};
|
|
728
|
+
double toX{0};
|
|
729
|
+
double toY{0};
|
|
730
|
+
// Wheel only: notches, positive being a turn towards the user, which scrolls
|
|
731
|
+
// content down. The sign is the one BASALT_TEST_SCROLL takes on the other
|
|
732
|
+
// hosts, not the one WM_MOUSEWHEEL uses.
|
|
733
|
+
double lines{0};
|
|
734
|
+
// Which window this is about. A tap may name one -- "x,y@3" -- because each
|
|
735
|
+
// window has its own touch dispatcher, and the app's would happily hit-test a
|
|
736
|
+
// tree that is not on screen; a Close names the window to close. Defaults to
|
|
737
|
+
// the app's own, so every spec written before windows existed still means
|
|
738
|
+
// what it did.
|
|
739
|
+
facebook::react::SurfaceId surfaceId{kSurfaceId};
|
|
740
|
+
// Tap only: which button. A secondary tap presses nothing and arrives as a
|
|
741
|
+
// pointer event, which is the whole thing BASALT_TEST_SECONDARY_TAP exists to
|
|
742
|
+
// drive. See core/PointerButtons.h.
|
|
743
|
+
basalt::PointerButton button{basalt::PointerButton::Primary};
|
|
744
|
+
// Type only. Default-initialised explicitly, so that the three kinds that do
|
|
745
|
+
// not carry text can leave it out of a designated initialiser.
|
|
746
|
+
std::string text{};
|
|
747
|
+
// Key only: the combination to press. See core/TestKeys.h.
|
|
748
|
+
basalt::KeyCombination pressed{};
|
|
749
|
+
};
|
|
750
|
+
|
|
751
|
+
// Fired from timers keyed by index, so the vector has to outlive the loop.
|
|
752
|
+
std::vector<ScriptedInput> gScriptedInput;
|
|
753
|
+
|
|
754
|
+
constexpr UINT_PTR kScriptedInputTimerBase = 200;
|
|
755
|
+
|
|
756
|
+
// Every timer id this host uses is distinct.
|
|
757
|
+
//
|
|
758
|
+
// `SetTimer` with an id that is already in use replaces the timer rather than
|
|
759
|
+
// failing, so a collision is silent and its symptom is whatever the timer it
|
|
760
|
+
// replaced was for. That happened: the spinner's repaint timer was given the
|
|
761
|
+
// same id as BASALT_QUIT_AFTER_MS's, and an app with an <ActivityIndicator> in
|
|
762
|
+
// it cancelled its own shutdown and hung. A static_assert cannot check the
|
|
763
|
+
// scripted range against the rest, so the base is held above everything else.
|
|
764
|
+
static_assert(kSecondTreeTimer != kQuitAfterTimer && kSecondTreeTimer != kSpinnerTimer &&
|
|
765
|
+
kSecondTreeTimer != kQuitFileTimer && kQuitAfterTimer != kSpinnerTimer &&
|
|
766
|
+
kQuitAfterTimer != kQuitFileTimer && kSpinnerTimer != kQuitFileTimer,
|
|
767
|
+
"two timers share an id, and SetTimer would silently replace one with the other");
|
|
768
|
+
static_assert(kScriptedInputTimerBase > kSecondTreeTimer &&
|
|
769
|
+
kScriptedInputTimerBase > kQuitAfterTimer &&
|
|
770
|
+
kScriptedInputTimerBase > kSpinnerTimer &&
|
|
771
|
+
kScriptedInputTimerBase > kQuitFileTimer,
|
|
772
|
+
"the scripted-input ids count up from the base and must start above the rest");
|
|
773
|
+
|
|
774
|
+
// The numbers in "10,20,30" -- however many there are, which is what lets one
|
|
775
|
+
// parser read both a tap's pair and a drag's four.
|
|
776
|
+
std::vector<double> parseNumbers(const std::string &spec) {
|
|
777
|
+
std::vector<double> numbers;
|
|
778
|
+
size_t start = 0;
|
|
779
|
+
while (start <= spec.size()) {
|
|
780
|
+
const size_t comma = spec.find(',', start);
|
|
781
|
+
const std::string piece =
|
|
782
|
+
spec.substr(start, comma == std::string::npos ? std::string::npos : comma - start);
|
|
783
|
+
if (!piece.empty()) {
|
|
784
|
+
numbers.push_back(std::strtod(piece.c_str(), nullptr));
|
|
785
|
+
}
|
|
786
|
+
if (comma == std::string::npos) {
|
|
787
|
+
break;
|
|
788
|
+
}
|
|
789
|
+
start = comma + 1;
|
|
790
|
+
}
|
|
791
|
+
return numbers;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
void CALLBACK fireScriptedInput(HWND hwnd, UINT, UINT_PTR id, DWORD) {
|
|
795
|
+
KillTimer(hwnd, id);
|
|
796
|
+
const size_t index = static_cast<size_t>(id - kScriptedInputTimerBase);
|
|
797
|
+
if (index >= gScriptedInput.size()) {
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
const ScriptedInput &action = gScriptedInput[index];
|
|
801
|
+
switch (action.kind) {
|
|
802
|
+
case ScriptedInput::Kind::Tap: {
|
|
803
|
+
const bool secondary = action.button != basalt::PointerButton::Primary;
|
|
804
|
+
const char *name = secondary ? "BASALT_TEST_SECONDARY_TAP" : "BASALT_TEST_TAP";
|
|
805
|
+
std::fprintf(stderr,
|
|
806
|
+
"%s: tapping (%.0f, %.0f) in window %d\n",
|
|
807
|
+
name,
|
|
808
|
+
action.fromX,
|
|
809
|
+
action.fromY,
|
|
810
|
+
static_cast<int>(action.surfaceId));
|
|
811
|
+
HostWindow *target = gHost.windowFor(action.surfaceId);
|
|
812
|
+
if (target == nullptr) {
|
|
813
|
+
std::fprintf(stderr, "%s: no window %d\n", name, static_cast<int>(action.surfaceId));
|
|
814
|
+
break;
|
|
815
|
+
}
|
|
816
|
+
if (target->touchDispatcher != nullptr) {
|
|
817
|
+
target->touchDispatcher->synthesiseTap(action.fromX, action.fromY, action.button);
|
|
818
|
+
}
|
|
819
|
+
// A real click on a <TextInput> never reaches the touch dispatcher: the
|
|
820
|
+
// peer is a child window, so USER32 routes the click to it and the
|
|
821
|
+
// control focuses itself. That is the one thing a synthesised tap cannot
|
|
822
|
+
// reproduce, so it is done here instead -- and only here, because the
|
|
823
|
+
// real path needs none of it.
|
|
824
|
+
//
|
|
825
|
+
// The primary button only: a right-click on a field does not focus it on
|
|
826
|
+
// any desktop, it asks for a menu.
|
|
827
|
+
if (!secondary && gHost.mountingManager != nullptr &&
|
|
828
|
+
gHost.mountingManager->focusTextInputAt(target->root, action.fromX, action.fromY)) {
|
|
829
|
+
std::fprintf(stderr, "BASALT_TEST_TAP: focused the field there\n");
|
|
830
|
+
}
|
|
831
|
+
break;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
case ScriptedInput::Kind::Hover:
|
|
835
|
+
std::fprintf(stderr,
|
|
836
|
+
"BASALT_TEST_HOVER: hovering (%.0f, %.0f)\n",
|
|
837
|
+
action.fromX,
|
|
838
|
+
action.fromY);
|
|
839
|
+
if (gHost.main().touchDispatcher != nullptr) {
|
|
840
|
+
gHost.main().touchDispatcher->synthesiseHover(action.fromX, action.fromY);
|
|
841
|
+
}
|
|
842
|
+
break;
|
|
843
|
+
|
|
844
|
+
case ScriptedInput::Kind::Drag:
|
|
845
|
+
std::fprintf(stderr,
|
|
846
|
+
"BASALT_TEST_DRAG: (%.0f, %.0f) -> (%.0f, %.0f)\n",
|
|
847
|
+
action.fromX,
|
|
848
|
+
action.fromY,
|
|
849
|
+
action.toX,
|
|
850
|
+
action.toY);
|
|
851
|
+
gHost.main().touchDispatcher->synthesiseDrag(
|
|
852
|
+
action.fromX, action.fromY, action.toX, action.toY, 20);
|
|
853
|
+
break;
|
|
854
|
+
|
|
855
|
+
case ScriptedInput::Kind::Wheel: {
|
|
856
|
+
// A real WM_MOUSEWHEEL through the real window procedure, unlike the tap
|
|
857
|
+
// and the drag above -- because the two things most likely to be wrong
|
|
858
|
+
// about a wheel on Windows are the ones a dispatcher-level injection
|
|
859
|
+
// would skip. Its coordinates are in screen space and its sign is
|
|
860
|
+
// inverted against a contentOffset, and either mistake produces a wheel
|
|
861
|
+
// that scrolls the wrong thing or the wrong way rather than one that does
|
|
862
|
+
// nothing. So the point is converted back out to screen coordinates here
|
|
863
|
+
// for `hostProc` to convert in again, and the notches are negated for it
|
|
864
|
+
// to negate back.
|
|
865
|
+
//
|
|
866
|
+
// Sent rather than posted: a WM_TIMER callback is already on the UI
|
|
867
|
+
// thread, so this reaches hostProc synchronously and stays ordered with
|
|
868
|
+
// the actions around it.
|
|
869
|
+
POINT screen{static_cast<LONG>(action.fromX), static_cast<LONG>(action.fromY)};
|
|
870
|
+
ClientToScreen(hwnd, &screen);
|
|
871
|
+
|
|
872
|
+
const int delta = static_cast<int>(-action.lines * WHEEL_DELTA);
|
|
873
|
+
std::fprintf(stderr,
|
|
874
|
+
"BASALT_TEST_SCROLL: %.0f lines at (%.0f, %.0f)\n",
|
|
875
|
+
action.lines,
|
|
876
|
+
action.fromX,
|
|
877
|
+
action.fromY);
|
|
878
|
+
SendMessage(hwnd,
|
|
879
|
+
WM_MOUSEWHEEL,
|
|
880
|
+
MAKEWPARAM(0, delta),
|
|
881
|
+
MAKELPARAM(static_cast<WORD>(screen.x), static_cast<WORD>(screen.y)));
|
|
882
|
+
break;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
case ScriptedInput::Kind::Key: {
|
|
886
|
+
std::fprintf(stderr, "BASALT_TEST_KEY: %s\n", action.pressed.key.c_str());
|
|
887
|
+
if (gHost.main().focusManager != nullptr) {
|
|
888
|
+
gHost.main().focusManager->deliverTestKey(action.pressed);
|
|
889
|
+
}
|
|
890
|
+
break;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
case ScriptedInput::Kind::Focus: {
|
|
894
|
+
std::fprintf(stderr, "BASALT_TEST_FOCUS: %s\n", action.text.c_str());
|
|
895
|
+
// Not a focus action, and here anyway: this is the instrument for "a key
|
|
896
|
+
// was pressed and nothing on the window has to be focused for it to
|
|
897
|
+
// arrive", which is exactly what Escape closing a <Modal> is.
|
|
898
|
+
if (action.text == "devmenu") {
|
|
899
|
+
// Also not a focus action. Same instrument for the same reason: a key
|
|
900
|
+
// that needs nothing focused to arrive. What it opens is answered by
|
|
901
|
+
// BASALT_TEST_MENU; see core/TestDialog.h.
|
|
902
|
+
basalt::showDevMenu(gHost.reactHost.get());
|
|
903
|
+
break;
|
|
904
|
+
}
|
|
905
|
+
if (action.text == "escape") {
|
|
906
|
+
if (gHost.mountingManager != nullptr) {
|
|
907
|
+
gHost.mountingManager->requestCloseTopModal();
|
|
908
|
+
}
|
|
909
|
+
InvalidateRect(hwnd, nullptr, FALSE);
|
|
910
|
+
break;
|
|
911
|
+
}
|
|
912
|
+
if (gHost.main().focusManager != nullptr) {
|
|
913
|
+
if (action.text == "tab") {
|
|
914
|
+
gHost.main().focusManager->moveFocus(true);
|
|
915
|
+
} else if (action.text == "shift-tab") {
|
|
916
|
+
gHost.main().focusManager->moveFocus(false);
|
|
917
|
+
} else if (action.text == "activate") {
|
|
918
|
+
gHost.main().focusManager->activateFocused();
|
|
919
|
+
} else {
|
|
920
|
+
std::fprintf(stderr, "BASALT_TEST_FOCUS: unknown action\n");
|
|
921
|
+
}
|
|
922
|
+
InvalidateRect(hwnd, nullptr, FALSE);
|
|
923
|
+
}
|
|
924
|
+
break;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
case ScriptedInput::Kind::Type: {
|
|
928
|
+
// Real WM_CHARs into whichever <TextInput> has focus, so what this skips
|
|
929
|
+
// is the keyboard driver and nothing above it: the EDIT's own handling,
|
|
930
|
+
// EN_CHANGE, the emitter, the event beat, React's re-render and the
|
|
931
|
+
// controlled value coming back down all run exactly as they would.
|
|
932
|
+
//
|
|
933
|
+
// Focus is a precondition rather than something this arranges, which is
|
|
934
|
+
// why a BASALT_TEST_TAP on the field is scheduled before it -- the same
|
|
935
|
+
// pairing both other hosts use.
|
|
936
|
+
const bool typed = gHost.mountingManager != nullptr &&
|
|
937
|
+
gHost.mountingManager->typeIntoFocusedTextInput(action.text);
|
|
938
|
+
std::fprintf(stderr,
|
|
939
|
+
"BASALT_TEST_TYPE: \"%s\"%s\n",
|
|
940
|
+
action.text.c_str(),
|
|
941
|
+
typed ? "" : " -- no field has focus");
|
|
942
|
+
break;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
case ScriptedInput::Kind::Drop: {
|
|
946
|
+
// A file dropped at a point, reported the way OLE would report it.
|
|
947
|
+
// Entered below the toolkit for the reason the other hosts give: a real
|
|
948
|
+
// drag needs a source outside this process. `fromX`/`fromY` carry the
|
|
949
|
+
// point and `text` the path, reusing the fields the other kinds use.
|
|
950
|
+
basalt::DragPayload payload;
|
|
951
|
+
payload.files.push_back(action.text);
|
|
952
|
+
|
|
953
|
+
const facebook::react::Tag found = basalt::dropTargetAt(
|
|
954
|
+
gHost.windows.empty() ? nullptr : gHost.main().root,
|
|
955
|
+
action.fromX,
|
|
956
|
+
action.fromY,
|
|
957
|
+
basalt::DropAcceptsFiles);
|
|
958
|
+
std::fprintf(stderr,
|
|
959
|
+
"BASALT_TEST_DROP: %s at %g,%g onto tag %d\n",
|
|
960
|
+
action.text.c_str(),
|
|
961
|
+
action.fromX,
|
|
962
|
+
action.fromY,
|
|
963
|
+
static_cast<int>(found));
|
|
964
|
+
if (found == 0) {
|
|
965
|
+
break;
|
|
966
|
+
}
|
|
967
|
+
basalt::DropEvent event;
|
|
968
|
+
event.tag = found;
|
|
969
|
+
event.phase = basalt::DropPhase::Drop;
|
|
970
|
+
event.x = action.fromX;
|
|
971
|
+
event.y = action.fromY;
|
|
972
|
+
event.payload = payload;
|
|
973
|
+
basalt::reportDrop(event);
|
|
974
|
+
break;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
case ScriptedInput::Kind::Quit: {
|
|
978
|
+
// The session ending, which is what "quit" means here: Windows has no
|
|
979
|
+
// application-level quit gesture either -- Alt+F4 is a window close,
|
|
980
|
+
// which is WM_CLOSE and useCloseRequest's business. So this sends the
|
|
981
|
+
// message a logout sends, and the handler for it is the whole of this
|
|
982
|
+
// host's half.
|
|
983
|
+
std::fprintf(stderr, "BASALT_TEST_QUIT: ending the session\n");
|
|
984
|
+
HWND main = gHost.windows.empty() ? nullptr : gHost.main().window;
|
|
985
|
+
if (main != nullptr) {
|
|
986
|
+
SendMessage(main, WM_QUERYENDSESSION, 0, ENDSESSION_CLOSEAPP);
|
|
987
|
+
}
|
|
988
|
+
break;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
case ScriptedInput::Kind::Close: {
|
|
992
|
+
// The window manager's close, not the app's: WM_CLOSE is what the caption
|
|
993
|
+
// button sends, so the host's own handling runs exactly as it would for a
|
|
994
|
+
// person. Which is the whole point -- an app closing a window and a
|
|
995
|
+
// person closing one take different paths, and only one of them can leave
|
|
996
|
+
// the host holding a record whose HWND is gone.
|
|
997
|
+
HostWindow *closing = gHost.windowFor(action.surfaceId);
|
|
998
|
+
std::fprintf(stderr,
|
|
999
|
+
"BASALT_TEST_CLOSE_WINDOW: %d%s\n",
|
|
1000
|
+
static_cast<int>(action.surfaceId),
|
|
1001
|
+
closing != nullptr ? "" : " -- no such window");
|
|
1002
|
+
if (closing != nullptr && closing->window != nullptr) {
|
|
1003
|
+
PostMessage(closing->window, WM_CLOSE, 0, 0);
|
|
1004
|
+
}
|
|
1005
|
+
break;
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
// Queues one action a second, starting a second and a half in -- late enough
|
|
1011
|
+
// that the first tree has been committed and mounted. Returns the delay after
|
|
1012
|
+
// the last one, so a drag can be scheduled behind the taps.
|
|
1013
|
+
UINT scheduleScriptedInput(const ScriptedInput &action, UINT delayMs) {
|
|
1014
|
+
gScriptedInput.push_back(action);
|
|
1015
|
+
SetTimer(gHost.main().window,
|
|
1016
|
+
kScriptedInputTimerBase + gScriptedInput.size() - 1,
|
|
1017
|
+
delayMs,
|
|
1018
|
+
fireScriptedInput);
|
|
1019
|
+
return delayMs + 1000;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
// "x,y" pairs separated by ';' -- the spelling BASALT_TEST_TAP and
|
|
1023
|
+
// BASALT_TEST_HOVER share, since a press and a hover are both just a point.
|
|
1024
|
+
UINT scheduleTestPoints(const char *spec,
|
|
1025
|
+
UINT delayMs,
|
|
1026
|
+
ScriptedInput::Kind kind,
|
|
1027
|
+
basalt::PointerButton button = basalt::PointerButton::Primary) {
|
|
1028
|
+
const std::string all(spec);
|
|
1029
|
+
size_t start = 0;
|
|
1030
|
+
while (start <= all.size()) {
|
|
1031
|
+
const size_t semicolon = all.find(';', start);
|
|
1032
|
+
const std::string point =
|
|
1033
|
+
all.substr(start, semicolon == std::string::npos ? std::string::npos : semicolon - start);
|
|
1034
|
+
// "x,y@3" names the window; without an `@` it is the app's own.
|
|
1035
|
+
facebook::react::SurfaceId surfaceId = kSurfaceId;
|
|
1036
|
+
std::string coordinates = point;
|
|
1037
|
+
if (const size_t at = point.find('@'); at != std::string::npos) {
|
|
1038
|
+
surfaceId = static_cast<facebook::react::SurfaceId>(std::atoi(point.c_str() + at + 1));
|
|
1039
|
+
coordinates = point.substr(0, at);
|
|
1040
|
+
}
|
|
1041
|
+
const std::vector<double> numbers = parseNumbers(coordinates);
|
|
1042
|
+
if (numbers.size() == 2) {
|
|
1043
|
+
delayMs = scheduleScriptedInput(ScriptedInput{.kind = kind,
|
|
1044
|
+
.fromX = numbers[0],
|
|
1045
|
+
.fromY = numbers[1],
|
|
1046
|
+
.surfaceId = surfaceId,
|
|
1047
|
+
.button = button},
|
|
1048
|
+
delayMs);
|
|
1049
|
+
}
|
|
1050
|
+
if (semicolon == std::string::npos) {
|
|
1051
|
+
break;
|
|
1052
|
+
}
|
|
1053
|
+
start = semicolon + 1;
|
|
1054
|
+
}
|
|
1055
|
+
return delayMs;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
// BASALT_TEST_DRAG: "x1,y1,x2,y2" -- one press, twenty moves and a release, for
|
|
1059
|
+
// the gestures a tap cannot reach. A pan is defined by the movement between
|
|
1060
|
+
// press and release, so a tap can never exercise one.
|
|
1061
|
+
UINT scheduleTestDrag(const char *spec, UINT delayMs) {
|
|
1062
|
+
const std::vector<double> numbers = parseNumbers(std::string(spec));
|
|
1063
|
+
if (numbers.size() != 4) {
|
|
1064
|
+
return delayMs;
|
|
1065
|
+
}
|
|
1066
|
+
return scheduleScriptedInput(ScriptedInput{.kind = ScriptedInput::Kind::Drag,
|
|
1067
|
+
.fromX = numbers[0],
|
|
1068
|
+
.fromY = numbers[1],
|
|
1069
|
+
.toX = numbers[2],
|
|
1070
|
+
.toY = numbers[3]},
|
|
1071
|
+
delayMs);
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
// BASALT_TEST_SCROLL: "x,y,lines" triples separated by ';'. Positive lines
|
|
1075
|
+
// scroll down, as a contentOffset does -- the same spelling the macOS host
|
|
1076
|
+
// takes, so one variable drives a comparison across both.
|
|
1077
|
+
UINT scheduleTestScrolls(const char *spec, UINT delayMs) {
|
|
1078
|
+
const std::string all(spec);
|
|
1079
|
+
size_t start = 0;
|
|
1080
|
+
while (start <= all.size()) {
|
|
1081
|
+
const size_t semicolon = all.find(';', start);
|
|
1082
|
+
const std::string step =
|
|
1083
|
+
all.substr(start, semicolon == std::string::npos ? std::string::npos : semicolon - start);
|
|
1084
|
+
const std::vector<double> numbers = parseNumbers(step);
|
|
1085
|
+
if (numbers.size() == 3) {
|
|
1086
|
+
delayMs = scheduleScriptedInput(ScriptedInput{.kind = ScriptedInput::Kind::Wheel,
|
|
1087
|
+
.fromX = numbers[0],
|
|
1088
|
+
.fromY = numbers[1],
|
|
1089
|
+
.lines = numbers[2]},
|
|
1090
|
+
delayMs);
|
|
1091
|
+
}
|
|
1092
|
+
if (semicolon == std::string::npos) {
|
|
1093
|
+
break;
|
|
1094
|
+
}
|
|
1095
|
+
start = semicolon + 1;
|
|
1096
|
+
}
|
|
1097
|
+
return delayMs;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
// The two escape hatches, run once, before anything is torn down.
|
|
1101
|
+
//
|
|
1102
|
+
// "Before anything" has to include the *window*, which is why this is not
|
|
1103
|
+
// simply the first thing `shutdown` does. Destroying the host window destroys
|
|
1104
|
+
// every child window with it, and a <TextInput>'s peer is one -- so a tree
|
|
1105
|
+
// dumped after that reports every field as empty and unfocused, which reads as
|
|
1106
|
+
// a broken text field and is a dead HWND being asked a question. WM_CLOSE is
|
|
1107
|
+
// where nothing has been destroyed yet.
|
|
1108
|
+
//
|
|
1109
|
+
// Idempotent, because both callers are real: a window closed by the user or by
|
|
1110
|
+
// BASALT_QUIT_AFTER_MS arrives through WM_CLOSE, and a run that fails before
|
|
1111
|
+
// the window exists only reaches `shutdown`.
|
|
1112
|
+
void captureBeforeTeardown() {
|
|
1113
|
+
static bool captured = false;
|
|
1114
|
+
if (captured) {
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
captured = true;
|
|
1118
|
+
dumpTreeIfRequested();
|
|
1119
|
+
dumpMenuIfRequested();
|
|
1120
|
+
snapshotIfRequested();
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
void shutdown() {
|
|
1124
|
+
// Before the surface stops, which tears the tree down -- and before the
|
|
1125
|
+
// window goes, which takes every text field's peer with it.
|
|
1126
|
+
captureBeforeTeardown();
|
|
1127
|
+
|
|
1128
|
+
if (gHost.choreographer != nullptr) {
|
|
1129
|
+
gHost.choreographer->detach();
|
|
1130
|
+
}
|
|
1131
|
+
// Before the host goes: work posted to a host on its way out must not run.
|
|
1132
|
+
basalt::setRuntimeRunner(nullptr);
|
|
1133
|
+
if (gHost.reactHost != nullptr) {
|
|
1134
|
+
// Surfaces must stop before the host goes away, or teardown asserts.
|
|
1135
|
+
gHost.reactHost->stopAllSurfaces();
|
|
1136
|
+
gHost.surfaceStarted = false;
|
|
1137
|
+
// Destroying the host joins the JS thread, so it has to happen while the
|
|
1138
|
+
// mounting manager it holds is still alive.
|
|
1139
|
+
gHost.reactHost.reset();
|
|
1140
|
+
}
|
|
1141
|
+
gHost.choreographer.reset();
|
|
1142
|
+
gHost.runLoopObserverManager.reset();
|
|
1143
|
+
|
|
1144
|
+
// Every window, not only the app's own -- which is what this said when there
|
|
1145
|
+
// was only ever one, and did not say when there stopped being.
|
|
1146
|
+
//
|
|
1147
|
+
// It matters most for the Direct2D target. A record left for `gHost`'s own
|
|
1148
|
+
// destructor is released at static destruction, which is after `main` has
|
|
1149
|
+
// returned and after `CoUninitialize`, and releasing a COM interface once COM
|
|
1150
|
+
// has been torn down is undefined rather than merely late. A second window
|
|
1151
|
+
// still open at quit is exactly when that happens.
|
|
1152
|
+
//
|
|
1153
|
+
// The dispatchers go before the manager they hold a raw pointer into. Nothing
|
|
1154
|
+
// can reach them by now -- the message loop has already returned -- but the
|
|
1155
|
+
// order is the part that stays true if that ever stops being so.
|
|
1156
|
+
for (const auto &window : gHost.windows) {
|
|
1157
|
+
window->focusManager.reset();
|
|
1158
|
+
window->touchDispatcher.reset();
|
|
1159
|
+
window->target.Reset();
|
|
1160
|
+
}
|
|
1161
|
+
// And the roots, which the manager made and which name nothing after it goes.
|
|
1162
|
+
gHost.windows.clear();
|
|
1163
|
+
gHost.mountingManager.reset();
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
// ---------------------------------------------------------------------------
|
|
1167
|
+
// The window
|
|
1168
|
+
// ---------------------------------------------------------------------------
|
|
1169
|
+
|
|
1170
|
+
// Starts or stops React Native's error inspector.
|
|
1171
|
+
//
|
|
1172
|
+
// It is `LogBoxInspectorContainer`, registered by AppRegistry under the name
|
|
1173
|
+
// "LogBox" exactly as an app registers its own component -- so this is a second
|
|
1174
|
+
// surface rather than an overlay this host draws, and everything in it is React
|
|
1175
|
+
// Native's own JavaScript. See core/LogBoxSurface.h.
|
|
1176
|
+
//
|
|
1177
|
+
// Input is retargeted rather than layered. On the other two desktops a view is
|
|
1178
|
+
// a widget and the toolkit routes a press to whatever is on top; here there is
|
|
1179
|
+
// nothing to route, so the dispatchers are pointed at the inspector's root
|
|
1180
|
+
// while it is up and back at the app's when it comes down.
|
|
1181
|
+
void hideLogBoxSurface() {
|
|
1182
|
+
if (gHost.reactHost == nullptr || gHost.logBoxRoot == nullptr) {
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1185
|
+
gHost.reactHost->stopSurface(kLogBoxSurfaceId);
|
|
1186
|
+
gHost.mountingManager->destroySurfaceRoot(kLogBoxSurfaceId);
|
|
1187
|
+
gHost.logBoxRoot = nullptr;
|
|
1188
|
+
if (gHost.main().touchDispatcher != nullptr) {
|
|
1189
|
+
gHost.main().touchDispatcher->setSurfaceRoot(gHost.main().root);
|
|
1190
|
+
}
|
|
1191
|
+
if (gHost.main().focusManager != nullptr) {
|
|
1192
|
+
gHost.main().focusManager->setSurfaceRoot(gHost.main().root);
|
|
1193
|
+
}
|
|
1194
|
+
if (gHost.main().window != nullptr) {
|
|
1195
|
+
InvalidateRect(gHost.main().window, nullptr, FALSE);
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
void showLogBoxSurface(const std::string &appKey) {
|
|
1200
|
+
if (gHost.reactHost == nullptr || gHost.main().root == nullptr || gHost.logBoxRoot != nullptr) {
|
|
1201
|
+
return;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
RECT client{};
|
|
1205
|
+
GetClientRect(gHost.main().window, &client);
|
|
1206
|
+
const int width = client.right - client.left;
|
|
1207
|
+
const int height = client.bottom - client.top;
|
|
1208
|
+
if (width <= 0 || height <= 0) {
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
gHost.logBoxRoot = gHost.mountingManager->createSurfaceRoot(kLogBoxSurfaceId);
|
|
1213
|
+
gHost.logBoxRoot->setFrame(0, 0, static_cast<float>(width), static_cast<float>(height));
|
|
1214
|
+
if (gHost.main().touchDispatcher != nullptr) {
|
|
1215
|
+
gHost.main().touchDispatcher->setSurfaceRoot(gHost.logBoxRoot);
|
|
1216
|
+
}
|
|
1217
|
+
if (gHost.main().focusManager != nullptr) {
|
|
1218
|
+
gHost.main().focusManager->setSurfaceRoot(gHost.logBoxRoot);
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
gHost.reactHost->startSurface(kLogBoxSurfaceId,
|
|
1222
|
+
appKey,
|
|
1223
|
+
folly::dynamic::object(),
|
|
1224
|
+
constraintsFor(width, height),
|
|
1225
|
+
layoutContextFor(gHost.scaleFactor));
|
|
1226
|
+
InvalidateRect(gHost.main().window, nullptr, FALSE);
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
LRESULT CALLBACK hostProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) {
|
|
1230
|
+
// Which window this is for. One procedure serves every window this host
|
|
1231
|
+
// makes, so a message that reached for `the` window would paint, hit-test and
|
|
1232
|
+
// resize the app's own however many are open -- which is exactly the bug
|
|
1233
|
+
// multiple windows would otherwise introduce, and it would look like a second
|
|
1234
|
+
// window that renders nothing and a first one that flickers.
|
|
1235
|
+
//
|
|
1236
|
+
// Null before the main window is in the list, which is every message
|
|
1237
|
+
// CreateWindowEx sends before it returns. Those are the ones Windows sends to
|
|
1238
|
+
// set a window up, and DefWindowProc is the right answer to all of them.
|
|
1239
|
+
HostWindow *self = gHost.windowFor(hwnd);
|
|
1240
|
+
|
|
1241
|
+
switch (message) {
|
|
1242
|
+
// The title bar. Each of these is answered only while an app has asked for
|
|
1243
|
+
// a hidden one, and falls through to Windows otherwise; see
|
|
1244
|
+
// win32/Win32TitleBar.h.
|
|
1245
|
+
case WM_NCCALCSIZE: {
|
|
1246
|
+
LRESULT result = 0;
|
|
1247
|
+
if (basalt::titleBar().handleNcCalcSize(wparam, lparam, result)) {
|
|
1248
|
+
return result;
|
|
1249
|
+
}
|
|
1250
|
+
break;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
case WM_NCHITTEST: {
|
|
1254
|
+
LRESULT result = 0;
|
|
1255
|
+
if (basalt::titleBar().handleNcHitTest(lparam, gHost.main().root, result)) {
|
|
1256
|
+
return result;
|
|
1257
|
+
}
|
|
1258
|
+
break;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
case WM_NCMOUSEMOVE:
|
|
1262
|
+
case WM_NCMOUSELEAVE:
|
|
1263
|
+
case WM_NCLBUTTONDOWN:
|
|
1264
|
+
case WM_NCLBUTTONDBLCLK:
|
|
1265
|
+
case WM_NCLBUTTONUP: {
|
|
1266
|
+
LRESULT result = 0;
|
|
1267
|
+
if (basalt::titleBar().handleNcMouse(message, wparam, result)) {
|
|
1268
|
+
return result;
|
|
1269
|
+
}
|
|
1270
|
+
break;
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
// An inactive window's caption buttons are drawn dimmer, as Windows' own are.
|
|
1274
|
+
case WM_ACTIVATE:
|
|
1275
|
+
basalt::titleBar().handleActivate(LOWORD(wparam) != WA_INACTIVE);
|
|
1276
|
+
break;
|
|
1277
|
+
|
|
1278
|
+
// Light or dark, changed in Settings while the app runs. Windows announces
|
|
1279
|
+
// it with this message and this string, and nothing on this host listened
|
|
1280
|
+
// for it -- so neither the title bar nor Appearance ever changed.
|
|
1281
|
+
case WM_SETTINGCHANGE:
|
|
1282
|
+
if (lparam != 0 &&
|
|
1283
|
+
std::wcscmp(reinterpret_cast<const wchar_t *>(lparam), L"ImmersiveColorSet") == 0) {
|
|
1284
|
+
basalt::notifyColorSchemeChanged();
|
|
1285
|
+
}
|
|
1286
|
+
break;
|
|
1287
|
+
|
|
1288
|
+
// A move changes the bounds `useWindow()` reports and nothing else, so
|
|
1289
|
+
// unlike WM_SIZE there is no surface to reconstrain -- which is exactly why
|
|
1290
|
+
// it needs its own case: WM_SIZE would never arrive, and `center()` would
|
|
1291
|
+
// look like it had done nothing.
|
|
1292
|
+
case WM_MOVE:
|
|
1293
|
+
basalt::notifyWindowBoundsChanged();
|
|
1294
|
+
return 0;
|
|
1295
|
+
|
|
1296
|
+
case WM_SIZE: {
|
|
1297
|
+
if (self == nullptr) {
|
|
1298
|
+
break;
|
|
1299
|
+
}
|
|
1300
|
+
const int width = LOWORD(lparam);
|
|
1301
|
+
const int height = HIWORD(lparam);
|
|
1302
|
+
if (self->target) {
|
|
1303
|
+
self->target->Resize(D2D1::SizeU(static_cast<UINT32>(width), static_cast<UINT32>(height)));
|
|
1304
|
+
}
|
|
1305
|
+
// The surface has to be told, or the root keeps the size it started with
|
|
1306
|
+
// while the window changes around it. This window's surface: constraining
|
|
1307
|
+
// the app's from every window would lay the first window's tree out to the
|
|
1308
|
+
// second window's size.
|
|
1309
|
+
if (gHost.surfaceStarted && width > 0 && height > 0) {
|
|
1310
|
+
self->root->setFrame(0, 0, static_cast<float>(width), static_cast<float>(height));
|
|
1311
|
+
gHost.reactHost->setSurfaceConstraints(
|
|
1312
|
+
self->surfaceId, constraintsFor(width, height), layoutContextFor(gHost.scaleFactor));
|
|
1313
|
+
// A <Modal> is sized from its own shadow-node state rather than from a
|
|
1314
|
+
// style, and React Native's C++ platform answers "what size is the
|
|
1315
|
+
// screen" with zero. Told here, where the window's size is already
|
|
1316
|
+
// being handed to Fabric.
|
|
1317
|
+
gHost.mountingManager->setSurfaceSize(static_cast<float>(width), static_cast<float>(height));
|
|
1318
|
+
// The window moved or was resized, which `useWindow()` hands an app as live
|
|
1319
|
+
// bounds. Here rather than in a watcher of its own: this is already the one
|
|
1320
|
+
// place that learns it.
|
|
1321
|
+
basalt::notifyWindowBoundsChanged();
|
|
1322
|
+
// The inspector covers the main window, so it resizes with it -- and
|
|
1323
|
+
// only with it: an error is about the app rather than about a window.
|
|
1324
|
+
if (gHost.logBoxRoot != nullptr && self->surfaceId == kSurfaceId) {
|
|
1325
|
+
gHost.logBoxRoot->setFrame(0, 0, static_cast<float>(width), static_cast<float>(height));
|
|
1326
|
+
gHost.reactHost->setSurfaceConstraints(
|
|
1327
|
+
kLogBoxSurfaceId, constraintsFor(width, height), layoutContextFor(gHost.scaleFactor));
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
// A maximise moves a hidden title bar's caption, and nothing else says so.
|
|
1331
|
+
basalt::titleBar().refreshMetrics();
|
|
1332
|
+
return 0;
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
case WM_DPICHANGED: {
|
|
1336
|
+
// Yoga rounds to physical pixels, so telling it the wrong factor puts
|
|
1337
|
+
// half-pixel seams between adjacent views. The high word is the new DPI;
|
|
1338
|
+
// 96 is one.
|
|
1339
|
+
gHost.scaleFactor = static_cast<int>(HIWORD(wparam) / 96);
|
|
1340
|
+
if (gHost.scaleFactor < 1) {
|
|
1341
|
+
gHost.scaleFactor = 1;
|
|
1342
|
+
}
|
|
1343
|
+
// Windows also hands back the rectangle the window should move to, and
|
|
1344
|
+
// ignoring it leaves the window the wrong size on the new monitor.
|
|
1345
|
+
if (const auto *suggested = reinterpret_cast<const RECT *>(lparam)) {
|
|
1346
|
+
SetWindowPos(hwnd,
|
|
1347
|
+
nullptr,
|
|
1348
|
+
suggested->left,
|
|
1349
|
+
suggested->top,
|
|
1350
|
+
suggested->right - suggested->left,
|
|
1351
|
+
suggested->bottom - suggested->top,
|
|
1352
|
+
SWP_NOZORDER | SWP_NOACTIVATE);
|
|
1353
|
+
}
|
|
1354
|
+
// The caption is sized in device-independent pixels.
|
|
1355
|
+
basalt::titleBar().refreshMetrics();
|
|
1356
|
+
return 0;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
// Mouse coordinates arrive in client-area pixels, which are the surface
|
|
1360
|
+
// root's own coordinates: WM_SIZE sizes the root to the client rectangle,
|
|
1361
|
+
// so the two spaces are the same one and nothing has to be converted.
|
|
1362
|
+
// Each button is its own message on Windows, which is why there are six of
|
|
1363
|
+
// these rather than one with a parameter. Only the primary one captures or
|
|
1364
|
+
// presses anything; see core/PointerButtons.h.
|
|
1365
|
+
case WM_LBUTTONDOWN:
|
|
1366
|
+
case WM_RBUTTONDOWN:
|
|
1367
|
+
case WM_MBUTTONDOWN: {
|
|
1368
|
+
if (self == nullptr || self->touchDispatcher == nullptr) {
|
|
1369
|
+
break;
|
|
1370
|
+
}
|
|
1371
|
+
// A new press: the drag question is open again.
|
|
1372
|
+
self->dragAsked = false;
|
|
1373
|
+
const basalt::PointerButton button = message == WM_LBUTTONDOWN
|
|
1374
|
+
? basalt::PointerButton::Primary
|
|
1375
|
+
: (message == WM_RBUTTONDOWN ? basalt::PointerButton::Secondary
|
|
1376
|
+
: basalt::PointerButton::Middle);
|
|
1377
|
+
// Capture, or a drag that leaves the window stops being reported and the
|
|
1378
|
+
// release never arrives -- which leaves the responder system believing a
|
|
1379
|
+
// finger is still down and swallows every press after it. AppKit and GTK
|
|
1380
|
+
// route a drag back to the view that took the press for free; here it has
|
|
1381
|
+
// to be asked for.
|
|
1382
|
+
//
|
|
1383
|
+
// Only for the button that can drag. A secondary click produces a pointer
|
|
1384
|
+
// event and is over; capturing the mouse for it would take the pointer
|
|
1385
|
+
// away from everything else for no reason.
|
|
1386
|
+
if (basalt::isPressButton(button)) {
|
|
1387
|
+
SetCapture(hwnd);
|
|
1388
|
+
}
|
|
1389
|
+
self->touchDispatcher->dispatchTouchStart(
|
|
1390
|
+
GET_X_LPARAM(lparam), GET_Y_LPARAM(lparam), button);
|
|
1391
|
+
return 0;
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
case WM_MOUSEMOVE:
|
|
1395
|
+
basalt::titleBar().clearHover();
|
|
1396
|
+
if (self != nullptr && self->touchDispatcher != nullptr) {
|
|
1397
|
+
// The touch model has no place for motion with no button down, and the
|
|
1398
|
+
// dispatcher drops it; the check here is only to keep an idle mouse
|
|
1399
|
+
// from walking the view tree sixty times a second.
|
|
1400
|
+
if (self->touchDispatcher->isDown()) {
|
|
1401
|
+
// A drag out of the window, if the press began on a marked view.
|
|
1402
|
+
// Asked once per press and here rather than on the button going
|
|
1403
|
+
// down: a press is not a drag until it moves.
|
|
1404
|
+
//
|
|
1405
|
+
// DoDragDrop runs its own modal loop until the drop, so the touch
|
|
1406
|
+
// that started this never ends -- it is cancelled rather than left
|
|
1407
|
+
// hanging, the same arrangement the AppKit host uses.
|
|
1408
|
+
if (!self->dragAsked) {
|
|
1409
|
+
self->dragAsked = true;
|
|
1410
|
+
if (basalt::beginDragIfMarked(self->root, GET_X_LPARAM(lparam), GET_Y_LPARAM(lparam))) {
|
|
1411
|
+
self->touchDispatcher->dispatchTouchCancel();
|
|
1412
|
+
return 0;
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
self->touchDispatcher->dispatchTouchMove(GET_X_LPARAM(lparam), GET_Y_LPARAM(lparam));
|
|
1416
|
+
} else {
|
|
1417
|
+
// Hover, which is a different question about the same message. Views
|
|
1418
|
+
// that listen for none of it cost a hit test and nothing more; see
|
|
1419
|
+
// core/HoverTracker.h.
|
|
1420
|
+
self->touchDispatcher->dispatchHover(GET_X_LPARAM(lparam), GET_Y_LPARAM(lparam));
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
// Without this the cursor leaving the window is silent, and whatever it
|
|
1424
|
+
// was over stays hovered for good. GTK's motion controller and AppKit's
|
|
1425
|
+
// tracking area both report an exit on their own; Windows has to be asked,
|
|
1426
|
+
// once per entry, and disarms itself when it fires.
|
|
1427
|
+
if (!gHost.trackingMouseLeave) {
|
|
1428
|
+
TRACKMOUSEEVENT tracking{};
|
|
1429
|
+
tracking.cbSize = sizeof(tracking);
|
|
1430
|
+
tracking.dwFlags = TME_LEAVE;
|
|
1431
|
+
tracking.hwndTrack = hwnd;
|
|
1432
|
+
gHost.trackingMouseLeave = TrackMouseEvent(&tracking) != FALSE;
|
|
1433
|
+
}
|
|
1434
|
+
return 0;
|
|
1435
|
+
|
|
1436
|
+
// Tab, Enter and space. A <TextInput>'s peer is a real child window and
|
|
1437
|
+
// takes its keys directly, so anything reaching here is meant for the
|
|
1438
|
+
// painted views -- which have no window and so no focus of Windows' own.
|
|
1439
|
+
case WM_KEYDOWN:
|
|
1440
|
+
// Ctrl+D opens React Native's developer menu, which is the shortcut it
|
|
1441
|
+
// uses on a simulator and the closest thing a desktop has to a phone's
|
|
1442
|
+
// shake. Nothing in the window need have focus for it, which is why it is
|
|
1443
|
+
// answered here rather than by the focus manager.
|
|
1444
|
+
if (wparam == 'D' && (GetKeyState(VK_CONTROL) & 0x8000) != 0 && gHost.devMode &&
|
|
1445
|
+
gHost.reactHost != nullptr) {
|
|
1446
|
+
basalt::showDevMenu(gHost.reactHost.get());
|
|
1447
|
+
return 0;
|
|
1448
|
+
}
|
|
1449
|
+
// An app's own declared shortcuts, before Escape and before Tab: an app
|
|
1450
|
+
// that binds Escape means it. After Ctrl+D above, deliberately -- the
|
|
1451
|
+
// developer menu is not an app's to take, and an app that bound Ctrl+D
|
|
1452
|
+
// would otherwise make its own host undebuggable.
|
|
1453
|
+
if (gHost.main().focusManager != nullptr &&
|
|
1454
|
+
gHost.main().focusManager->handleDeclaredKey(static_cast<unsigned int>(wparam))) {
|
|
1455
|
+
return 0;
|
|
1456
|
+
}
|
|
1457
|
+
// Escape closes the topmost <Modal> -- or rather, asks the app to. React
|
|
1458
|
+
// Native's `onRequestClose` is the hardware back button on Android and
|
|
1459
|
+
// the swipe-down on iOS; on a desktop it is Escape, and a modal the app
|
|
1460
|
+
// does not close in response stays up, which is deliberate.
|
|
1461
|
+
if (wparam == VK_ESCAPE && gHost.mountingManager != nullptr &&
|
|
1462
|
+
gHost.mountingManager->requestCloseTopModal()) {
|
|
1463
|
+
return 0;
|
|
1464
|
+
}
|
|
1465
|
+
if (gHost.main().focusManager != nullptr &&
|
|
1466
|
+
gHost.main().focusManager->handleKeyDown(static_cast<unsigned int>(wparam))) {
|
|
1467
|
+
InvalidateRect(hwnd, nullptr, FALSE);
|
|
1468
|
+
return 0;
|
|
1469
|
+
}
|
|
1470
|
+
break;
|
|
1471
|
+
|
|
1472
|
+
case WM_MOUSELEAVE:
|
|
1473
|
+
gHost.trackingMouseLeave = false;
|
|
1474
|
+
basalt::titleBar().clearHover();
|
|
1475
|
+
if (gHost.main().touchDispatcher != nullptr) {
|
|
1476
|
+
gHost.main().touchDispatcher->dispatchHoverLeave();
|
|
1477
|
+
}
|
|
1478
|
+
return 0;
|
|
1479
|
+
|
|
1480
|
+
case WM_LBUTTONUP:
|
|
1481
|
+
case WM_RBUTTONUP:
|
|
1482
|
+
case WM_MBUTTONUP: {
|
|
1483
|
+
// `self`, not `gHost.main()`. This reached for the app's own window even
|
|
1484
|
+
// when the message was for another one, so a release in a second window
|
|
1485
|
+
// ended a touch in the first -- and left the second believing a finger
|
|
1486
|
+
// was still down, which swallows every press after it.
|
|
1487
|
+
if (self == nullptr || self->touchDispatcher == nullptr) {
|
|
1488
|
+
break;
|
|
1489
|
+
}
|
|
1490
|
+
const basalt::PointerButton button = message == WM_LBUTTONUP
|
|
1491
|
+
? basalt::PointerButton::Primary
|
|
1492
|
+
: (message == WM_RBUTTONUP ? basalt::PointerButton::Secondary
|
|
1493
|
+
: basalt::PointerButton::Middle);
|
|
1494
|
+
// The end first: ReleaseCapture sends WM_CAPTURECHANGED synchronously,
|
|
1495
|
+
// and that is a cancel. Releasing first would turn every ordinary click
|
|
1496
|
+
// into a cancelled touch, which is a press that never fires.
|
|
1497
|
+
self->touchDispatcher->dispatchTouchEnd(
|
|
1498
|
+
GET_X_LPARAM(lparam), GET_Y_LPARAM(lparam), button);
|
|
1499
|
+
if (basalt::isPressButton(button) && GetCapture() == hwnd) {
|
|
1500
|
+
ReleaseCapture();
|
|
1501
|
+
}
|
|
1502
|
+
return 0;
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
// The wheel. Unlike every other mouse message, its coordinates are in
|
|
1506
|
+
// *screen* space -- the message is sent to the focused window rather than
|
|
1507
|
+
// to the one under the pointer, so a client-relative position would be
|
|
1508
|
+
// meaningless. Forgetting ScreenToClient gives a wheel that scrolls the
|
|
1509
|
+
// wrong list, or nothing, depending on where the window is on the desktop.
|
|
1510
|
+
case WM_MOUSEWHEEL:
|
|
1511
|
+
case WM_MOUSEHWHEEL: {
|
|
1512
|
+
if (gHost.mountingManager == nullptr || gHost.main().root == nullptr) {
|
|
1513
|
+
break;
|
|
1514
|
+
}
|
|
1515
|
+
POINT point{GET_X_LPARAM(lparam), GET_Y_LPARAM(lparam)};
|
|
1516
|
+
ScreenToClient(hwnd, &point);
|
|
1517
|
+
|
|
1518
|
+
const double notches =
|
|
1519
|
+
static_cast<double>(GET_WHEEL_DELTA_WPARAM(wparam)) / static_cast<double>(WHEEL_DELTA);
|
|
1520
|
+
const double step = notches * basalt::Win32ScrollViewManager::kWheelStepPixels;
|
|
1521
|
+
|
|
1522
|
+
// Both axes are inverted against React Native's, and for different
|
|
1523
|
+
// reasons. A positive WM_MOUSEWHEEL delta is a turn away from the user,
|
|
1524
|
+
// which moves content down and so *reduces* contentOffset; a positive
|
|
1525
|
+
// WM_MOUSEHWHEEL delta is a tilt to the right, which is the direction
|
|
1526
|
+
// contentOffset already grows in. Getting either wrong gives a list that
|
|
1527
|
+
// scrolls backwards, which is the kind of bug nobody reports as a bug.
|
|
1528
|
+
const double dx = message == WM_MOUSEHWHEEL ? step : 0.0;
|
|
1529
|
+
const double dy = message == WM_MOUSEWHEEL ? -step : 0.0;
|
|
1530
|
+
|
|
1531
|
+
if (gHost.mountingManager->scrollAt(gHost.main().root, point.x, point.y, dx, dy)) {
|
|
1532
|
+
// Nothing else asks for this: a scroll changes no view's frame, so no
|
|
1533
|
+
// transaction is mounted and setOnDidMount never fires.
|
|
1534
|
+
syncPeersAndRepaint();
|
|
1535
|
+
return 0;
|
|
1536
|
+
}
|
|
1537
|
+
break;
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
// A <TextInput>'s peer is a real EDIT control, and an EDIT reports to its
|
|
1541
|
+
// parent rather than to itself. EN_CHANGE, EN_SETFOCUS and EN_KILLFOCUS all
|
|
1542
|
+
// arrive here, which is why the mounting manager has a method for them at
|
|
1543
|
+
// all: on GTK and AppKit the widget is the view and the toolkit routes this
|
|
1544
|
+
// without a host in the middle.
|
|
1545
|
+
case WM_COMMAND:
|
|
1546
|
+
// The application menu, first: its items arrive here with a null lParam,
|
|
1547
|
+
// which is how a menu command is told from a control's notification. See
|
|
1548
|
+
// win32/Win32MenuBar.h.
|
|
1549
|
+
if (lparam == 0 && HIWORD(wparam) == 0 &&
|
|
1550
|
+
basalt::win32::handleMenuCommand(static_cast<unsigned int>(LOWORD(wparam)))) {
|
|
1551
|
+
return 0;
|
|
1552
|
+
}
|
|
1553
|
+
// A field taking focus is the other half of the focus rule: the peer is a
|
|
1554
|
+
// real window and holds real Win32 focus, so whichever painted view was
|
|
1555
|
+
// wearing the ring has to give it up. Only one of the two kinds of focus
|
|
1556
|
+
// can be true at a time.
|
|
1557
|
+
if (HIWORD(wparam) == EN_SETFOCUS && gHost.main().focusManager != nullptr) {
|
|
1558
|
+
gHost.main().focusManager->textInputTookFocus();
|
|
1559
|
+
InvalidateRect(hwnd, nullptr, FALSE);
|
|
1560
|
+
}
|
|
1561
|
+
if (gHost.mountingManager != nullptr &&
|
|
1562
|
+
gHost.mountingManager->handleControlCommand(wparam, lparam)) {
|
|
1563
|
+
return 0;
|
|
1564
|
+
}
|
|
1565
|
+
break;
|
|
1566
|
+
|
|
1567
|
+
// And an EDIT asks its parent what colours to use. Without an answer it
|
|
1568
|
+
// draws in the system's, which have nothing to do with the `style` the
|
|
1569
|
+
// component was given -- on a dark field that is dark text on dark.
|
|
1570
|
+
// WM_CTLCOLORSTATIC as well, because a read-only EDIT asks with that one.
|
|
1571
|
+
case WM_CTLCOLOREDIT:
|
|
1572
|
+
case WM_CTLCOLORSTATIC:
|
|
1573
|
+
if (gHost.mountingManager != nullptr) {
|
|
1574
|
+
if (HBRUSH brush = gHost.mountingManager->controlColor(
|
|
1575
|
+
reinterpret_cast<HDC>(wparam), reinterpret_cast<HWND>(lparam))) {
|
|
1576
|
+
return reinterpret_cast<LRESULT>(brush);
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
break;
|
|
1580
|
+
|
|
1581
|
+
case WM_CAPTURECHANGED:
|
|
1582
|
+
// Capture taken away by something else -- a modal dialog, Alt+Tab, the
|
|
1583
|
+
// debugger. The release will never arrive, so the touch has to be
|
|
1584
|
+
// cancelled here or the responder system waits for a finger that is gone.
|
|
1585
|
+
if (gHost.main().touchDispatcher != nullptr) {
|
|
1586
|
+
gHost.main().touchDispatcher->dispatchTouchCancel();
|
|
1587
|
+
}
|
|
1588
|
+
return 0;
|
|
1589
|
+
|
|
1590
|
+
case WM_TIMER:
|
|
1591
|
+
if (wparam == kSpinnerTimer) {
|
|
1592
|
+
InvalidateRect(hwnd, nullptr, FALSE);
|
|
1593
|
+
return 0;
|
|
1594
|
+
}
|
|
1595
|
+
if (wparam == kSecondTreeTimer) {
|
|
1596
|
+
KillTimer(hwnd, kSecondTreeTimer);
|
|
1597
|
+
std::fprintf(stderr, "--- committing tree 2 from JS ---\n");
|
|
1598
|
+
callRenderFunction(2);
|
|
1599
|
+
}
|
|
1600
|
+
return 0;
|
|
1601
|
+
|
|
1602
|
+
case WM_PAINT: {
|
|
1603
|
+
PAINTSTRUCT paint{};
|
|
1604
|
+
BeginPaint(hwnd, &paint);
|
|
1605
|
+
// Into this window's own target: an ID2D1HwndRenderTarget belongs to one
|
|
1606
|
+
// HWND, and painting a second window through the first one's would draw
|
|
1607
|
+
// into the first.
|
|
1608
|
+
if (self != nullptr && ensureTarget(self) && self->root != nullptr) {
|
|
1609
|
+
self->target->BeginDraw();
|
|
1610
|
+
self->target->Clear(D2D1::ColorF(D2D1::ColorF::Black));
|
|
1611
|
+
self->target->SetTransform(D2D1::Matrix3x2F::Identity());
|
|
1612
|
+
self->root->paint(self->target.Get());
|
|
1613
|
+
// The error inspector, over the app, in the main window. A second
|
|
1614
|
+
// surface rather than anything this host draws; see
|
|
1615
|
+
// core/LogBoxSurface.h.
|
|
1616
|
+
if (gHost.logBoxRoot != nullptr && self->surfaceId == kSurfaceId) {
|
|
1617
|
+
self->target->SetTransform(D2D1::Matrix3x2F::Identity());
|
|
1618
|
+
gHost.logBoxRoot->paint(self->target.Get());
|
|
1619
|
+
}
|
|
1620
|
+
// A hidden title bar's caption buttons, over the app's own content.
|
|
1621
|
+
// The main window's: the title bar is a process-wide seam.
|
|
1622
|
+
if (self->surfaceId == kSurfaceId) {
|
|
1623
|
+
basalt::titleBar().paintButtons(self->target.Get());
|
|
1624
|
+
}
|
|
1625
|
+
// A lost device is reported here and nowhere else. Dropping the target
|
|
1626
|
+
// is the whole recovery: the next WM_PAINT rebuilds it.
|
|
1627
|
+
if (self->target->EndDraw() == D2DERR_RECREATE_TARGET) {
|
|
1628
|
+
self->target.Reset();
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
EndPaint(hwnd, &paint);
|
|
1632
|
+
return 0;
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
// How big this window may be. Windows asks rather than being told, which is
|
|
1636
|
+
// why the limits live in core and are read here: there is nowhere to have
|
|
1637
|
+
// passed them in. See core/WindowControl.h.
|
|
1638
|
+
//
|
|
1639
|
+
// The numbers are the *frame*, and an app's are its content -- so each is
|
|
1640
|
+
// adjusted outwards by whatever the border and caption cost, the same
|
|
1641
|
+
// correction `setWindowSize` makes in the other direction.
|
|
1642
|
+
case WM_GETMINMAXINFO: {
|
|
1643
|
+
// The app's own window only. The limits are one set of numbers for one
|
|
1644
|
+
// window, because `useWindow()` is about the window an app is in --
|
|
1645
|
+
// answering this for every window would make Windows the one host where a
|
|
1646
|
+
// limit set for one applied to all of them. Per-window geometry is its
|
|
1647
|
+
// own piece of work and is in docs/BACKLOG.md.
|
|
1648
|
+
if (self != nullptr && self->surfaceId != kSurfaceId) {
|
|
1649
|
+
break;
|
|
1650
|
+
}
|
|
1651
|
+
const basalt::WindowSizeLimits limits = basalt::windowSizeLimits();
|
|
1652
|
+
if (limits.minWidth <= 0.0 && limits.minHeight <= 0.0 && limits.maxWidth <= 0.0 &&
|
|
1653
|
+
limits.maxHeight <= 0.0) {
|
|
1654
|
+
break;
|
|
1655
|
+
}
|
|
1656
|
+
RECT frame{0, 0, 0, 0};
|
|
1657
|
+
AdjustWindowRectEx(&frame,
|
|
1658
|
+
static_cast<DWORD>(GetWindowLongW(hwnd, GWL_STYLE)),
|
|
1659
|
+
FALSE,
|
|
1660
|
+
static_cast<DWORD>(GetWindowLongW(hwnd, GWL_EXSTYLE)));
|
|
1661
|
+
const LONG extraWidth = frame.right - frame.left;
|
|
1662
|
+
const LONG extraHeight = frame.bottom - frame.top;
|
|
1663
|
+
|
|
1664
|
+
auto *bounds = reinterpret_cast<MINMAXINFO *>(lparam);
|
|
1665
|
+
if (limits.minWidth > 0.0) {
|
|
1666
|
+
bounds->ptMinTrackSize.x = static_cast<LONG>(limits.minWidth) + extraWidth;
|
|
1667
|
+
}
|
|
1668
|
+
if (limits.minHeight > 0.0) {
|
|
1669
|
+
bounds->ptMinTrackSize.y = static_cast<LONG>(limits.minHeight) + extraHeight;
|
|
1670
|
+
}
|
|
1671
|
+
if (limits.maxWidth > 0.0) {
|
|
1672
|
+
bounds->ptMaxTrackSize.x = static_cast<LONG>(limits.maxWidth) + extraWidth;
|
|
1673
|
+
}
|
|
1674
|
+
if (limits.maxHeight > 0.0) {
|
|
1675
|
+
bounds->ptMaxTrackSize.y = static_cast<LONG>(limits.maxHeight) + extraHeight;
|
|
1676
|
+
}
|
|
1677
|
+
return 0;
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
// The session is ending -- logout, shutdown, reboot -- which is the only
|
|
1681
|
+
// thing on Windows that corresponds to macOS's Cmd-Q. There is no
|
|
1682
|
+
// application-level quit gesture to intercept: Alt+F4 is a window close,
|
|
1683
|
+
// which arrives as WM_CLOSE below and is useCloseRequest's business.
|
|
1684
|
+
//
|
|
1685
|
+
// FALSE refuses the session end, which is what an app with unsaved work
|
|
1686
|
+
// wants and is the whole of this host's half. Windows may end the session
|
|
1687
|
+
// anyway -- a forced shutdown does not take no for an answer, and nothing
|
|
1688
|
+
// here can change that -- so what this buys is the ordinary case, which is
|
|
1689
|
+
// the one an app can act on.
|
|
1690
|
+
// A monitor plugged in, unplugged, or a resolution change. Not handled
|
|
1691
|
+
// -- DefWindowProc still gets it -- because this is a notification and
|
|
1692
|
+
// nothing here is answering it.
|
|
1693
|
+
case WM_DISPLAYCHANGE:
|
|
1694
|
+
basalt::notifyDisplaysChanged();
|
|
1695
|
+
break;
|
|
1696
|
+
|
|
1697
|
+
case WM_QUERYENDSESSION:
|
|
1698
|
+
if (basalt::hostQuitIntercepted()) {
|
|
1699
|
+
basalt::hostQuitRequested();
|
|
1700
|
+
return FALSE;
|
|
1701
|
+
}
|
|
1702
|
+
return TRUE;
|
|
1703
|
+
|
|
1704
|
+
// Before DestroyWindow, which takes every <TextInput>'s peer with it. See
|
|
1705
|
+
// captureBeforeTeardown.
|
|
1706
|
+
//
|
|
1707
|
+
// The app's own window only. A second window closing is not teardown, and
|
|
1708
|
+
// capturing there would be worse than pointless: `captureBeforeTeardown`
|
|
1709
|
+
// latches, so the dump taken as the window went would be the one the run
|
|
1710
|
+
// reported -- showing the window still open, because it is, and silencing
|
|
1711
|
+
// the dump at shutdown that was meant to show it gone.
|
|
1712
|
+
case WM_CLOSE:
|
|
1713
|
+
// Somebody is trying to close this window: its caption button, Alt+F4, or
|
|
1714
|
+
// the system menu. WM_CLOSE is the attempt rather than the act -- nothing
|
|
1715
|
+
// is destroyed until DestroyWindow below -- which is what makes it the
|
|
1716
|
+
// place a window that asked to be asked first can refuse.
|
|
1717
|
+
//
|
|
1718
|
+
// Every window, including the app's own: an app that wants to ask "are
|
|
1719
|
+
// you sure" wants to ask hardest about the one it is running in. What
|
|
1720
|
+
// happens next is its decision, and if that is "go ahead" it calls
|
|
1721
|
+
// close() itself.
|
|
1722
|
+
if (self != nullptr && basalt::hostWindowCloseIntercepted(self->surfaceId)) {
|
|
1723
|
+
basalt::hostWindowCloseRequested(self->surfaceId);
|
|
1724
|
+
return 0;
|
|
1725
|
+
}
|
|
1726
|
+
if (self == nullptr || self->surfaceId == kSurfaceId) {
|
|
1727
|
+
captureBeforeTeardown();
|
|
1728
|
+
}
|
|
1729
|
+
DestroyWindow(hwnd);
|
|
1730
|
+
return 0;
|
|
1731
|
+
|
|
1732
|
+
case WM_DESTROY:
|
|
1733
|
+
// Only the app's own window ends the process. A second window closing is
|
|
1734
|
+
// an app closing a window, and quitting on it would mean a preview panel
|
|
1735
|
+
// taking the whole application with it.
|
|
1736
|
+
if (self == nullptr || self->surfaceId == kSurfaceId) {
|
|
1737
|
+
PostQuitMessage(0);
|
|
1738
|
+
return 0;
|
|
1739
|
+
}
|
|
1740
|
+
// The app is already taking this window down; `closeHostWindow` is
|
|
1741
|
+
// mid-teardown and everything below is its job. See `closingFromApp`.
|
|
1742
|
+
if (self->closingFromApp) {
|
|
1743
|
+
return 0;
|
|
1744
|
+
}
|
|
1745
|
+
// Closed by the person rather than by the app: its own close button, or
|
|
1746
|
+
// Alt+F4. Without this the host keeps a record whose HWND is gone -- a
|
|
1747
|
+
// dangling handle rather than a stale flag -- and the `<Window>` that
|
|
1748
|
+
// opened it goes on believing it is open.
|
|
1749
|
+
//
|
|
1750
|
+
// The window is already being destroyed, so this cannot go through
|
|
1751
|
+
// closeHostWindow's teardown; what it does instead is stop the surface
|
|
1752
|
+
// and drop the record, which is the same work in the order this message
|
|
1753
|
+
// leaves available.
|
|
1754
|
+
{
|
|
1755
|
+
const facebook::react::SurfaceId closing = self->surfaceId;
|
|
1756
|
+
basalt::hostWindowClosed(closing);
|
|
1757
|
+
if (gHost.reactHost != nullptr) {
|
|
1758
|
+
gHost.reactHost->stopSurface(closing);
|
|
1759
|
+
}
|
|
1760
|
+
self->focusManager.reset();
|
|
1761
|
+
self->touchDispatcher.reset();
|
|
1762
|
+
self->target.Reset();
|
|
1763
|
+
self->window = nullptr;
|
|
1764
|
+
// The root outlives this message: the unmount mutations the stop
|
|
1765
|
+
// produces are still to come, and they name views in it. Dropped from
|
|
1766
|
+
// a round trip later, which is ordered behind them.
|
|
1767
|
+
if (gHost.reactHost != nullptr) {
|
|
1768
|
+
gHost.reactHost->runOnRuntimeScheduler([closing](facebook::jsi::Runtime &) {
|
|
1769
|
+
basalt::postToUiThread([closing] {
|
|
1770
|
+
HostWindow *going = gHost.windowFor(closing);
|
|
1771
|
+
if (going == nullptr) {
|
|
1772
|
+
return;
|
|
1773
|
+
}
|
|
1774
|
+
gHost.mountingManager->destroySurfaceRoot(closing);
|
|
1775
|
+
for (auto it = gHost.windows.begin(); it != gHost.windows.end(); ++it) {
|
|
1776
|
+
if (it->get() == going) {
|
|
1777
|
+
gHost.windows.erase(it);
|
|
1778
|
+
break;
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
});
|
|
1782
|
+
});
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
return 0;
|
|
1786
|
+
|
|
1787
|
+
default:
|
|
1788
|
+
break;
|
|
1789
|
+
}
|
|
1790
|
+
return DefWindowProc(hwnd, message, wparam, lparam);
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
// Makes a window, a surface root for it, and the input that drives them.
|
|
1794
|
+
//
|
|
1795
|
+
// Called for the main window and for every one an app opens afterwards, which
|
|
1796
|
+
// is the point: the second window is not a special case of the first, it is the
|
|
1797
|
+
// same function with a different surface id. See core/WindowHost.h.
|
|
1798
|
+
//
|
|
1799
|
+
// The window is not shown and the surface is not started here; both are the
|
|
1800
|
+
// caller's business.
|
|
1801
|
+
HostWindow *createHostWindow(facebook::react::SurfaceId surfaceId,
|
|
1802
|
+
const wchar_t *title,
|
|
1803
|
+
int width,
|
|
1804
|
+
int height) {
|
|
1805
|
+
const HINSTANCE instance = GetModuleHandle(nullptr);
|
|
1806
|
+
|
|
1807
|
+
// Registered once for the process. A second call with the same name fails
|
|
1808
|
+
// with ERROR_CLASS_ALREADY_EXISTS, which is not an error here -- the class is
|
|
1809
|
+
// exactly what a second window wants.
|
|
1810
|
+
static bool classRegistered = false;
|
|
1811
|
+
if (!classRegistered) {
|
|
1812
|
+
WNDCLASSEX windowClass{};
|
|
1813
|
+
windowClass.cbSize = sizeof(windowClass);
|
|
1814
|
+
windowClass.style = CS_HREDRAW | CS_VREDRAW;
|
|
1815
|
+
windowClass.lpfnWndProc = hostProc;
|
|
1816
|
+
windowClass.hInstance = instance;
|
|
1817
|
+
windowClass.hCursor = LoadCursor(nullptr, IDC_ARROW);
|
|
1818
|
+
windowClass.lpszClassName = L"BasaltHost";
|
|
1819
|
+
if (RegisterClassEx(&windowClass) == 0) {
|
|
1820
|
+
return nullptr;
|
|
1821
|
+
}
|
|
1822
|
+
classRegistered = true;
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
// WS_CLIPCHILDREN so that the Direct2D paint excludes any <TextInput> peer.
|
|
1826
|
+
// Without it the whole client area is painted and every EDIT child then
|
|
1827
|
+
// repaints itself on top, which is a visible flicker on every mount.
|
|
1828
|
+
constexpr DWORD kWindowStyle = WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN;
|
|
1829
|
+
|
|
1830
|
+
RECT wanted{0, 0, width, height};
|
|
1831
|
+
AdjustWindowRect(&wanted, kWindowStyle, FALSE);
|
|
1832
|
+
|
|
1833
|
+
auto owned = std::make_unique<HostWindow>();
|
|
1834
|
+
HostWindow *made = owned.get();
|
|
1835
|
+
made->surfaceId = surfaceId;
|
|
1836
|
+
|
|
1837
|
+
// The root before the window: hostProc answers messages CreateWindowEx sends
|
|
1838
|
+
// before it returns, and it finds this record by HWND -- which is not set
|
|
1839
|
+
// yet, so those messages go to DefWindowProc. Pushing the record afterwards
|
|
1840
|
+
// is what keeps that true rather than half-true.
|
|
1841
|
+
made->root = gHost.mountingManager->createSurfaceRoot(surfaceId);
|
|
1842
|
+
made->root->setFrame(0, 0, static_cast<float>(width), static_cast<float>(height));
|
|
1843
|
+
|
|
1844
|
+
made->window = CreateWindowEx(0,
|
|
1845
|
+
L"BasaltHost",
|
|
1846
|
+
title,
|
|
1847
|
+
kWindowStyle,
|
|
1848
|
+
CW_USEDEFAULT,
|
|
1849
|
+
CW_USEDEFAULT,
|
|
1850
|
+
wanted.right - wanted.left,
|
|
1851
|
+
wanted.bottom - wanted.top,
|
|
1852
|
+
nullptr,
|
|
1853
|
+
nullptr,
|
|
1854
|
+
instance,
|
|
1855
|
+
nullptr);
|
|
1856
|
+
if (made->window == nullptr) {
|
|
1857
|
+
gHost.mountingManager->destroySurfaceRoot(surfaceId);
|
|
1858
|
+
return nullptr;
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
// Input, per window. The dispatcher holds this window's root, not a surface,
|
|
1862
|
+
// so it outlives every transaction mounted into it -- and a second window
|
|
1863
|
+
// with the first one's would deliver every press to the wrong tree.
|
|
1864
|
+
made->touchDispatcher =
|
|
1865
|
+
std::make_unique<basalt::Win32TouchDispatcher>(gHost.mountingManager.get(), made->root);
|
|
1866
|
+
// The keyboard half: Tab reaching a <Pressable>, and Enter activating it.
|
|
1867
|
+
// All of it is this project's -- a React Native view is not a window here, so
|
|
1868
|
+
// there is nothing for Windows to focus. See Win32Focus.h.
|
|
1869
|
+
made->focusManager =
|
|
1870
|
+
std::make_unique<basalt::Win32FocusManager>(gHost.mountingManager.get(), made->root);
|
|
1871
|
+
|
|
1872
|
+
gHost.windows.push_back(std::move(owned));
|
|
1873
|
+
return made;
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
bool createWindow() {
|
|
1877
|
+
return createHostWindow(kSurfaceId, L"basalt-core \x2014 Windows", kInitialWidth,
|
|
1878
|
+
kInitialHeight) != nullptr;
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
} // namespace
|
|
1882
|
+
|
|
1883
|
+
namespace basalt {
|
|
1884
|
+
|
|
1885
|
+
// core/WindowHost.h, on Windows.
|
|
1886
|
+
//
|
|
1887
|
+
// The surface id is allocated here rather than by JavaScript: it is Fabric's
|
|
1888
|
+
// number, the mounting manager keys its roots by it, and two windows racing to
|
|
1889
|
+
// pick one would be two windows sharing a root. Above the ids this host
|
|
1890
|
+
// reserves -- the app's surface is 1 and the error inspector's is 2.
|
|
1891
|
+
facebook::react::SurfaceId openHostWindow(const NewWindowOptions &options) {
|
|
1892
|
+
if (gHost.reactHost == nullptr || !gHost.surfaceStarted || options.component.empty() ||
|
|
1893
|
+
gHost.windows.empty()) {
|
|
1894
|
+
return 0;
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
static facebook::react::SurfaceId nextSurfaceId = kLogBoxSurfaceId + 1;
|
|
1898
|
+
const facebook::react::SurfaceId surfaceId = nextSurfaceId++;
|
|
1899
|
+
|
|
1900
|
+
const int width = static_cast<int>(options.width);
|
|
1901
|
+
const int height = static_cast<int>(options.height);
|
|
1902
|
+
const std::wstring title = basalt::win32::widen(options.title);
|
|
1903
|
+
HostWindow *made = createHostWindow(surfaceId, title.c_str(), width, height);
|
|
1904
|
+
if (made == nullptr) {
|
|
1905
|
+
return 0;
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
gHost.reactHost->startSurface(surfaceId,
|
|
1909
|
+
options.component,
|
|
1910
|
+
options.props,
|
|
1911
|
+
constraintsFor(width, height),
|
|
1912
|
+
layoutContextFor(gHost.scaleFactor));
|
|
1913
|
+
ShowWindow(made->window, SW_SHOW);
|
|
1914
|
+
UpdateWindow(made->window);
|
|
1915
|
+
std::fprintf(stderr,
|
|
1916
|
+
"opened window %d for module %s\n",
|
|
1917
|
+
static_cast<int>(surfaceId),
|
|
1918
|
+
options.component.c_str());
|
|
1919
|
+
return surfaceId;
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
// Ends the application. `PostQuitMessage` rather than `DestroyWindow` on
|
|
1923
|
+
// each: it stops the message loop whatever is open, which is what a person
|
|
1924
|
+
// choosing Quit means, and the ordinary teardown follows it.
|
|
1925
|
+
void quitHost() {
|
|
1926
|
+
PostQuitMessage(0);
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
void closeHostWindow(facebook::react::SurfaceId surfaceId) {
|
|
1930
|
+
// The main window is not closed this way: destroying the surface an app is
|
|
1931
|
+
// running in is not the same thing as closing its window, and an app that
|
|
1932
|
+
// means the second should say so through `close()` on the window itself.
|
|
1933
|
+
if (gHost.reactHost == nullptr || surfaceId == kSurfaceId || surfaceId == kLogBoxSurfaceId) {
|
|
1934
|
+
return;
|
|
1935
|
+
}
|
|
1936
|
+
if (gHost.windowFor(surfaceId) == nullptr) {
|
|
1937
|
+
return;
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
// Stopping a surface unmounts its React tree, which produces one last
|
|
1941
|
+
// transaction of Remove and Delete mutations. Those arrive on the UI thread
|
|
1942
|
+
// afterwards, and the views they name have to still be there when they do.
|
|
1943
|
+
gHost.reactHost->stopSurface(surfaceId);
|
|
1944
|
+
|
|
1945
|
+
// So the window goes a round trip later: out to the JavaScript thread, which
|
|
1946
|
+
// is where the teardown runs, and back to this one, which is where its
|
|
1947
|
+
// mutations are applied. Both queues are ordered, so anything the stop
|
|
1948
|
+
// produced is ahead of this.
|
|
1949
|
+
gHost.reactHost->runOnRuntimeScheduler([surfaceId](facebook::jsi::Runtime &) {
|
|
1950
|
+
basalt::postToUiThread([surfaceId] {
|
|
1951
|
+
HostWindow *going = gHost.windowFor(surfaceId);
|
|
1952
|
+
if (going == nullptr) {
|
|
1953
|
+
return;
|
|
1954
|
+
}
|
|
1955
|
+
// The dispatchers before the views they hold: both keep a borrowed root.
|
|
1956
|
+
going->focusManager.reset();
|
|
1957
|
+
going->touchDispatcher.reset();
|
|
1958
|
+
// The target before the window it belongs to, and the window before the
|
|
1959
|
+
// root the mutations named. DestroyWindow takes any <TextInput> peer
|
|
1960
|
+
// parented to it with it, which is why nothing here frees one by hand.
|
|
1961
|
+
going->target.Reset();
|
|
1962
|
+
// Before DestroyWindow, because DestroyWindow sends WM_DESTROY on this
|
|
1963
|
+
// thread and synchronously: the handler runs before the call returns, and
|
|
1964
|
+
// has to know this is the app's own doing.
|
|
1965
|
+
going->closingFromApp = true;
|
|
1966
|
+
// Null when WM_DESTROY got here first, which is a window the person
|
|
1967
|
+
// closed rather than the app.
|
|
1968
|
+
if (going->window != nullptr) {
|
|
1969
|
+
DestroyWindow(going->window);
|
|
1970
|
+
}
|
|
1971
|
+
gHost.mountingManager->destroySurfaceRoot(surfaceId);
|
|
1972
|
+
|
|
1973
|
+
for (auto it = gHost.windows.begin(); it != gHost.windows.end(); ++it) {
|
|
1974
|
+
if (it->get() == going) {
|
|
1975
|
+
gHost.windows.erase(it);
|
|
1976
|
+
break;
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
std::fprintf(stderr, "closed window %d\n", static_cast<int>(surfaceId));
|
|
1980
|
+
});
|
|
1981
|
+
});
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
std::vector<facebook::react::SurfaceId> hostWindows() {
|
|
1985
|
+
std::vector<facebook::react::SurfaceId> open;
|
|
1986
|
+
open.reserve(gHost.windows.size());
|
|
1987
|
+
for (const auto &candidate : gHost.windows) {
|
|
1988
|
+
open.push_back(candidate->surfaceId);
|
|
1989
|
+
}
|
|
1990
|
+
return open;
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
} // namespace basalt
|
|
1994
|
+
|
|
1995
|
+
int main(int argc, char **argv) {
|
|
1996
|
+
// glog, before anything logs through it.
|
|
1997
|
+
//
|
|
1998
|
+
// Without this every run opens with "WARNING: Logging before
|
|
1999
|
+
// InitGoogleLogging() is written to STDERR" -- glog saying it has not been
|
|
2000
|
+
// told where to write, in a line that reads like something went wrong on
|
|
2001
|
+
// start-up. It goes to stderr either way; this only stops it apologising for
|
|
2002
|
+
// it. React Native's C++ logs through glog throughout, so the first message
|
|
2003
|
+
// arrives before anything this project writes.
|
|
2004
|
+
FLAGS_logtostderr = true;
|
|
2005
|
+
google::InitGoogleLogging(argv[0]);
|
|
2006
|
+
|
|
2007
|
+
gHost.bundlePath = argc > 1 ? argv[1] : "build/main.jsbundle.js";
|
|
2008
|
+
// Defaults to the raw-Fabric script, because a React screen needs
|
|
2009
|
+
// <ScrollView> and <TextInput> that Windows does not mount yet and would
|
|
2010
|
+
// render with holes in it. A React app that stays inside <View>, <Text>,
|
|
2011
|
+
// <Image> and <Pressable> -- js/press.js, say -- runs from here.
|
|
2012
|
+
gHost.moduleName = argc > 2 ? argv[2] : "";
|
|
2013
|
+
// Metro's entry, as the GTK and AppKit hosts read it: BASALT_DEV_ENTRY, which
|
|
2014
|
+
// is what `run-windows` sets, and "index" otherwise. A third argument still
|
|
2015
|
+
// wins for anyone passing one. This used to read the argument alone, which
|
|
2016
|
+
// nothing passes, so sourcePath was empty in every development run --
|
|
2017
|
+
// DevServerHelper then builds no URL at all ("Failed to download JS bundle
|
|
2018
|
+
// from Url: ."), and loadScript quietly fell back to the on-disk bundle: the
|
|
2019
|
+
// window opened, and no edit ever reached it.
|
|
2020
|
+
if (argc > 3) {
|
|
2021
|
+
gHost.sourcePath = argv[3];
|
|
2022
|
+
} else if (const char *entry = std::getenv("BASALT_DEV_ENTRY")) {
|
|
2023
|
+
gHost.sourcePath = entry;
|
|
2024
|
+
} else {
|
|
2025
|
+
gHost.sourcePath = "index";
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
// A URL the desktop launched this with, for `Linking.getInitialURL()`. Any
|
|
2029
|
+
// argument after the bundle and the module that carries a scheme: a shell
|
|
2030
|
+
// association passes one, and it does not know what came before. `://` rather
|
|
2031
|
+
// than a bare colon, so a Windows path like `C:\bundle.js` is not mistaken
|
|
2032
|
+
// for one.
|
|
2033
|
+
for (int i = 1; i < argc; i++) {
|
|
2034
|
+
const std::string argument(argv[i]);
|
|
2035
|
+
if (argument.find("://") != std::string::npos) {
|
|
2036
|
+
basalt::setInitialUrl(argument);
|
|
2037
|
+
break;
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
// Per-monitor DPI, declared in code rather than in a manifest so that running
|
|
2042
|
+
// the binary straight out of the build directory behaves the same as running
|
|
2043
|
+
// an installed one. Without it Windows scales the window's bitmap and every
|
|
2044
|
+
// line goes soft on a high-DPI display.
|
|
2045
|
+
SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
|
|
2046
|
+
|
|
2047
|
+
// COM, on the thread that will outlive every other one.
|
|
2048
|
+
//
|
|
2049
|
+
// WIC decodes an <Image> on a worker, and the IWICBitmap it produces is
|
|
2050
|
+
// released here when the view holding it is deleted. A release in a thread
|
|
2051
|
+
// with no apartment at all is what that used to be, and the process exited
|
|
2052
|
+
// 0xC0000005 every time an app rendered an image. ShellExecute wants an
|
|
2053
|
+
// initialised thread too, and that is how core/PlatformServices opens a URL.
|
|
2054
|
+
//
|
|
2055
|
+
// Apartment-threaded because this thread owns a window and pumps messages,
|
|
2056
|
+
// which is what an STA is for.
|
|
2057
|
+
// OleInitialize rather than CoInitializeEx: it initialises the same
|
|
2058
|
+
// apartment and additionally the OLE machinery RegisterDragDrop needs. A
|
|
2059
|
+
// thread that only called CoInitializeEx can open a URL and cannot accept a
|
|
2060
|
+
// drop, and the failure is a DRAGDROP_E_NOTREGISTERED nobody looks for.
|
|
2061
|
+
OleInitialize(nullptr);
|
|
2062
|
+
|
|
2063
|
+
// Before anything posts: this is what makes postToUiThread marshal rather
|
|
2064
|
+
// than run inline, and the mounting manager depends on it. See
|
|
2065
|
+
// win32/Win32UiThread.h.
|
|
2066
|
+
basalt::installUiThread();
|
|
2067
|
+
|
|
2068
|
+
// Before any window exists, which is the part that matters: the shell reads
|
|
2069
|
+
// a process's AppUserModelID when its first window is shown and does not
|
|
2070
|
+
// notice a later change. What the id buys is a notification Windows will
|
|
2071
|
+
// attribute to this application rather than refuse. Needs COM, which is
|
|
2072
|
+
// already initialised above. See win32/Win32Packaging.h.
|
|
2073
|
+
basalt::win32::applyPackaging(basalt::appIdentity(gHost.bundlePath));
|
|
2074
|
+
|
|
2075
|
+
if (FAILED(D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED,
|
|
2076
|
+
gHost.d2dFactory.GetAddressOf()))) {
|
|
2077
|
+
std::fprintf(stderr, "could not create a Direct2D factory\n");
|
|
2078
|
+
return 1;
|
|
2079
|
+
}
|
|
2080
|
+
// Before the window, which is the ordering multiple windows imposes: a window
|
|
2081
|
+
// makes a surface root, and only the mounting manager can. Constructed on
|
|
2082
|
+
// this thread, which it records and asserts every mutation lands back on.
|
|
2083
|
+
gHost.mountingManager = std::make_shared<basalt::Win32MountingManager>();
|
|
2084
|
+
|
|
2085
|
+
if (!createWindow()) {
|
|
2086
|
+
std::fprintf(stderr, "could not create the window\n");
|
|
2087
|
+
return 1;
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
// The app's Expo config, if it is an Expo app and has been bundled. Before the
|
|
2091
|
+
// runtime is installed, as the other two hosts load it -- this one never did,
|
|
2092
|
+
// so Constants.expoConfig was null on Windows. It also names the window.
|
|
2093
|
+
basalt::loadExpoAppConfigBeside(gHost.bundlePath);
|
|
2094
|
+
{
|
|
2095
|
+
const std::string appName = basalt::expoAppName();
|
|
2096
|
+
basalt::titleBar().setDefaultTitle(
|
|
2097
|
+
!appName.empty() ? appName
|
|
2098
|
+
: (!gHost.moduleName.empty() ? gHost.moduleName : "basalt-core"));
|
|
2099
|
+
}
|
|
2100
|
+
// Dark or light to match the app, and again whenever that changes -- in
|
|
2101
|
+
// Settings, or through Appearance.setColorScheme on the JavaScript thread, so
|
|
2102
|
+
// the change is marshalled here from wherever it was announced.
|
|
2103
|
+
basalt::titleBar().setDarkMode(basalt::effectiveColorScheme() == basalt::ColorScheme::Dark);
|
|
2104
|
+
basalt::titleBar().attach(gHost.main().window);
|
|
2105
|
+
basalt::addColorSchemeObserver([](basalt::ColorScheme scheme) {
|
|
2106
|
+
basalt::postToUiThread(
|
|
2107
|
+
[scheme] { basalt::titleBar().setDarkMode(scheme == basalt::ColorScheme::Dark); });
|
|
2108
|
+
});
|
|
2109
|
+
|
|
2110
|
+
UINT dpi = GetDpiForWindow(gHost.main().window);
|
|
2111
|
+
gHost.scaleFactor = dpi >= 96 ? static_cast<int>(dpi / 96) : 1;
|
|
2112
|
+
|
|
2113
|
+
// Bridgeless. React Native's C++ host is the bridgeless one, and there is no
|
|
2114
|
+
// bridge here to be the alternative -- so it asserts that.
|
|
2115
|
+
facebook::react::ReactNativeFeatureFlags::override(
|
|
2116
|
+
std::make_unique<DesktopFeatureFlags>());
|
|
2117
|
+
|
|
2118
|
+
gHost.runLoopObserverManager = std::make_shared<RunLoopObserverManager>();
|
|
2119
|
+
gHost.choreographer = std::make_shared<basalt::Win32AnimationChoreographer>();
|
|
2120
|
+
|
|
2121
|
+
// Every mounted transaction has to reach the screen. Windows repaints on
|
|
2122
|
+
// demand, so this is the difference between mutations arriving and anything
|
|
2123
|
+
// being visible.
|
|
2124
|
+
gHost.mountingManager->setOnDidMount(syncPeersAndRepaint);
|
|
2125
|
+
|
|
2126
|
+
// Prime the bounds cache, which `getBounds()` answers from: without this an
|
|
2127
|
+
// app's first render sees a window of no size, and only a later resize
|
|
2128
|
+
// corrects it. See core/WindowBoundsCache.cpp.
|
|
2129
|
+
basalt::notifyWindowBoundsChanged();
|
|
2130
|
+
|
|
2131
|
+
// And the display list, for the same reason and off the same seam: an app
|
|
2132
|
+
// asking which screens exist before anything has been plugged or unplugged
|
|
2133
|
+
// should not be told there are none.
|
|
2134
|
+
basalt::notifyDisplaysChanged();
|
|
2135
|
+
|
|
2136
|
+
// Accepting what the desktop drags onto this window. Per window rather than
|
|
2137
|
+
// per view: OLE registers a target against an HWND, and which *app* view is
|
|
2138
|
+
// under the pointer is core/DragAndDrop.h's question.
|
|
2139
|
+
basalt::attachDropTarget(gHost.main().window, gHost.main().root);
|
|
2140
|
+
|
|
2141
|
+
// Every <TextInput>'s EDIT peer is a child of this window. Set before the
|
|
2142
|
+
// first transaction, because a field that mounts without one gets no control
|
|
2143
|
+
// and no second chance -- `update` only creates on first sight.
|
|
2144
|
+
gHost.mountingManager->setHostWindow(gHost.main().window);
|
|
2145
|
+
|
|
2146
|
+
// Light or dark, and any change to it. Before ReactHost, so the module can
|
|
2147
|
+
// answer from the first query.
|
|
2148
|
+
basalt::startObservingColorScheme();
|
|
2149
|
+
|
|
2150
|
+
ReactInstanceConfig config;
|
|
2151
|
+
// The appId is how this host tells Metro which platform it is; see
|
|
2152
|
+
// APP_ID_PREFIX in packages/basalt-core/metro-config.js.
|
|
2153
|
+
config.appId = "basalt-windows";
|
|
2154
|
+
config.deviceName = "windows";
|
|
2155
|
+
|
|
2156
|
+
// Dev mode changes three things at once: loadScript tries Metro before the
|
|
2157
|
+
// on-disk bundle; DevServerHelper exists, which is the only condition under
|
|
2158
|
+
// which ReactCxxTurboModuleProvider serves the DevSettings module a __DEV__
|
|
2159
|
+
// bundle requires; and ReactHost opens a packager connection whose reload
|
|
2160
|
+
// message reloads the instance.
|
|
2161
|
+
config.enableDevMode = std::getenv("BASALT_DEV") != nullptr;
|
|
2162
|
+
gHost.devMode = config.enableDevMode;
|
|
2163
|
+
config.enableInspector = config.enableDevMode;
|
|
2164
|
+
if (const char *devHost = std::getenv("BASALT_DEV_HOST")) {
|
|
2165
|
+
config.devServerHost = devHost;
|
|
2166
|
+
}
|
|
2167
|
+
if (const char *devPort = std::getenv("BASALT_DEV_PORT")) {
|
|
2168
|
+
config.devServerPort = static_cast<uint32_t>(std::strtoul(devPort, nullptr, 10));
|
|
2169
|
+
}
|
|
2170
|
+
// So the http client can tell the bundle request apart from an app's own
|
|
2171
|
+
// fetch, and refuse to feed a Metro error page to the JS engine. See
|
|
2172
|
+
// core/DevBundle.h.
|
|
2173
|
+
if (config.enableDevMode) {
|
|
2174
|
+
basalt::setDevServerOrigin(config.devServerHost, config.devServerPort);
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
try {
|
|
2178
|
+
gHost.reactHost = std::make_unique<ReactHost>(
|
|
2179
|
+
config,
|
|
2180
|
+
gHost.mountingManager,
|
|
2181
|
+
gHost.runLoopObserverManager,
|
|
2182
|
+
makeContextContainer(),
|
|
2183
|
+
facebook::react::getDefaultOnJsErrorFunc(),
|
|
2184
|
+
logToConsole,
|
|
2185
|
+
nullptr,
|
|
2186
|
+
makeTurboModuleProviders(
|
|
2187
|
+
basalt::scriptURLFor(gHost.bundlePath,
|
|
2188
|
+
config.enableDevMode,
|
|
2189
|
+
config.devServerHost,
|
|
2190
|
+
config.devServerPort,
|
|
2191
|
+
gHost.sourcePath.empty() ? "index" : gHost.sourcePath,
|
|
2192
|
+
"windows"),
|
|
2193
|
+
config.enableDevMode,
|
|
2194
|
+
gHost.mountingManager->imageLoader()),
|
|
2195
|
+
// React Native's error inspector. ReactCxxPlatform implements the
|
|
2196
|
+
// LogBox TurboModule itself and only provides it when a host hands over
|
|
2197
|
+
// one of these; passing null, as this did, left NativeLogBox.show() a
|
|
2198
|
+
// call into nothing. See core/LogBoxSurface.h.
|
|
2199
|
+
std::make_shared<basalt::LogBoxSurfaceDelegate>(
|
|
2200
|
+
[](const std::string &appKey) { showLogBoxSurface(appKey); },
|
|
2201
|
+
[]() { hideLogBoxSurface(); }),
|
|
2202
|
+
// `useNativeDriver: true`. ReactCommon has a C++ implementation of the
|
|
2203
|
+
// whole animated graph and ReactCxxPlatform provides the module for it,
|
|
2204
|
+
// but only when a host asks by handing over a provider. Without it every
|
|
2205
|
+
// native-driven Animated call throws "Native animated module is not
|
|
2206
|
+
// available".
|
|
2207
|
+
std::make_shared<facebook::react::NativeAnimatedNodesManagerProvider>(),
|
|
2208
|
+
installBindings,
|
|
2209
|
+
gHost.choreographer);
|
|
2210
|
+
} catch (const std::exception &error) {
|
|
2211
|
+
std::fprintf(stderr, "could not construct ReactHost: %s\n", error.what());
|
|
2212
|
+
return 1;
|
|
2213
|
+
}
|
|
2214
|
+
|
|
2215
|
+
// How anything in the core reaches the scheduler, which only the host has.
|
|
2216
|
+
// Reanimated is the one caller; see core/UIManagerAccess.h.
|
|
2217
|
+
basalt::setEventListenerInstaller(
|
|
2218
|
+
[](std::shared_ptr<const facebook::react::EventListener> listener) {
|
|
2219
|
+
if (gHost.reactHost == nullptr) {
|
|
2220
|
+
return;
|
|
2221
|
+
}
|
|
2222
|
+
gHost.reactHost->runOnScheduler(
|
|
2223
|
+
[listener = std::move(listener)](facebook::react::Scheduler &scheduler) {
|
|
2224
|
+
scheduler.addEventListener(listener);
|
|
2225
|
+
});
|
|
2226
|
+
});
|
|
2227
|
+
|
|
2228
|
+
// And how anything in the core reaches the JavaScript thread. An Expo
|
|
2229
|
+
// module is given no call invoker, so this is the only way one can emit an
|
|
2230
|
+
// event or settle a promise later; see core/JsRuntimeAccess.h.
|
|
2231
|
+
basalt::setRuntimeRunner(
|
|
2232
|
+
[](std::function<void(facebook::jsi::Runtime &)> work) {
|
|
2233
|
+
if (gHost.reactHost == nullptr) {
|
|
2234
|
+
return;
|
|
2235
|
+
}
|
|
2236
|
+
gHost.reactHost->runOnRuntimeScheduler(std::move(work));
|
|
2237
|
+
});
|
|
2238
|
+
|
|
2239
|
+
// `loadScript` falls back to the on-disk bundle whenever the Metro fetch
|
|
2240
|
+
// fails, which is right when nothing is listening and wrong when Metro
|
|
2241
|
+
// answered with an error: running the last bundle that built, while the
|
|
2242
|
+
// developer looks at source that is not what is executing, hides the very
|
|
2243
|
+
// thing they need to see. So the fallback is allowed only for the first case,
|
|
2244
|
+
// and the second stops here with Metro's own message.
|
|
2245
|
+
const bool loaded = gHost.reactHost->loadScript(gHost.bundlePath, gHost.sourcePath);
|
|
2246
|
+
if (const auto devError = basalt::devBundleError()) {
|
|
2247
|
+
std::fprintf(stderr,
|
|
2248
|
+
"Metro could not build the bundle (HTTP %ld):\n%s\n",
|
|
2249
|
+
static_cast<long>(devError->status),
|
|
2250
|
+
devError->message.c_str());
|
|
2251
|
+
// Through shutdown() rather than straight out: the fallback bundle is
|
|
2252
|
+
// already running on the JS thread, and dropping the host without joining
|
|
2253
|
+
// it aborts in a destructor instead of exiting.
|
|
2254
|
+
shutdown();
|
|
2255
|
+
return 1;
|
|
2256
|
+
}
|
|
2257
|
+
if (!loaded) {
|
|
2258
|
+
std::fprintf(stderr, "could not load script: %s\n", gHost.bundlePath.c_str());
|
|
2259
|
+
shutdown();
|
|
2260
|
+
return 1;
|
|
2261
|
+
}
|
|
2262
|
+
// Where it came from, not where it might have. See core/DevBundle.h.
|
|
2263
|
+
if (const auto from = basalt::devBundleFetched()) {
|
|
2264
|
+
std::fprintf(stderr, "loaded script from the packager: %s\n", from->c_str());
|
|
2265
|
+
} else {
|
|
2266
|
+
std::fprintf(stderr, "loaded script: %s\n", gHost.bundlePath.c_str());
|
|
2267
|
+
}
|
|
2268
|
+
|
|
2269
|
+
// The module name decides who drives the surface. Non-empty:
|
|
2270
|
+
// SurfaceHandler::start calls AppRegistry.runApplication. Empty: the surface
|
|
2271
|
+
// is registered without JS being called at all, leaving it for a script to
|
|
2272
|
+
// commit into through nativeFabricUIManager by hand.
|
|
2273
|
+
gHost.reactHost->startSurface(kSurfaceId,
|
|
2274
|
+
gHost.moduleName,
|
|
2275
|
+
folly::dynamic::object(),
|
|
2276
|
+
constraintsFor(kInitialWidth, kInitialHeight),
|
|
2277
|
+
layoutContextFor(gHost.scaleFactor));
|
|
2278
|
+
gHost.surfaceStarted = true;
|
|
2279
|
+
std::fprintf(stderr,
|
|
2280
|
+
"started surface %d%s%s\n",
|
|
2281
|
+
static_cast<int>(kSurfaceId),
|
|
2282
|
+
gHost.moduleName.empty() ? " (no module; raw Fabric script)" : " for module ",
|
|
2283
|
+
gHost.moduleName.c_str());
|
|
2284
|
+
|
|
2285
|
+
ShowWindow(gHost.main().window, SW_SHOWNORMAL);
|
|
2286
|
+
UpdateWindow(gHost.main().window);
|
|
2287
|
+
|
|
2288
|
+
if (gHost.moduleName.empty()) {
|
|
2289
|
+
std::fprintf(stderr, "--- committing tree 1 from JS ---\n");
|
|
2290
|
+
callRenderFunction(1);
|
|
2291
|
+
SetTimer(gHost.main().window, kSecondTreeTimer, 2000, nullptr);
|
|
2292
|
+
}
|
|
2293
|
+
|
|
2294
|
+
UINT scriptedDelayMs = 1500;
|
|
2295
|
+
if (const char *taps = std::getenv("BASALT_TEST_TAP")) {
|
|
2296
|
+
scriptedDelayMs = scheduleTestPoints(taps, scriptedDelayMs, ScriptedInput::Kind::Tap);
|
|
2297
|
+
}
|
|
2298
|
+
// BASALT_TEST_SECONDARY_TAP: the other button, which presses nothing and
|
|
2299
|
+
// arrives as a pointer event. See core/PointerButtons.h.
|
|
2300
|
+
if (const char *taps = std::getenv("BASALT_TEST_SECONDARY_TAP")) {
|
|
2301
|
+
scriptedDelayMs = scheduleTestPoints(
|
|
2302
|
+
taps, scriptedDelayMs, ScriptedInput::Kind::Tap, basalt::PointerButton::Secondary);
|
|
2303
|
+
}
|
|
2304
|
+
// BASALT_TEST_HOVER: the pointer moving with no button down. A negative point
|
|
2305
|
+
// means it left the window, which is what WM_MOUSELEAVE reports.
|
|
2306
|
+
if (const char *hovers = std::getenv("BASALT_TEST_HOVER")) {
|
|
2307
|
+
scriptedDelayMs = scheduleTestPoints(hovers, scriptedDelayMs, ScriptedInput::Kind::Hover);
|
|
2308
|
+
}
|
|
2309
|
+
if (const char *drag = std::getenv("BASALT_TEST_DRAG")) {
|
|
2310
|
+
scriptedDelayMs = scheduleTestDrag(drag, scriptedDelayMs);
|
|
2311
|
+
}
|
|
2312
|
+
if (const char *scrolls = std::getenv("BASALT_TEST_SCROLL")) {
|
|
2313
|
+
scriptedDelayMs = scheduleTestScrolls(scrolls, scriptedDelayMs);
|
|
2314
|
+
}
|
|
2315
|
+
// BASALT_TEST_CLOSE_WINDOW: the surface id of a window to close the way a
|
|
2316
|
+
// person would -- its own close button, not the app asking. The two take
|
|
2317
|
+
// different paths through the host and only one of them can leave a record
|
|
2318
|
+
// whose window is gone, which is why it is worth being able to drive.
|
|
2319
|
+
if (const char *closing = std::getenv("BASALT_TEST_CLOSE_WINDOW")) {
|
|
2320
|
+
scriptedDelayMs = scheduleScriptedInput(
|
|
2321
|
+
ScriptedInput{.kind = ScriptedInput::Kind::Close,
|
|
2322
|
+
.surfaceId = static_cast<facebook::react::SurfaceId>(std::atoi(closing))},
|
|
2323
|
+
scriptedDelayMs);
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2326
|
+
// BASALT_TEST_DROP: "x,y:path" -- a file dropped at that point. On all three
|
|
2327
|
+
// hosts, because a scenario that uses an instrument two of them have is a
|
|
2328
|
+
// scenario that fails on the third for a reason that has nothing to do with
|
|
2329
|
+
// what it tests. Learned twice: BASALT_TEST_QUIT did the same.
|
|
2330
|
+
if (const char *drop = std::getenv("BASALT_TEST_DROP")) {
|
|
2331
|
+
const std::string spec(drop);
|
|
2332
|
+
const size_t comma = spec.find(',');
|
|
2333
|
+
const size_t colon = comma == std::string::npos ? std::string::npos : spec.find(':', comma);
|
|
2334
|
+
if (comma == std::string::npos || colon == std::string::npos) {
|
|
2335
|
+
std::fprintf(stderr, "BASALT_TEST_DROP: expected x,y:path\n");
|
|
2336
|
+
} else {
|
|
2337
|
+
scriptedDelayMs = scheduleScriptedInput(
|
|
2338
|
+
ScriptedInput{.kind = ScriptedInput::Kind::Drop,
|
|
2339
|
+
.fromX = std::strtod(spec.substr(0, comma).c_str(), nullptr),
|
|
2340
|
+
.fromY = std::strtod(spec.substr(comma + 1, colon - comma - 1).c_str(),
|
|
2341
|
+
nullptr),
|
|
2342
|
+
.text = spec.substr(colon + 1)},
|
|
2343
|
+
scriptedDelayMs);
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
|
|
2347
|
+
// BASALT_TEST_QUIT: ask the application to quit the way a person does. A
|
|
2348
|
+
// count rather than a flag, for the reason the other hosts give: one ask
|
|
2349
|
+
// cannot show both a refusal and the agreement after it.
|
|
2350
|
+
if (const char *asks = std::getenv("BASALT_TEST_QUIT")) {
|
|
2351
|
+
const int times = std::max(1, std::atoi(asks));
|
|
2352
|
+
for (int ask = 0; ask < times; ask++) {
|
|
2353
|
+
scriptedDelayMs = scheduleScriptedInput(
|
|
2354
|
+
ScriptedInput{.kind = ScriptedInput::Kind::Quit}, scriptedDelayMs);
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
|
|
2358
|
+
// BASALT_TEST_FOCUS: keyboard actions separated by ';' -- `tab`, `shift-tab`,
|
|
2359
|
+
// `activate`, `escape` and `devmenu`. The same reason the other instruments exist: a
|
|
2360
|
+
// real Tab needs a window the system considers focused, which an automated
|
|
2361
|
+
// run does not reliably have.
|
|
2362
|
+
// BASALT_TEST_KEY: a declared shortcut, pressed. The step this host was missing
|
|
2363
|
+
// -- the delivery and the WM_KEYDOWN hook were here and nothing scheduled
|
|
2364
|
+
// anything, so the scenario's own "did the instrument run at all" guard failed
|
|
2365
|
+
// rather than an assertion about keys. Which is what that guard is for.
|
|
2366
|
+
for (const basalt::KeyCombination &pressed : basalt::scriptedKeyPresses()) {
|
|
2367
|
+
scriptedDelayMs = scheduleScriptedInput(
|
|
2368
|
+
ScriptedInput{.kind = ScriptedInput::Kind::Key, .pressed = pressed}, scriptedDelayMs);
|
|
2369
|
+
}
|
|
2370
|
+
|
|
2371
|
+
if (const char *focus = std::getenv("BASALT_TEST_FOCUS")) {
|
|
2372
|
+
const std::string all(focus);
|
|
2373
|
+
size_t start = 0;
|
|
2374
|
+
while (start <= all.size()) {
|
|
2375
|
+
const size_t semicolon = all.find(';', start);
|
|
2376
|
+
const std::string action =
|
|
2377
|
+
all.substr(start, semicolon == std::string::npos ? std::string::npos : semicolon - start);
|
|
2378
|
+
if (!action.empty()) {
|
|
2379
|
+
scriptedDelayMs = scheduleScriptedInput(
|
|
2380
|
+
ScriptedInput{.kind = ScriptedInput::Kind::Focus, .text = action}, scriptedDelayMs);
|
|
2381
|
+
}
|
|
2382
|
+
if (semicolon == std::string::npos) {
|
|
2383
|
+
break;
|
|
2384
|
+
}
|
|
2385
|
+
start = semicolon + 1;
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
// BASALT_TEST_TYPE: text for whichever field has focus. Last, so that a tap
|
|
2390
|
+
// scheduled above has already put focus somewhere.
|
|
2391
|
+
if (const char *text = std::getenv("BASALT_TEST_TYPE")) {
|
|
2392
|
+
scriptedDelayMs = scheduleScriptedInput(
|
|
2393
|
+
ScriptedInput{.kind = ScriptedInput::Kind::Type, .text = text}, scriptedDelayMs);
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2396
|
+
// What the schedule above came to, so BASALT_QUIT_WHEN_SETTLED waits for the
|
|
2397
|
+
// last of it rather than only for the first mount. Said once, after everything
|
|
2398
|
+
// that moves the total. See core/TestSettle.h.
|
|
2399
|
+
basalt::noteScriptedInputEndsInMs(scriptedDelayMs);
|
|
2400
|
+
|
|
2401
|
+
// BASALT_QUIT_AFTER_MS, so an automated run terminates without anyone
|
|
2402
|
+
// clicking anything. The same escape hatch both other hosts have.
|
|
2403
|
+
if (const char *quitAfter = std::getenv("BASALT_QUIT_AFTER_MS")) {
|
|
2404
|
+
const UINT delay = static_cast<UINT>(std::strtoul(quitAfter, nullptr, 10));
|
|
2405
|
+
SetTimer(
|
|
2406
|
+
gHost.main().window, kQuitAfterTimer, delay, [](HWND hwnd, UINT, UINT_PTR id, DWORD) {
|
|
2407
|
+
KillTimer(hwnd, id);
|
|
2408
|
+
// The same line the other two hosts log, so a person reading three
|
|
2409
|
+
// transcripts side by side sees the same shutdown in each. This
|
|
2410
|
+
// host was silent about it, which is how a scenario came to assert
|
|
2411
|
+
// on a string only two of them print.
|
|
2412
|
+
std::fprintf(stderr, "BASALT_QUIT_AFTER_MS elapsed; quitting\n");
|
|
2413
|
+
// What WM_CLOSE does, without going through WM_CLOSE. This is the
|
|
2414
|
+
// harness ending the run, not a person closing a window, and an app
|
|
2415
|
+
// that intercepts its own close would otherwise refuse it -- a test
|
|
2416
|
+
// that asks "does refusing work" by never exiting.
|
|
2417
|
+
captureBeforeTeardown();
|
|
2418
|
+
DestroyWindow(hwnd);
|
|
2419
|
+
});
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2422
|
+
// BASALT_TEST_QUIT_FILE: the same shutdown, at a moment the harness picks
|
|
2423
|
+
// rather than on a budget fixed before launch. captureBeforeTeardown and
|
|
2424
|
+
// DestroyWindow either way, so the widget tree is dumped -- which is the whole
|
|
2425
|
+
// reason this instrument exists. TerminateProcess, which is what
|
|
2426
|
+
// Popen.terminate() is on this platform, reaches neither, so before this the
|
|
2427
|
+
// scenarios that read the tree could not be run here at all.
|
|
2428
|
+
//
|
|
2429
|
+
// See core/TestQuitFile.h.
|
|
2430
|
+
//
|
|
2431
|
+
// Two instruments, one timer, because they want the same thing at different
|
|
2432
|
+
// moments: the quit file is the harness deciding, BASALT_QUIT_WHEN_SETTLED is
|
|
2433
|
+
// the app noticing it has nothing left to do. One timer is also one id, and
|
|
2434
|
+
// SetTimer replaces silently on a collision. See core/TestSettle.h.
|
|
2435
|
+
if (basalt::testQuitFilePath().has_value() || basalt::quitWhenSettledMs().has_value()) {
|
|
2436
|
+
SetTimer(gHost.main().window,
|
|
2437
|
+
kQuitFileTimer,
|
|
2438
|
+
basalt::kTestQuitFilePollMs,
|
|
2439
|
+
[](HWND hwnd, UINT, UINT_PTR id, DWORD) {
|
|
2440
|
+
const char *why = nullptr;
|
|
2441
|
+
if (basalt::testQuitFileAppeared()) {
|
|
2442
|
+
why = basalt::kTestQuitFileMessage;
|
|
2443
|
+
} else if (basalt::hasSettled()) {
|
|
2444
|
+
why = basalt::kQuitWhenSettledMessage;
|
|
2445
|
+
} else {
|
|
2446
|
+
return;
|
|
2447
|
+
}
|
|
2448
|
+
KillTimer(hwnd, id);
|
|
2449
|
+
std::fprintf(stderr, "%s\n", why);
|
|
2450
|
+
captureBeforeTeardown();
|
|
2451
|
+
DestroyWindow(hwnd);
|
|
2452
|
+
});
|
|
2453
|
+
}
|
|
2454
|
+
|
|
2455
|
+
// Not a plain GetMessage loop: the event beat has to be induced each time the
|
|
2456
|
+
// queue empties, and Win32 has no "about to wait" hook to install one into.
|
|
2457
|
+
// See win32/Win32RunLoopObserver.h.
|
|
2458
|
+
const int exitCode = basalt::runMessageLoopWithBeat(gHost.runLoopObserverManager);
|
|
2459
|
+
|
|
2460
|
+
// Before the window goes: OLE holds a reference to the drop target, and a
|
|
2461
|
+
// revoked target is one it can release. Leaving it registered would leave
|
|
2462
|
+
// OLE pointing at a destroyed HWND.
|
|
2463
|
+
basalt::detachDropTarget(gHost.windows.empty() ? nullptr : gHost.main().window);
|
|
2464
|
+
|
|
2465
|
+
shutdown();
|
|
2466
|
+
// After shutdown, which is where the last WIC bitmap is released.
|
|
2467
|
+
// OleUninitialize rather than CoUninitialize, to match the OleInitialize
|
|
2468
|
+
// above: it undoes both halves.
|
|
2469
|
+
OleUninitialize();
|
|
2470
|
+
return exitCode;
|
|
2471
|
+
}
|
|
2472
|
+
|
|
2473
|
+
// What is missing is now two components rather than the whole of input:
|
|
2474
|
+
// <ScrollView> and <TextInput>. Both other hosts have them, and an ordinary
|
|
2475
|
+
// React screen renders with holes in it without them, which is why this host
|
|
2476
|
+
// still defaults to the raw-Fabric script rather than to a React app.
|
|
2477
|
+
//
|
|
2478
|
+
// Keyboard input is no longer among them. A <TextInput>'s peer is a real child
|
|
2479
|
+
// window and takes its own keys; everything else is reached by Tab through
|
|
2480
|
+
// Win32Focus.h, which owns the whole chain because a React Native view here is
|
|
2481
|
+
// not a window and so has no focus of Windows' own.
|