basalt-appkit 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/runMacos.js +33 -0
- package/cli/shared.js +75 -0
- package/native/CMakeLists.txt +298 -0
- package/native/appkit/AppKitAnimationChoreographer.h +54 -0
- package/native/appkit/AppKitAnimationChoreographer.mm +125 -0
- package/native/appkit/AppKitDropTarget.h +34 -0
- package/native/appkit/AppKitDropTarget.mm +292 -0
- package/native/appkit/AppKitFocus.h +120 -0
- package/native/appkit/AppKitFocus.mm +293 -0
- package/native/appkit/AppKitImageLoader.h +80 -0
- package/native/appkit/AppKitImageLoader.mm +156 -0
- package/native/appkit/AppKitKeyEvents.h +26 -0
- package/native/appkit/AppKitKeyEvents.mm +97 -0
- package/native/appkit/AppKitMenuBar.mm +424 -0
- package/native/appkit/AppKitMountingManager.h +135 -0
- package/native/appkit/AppKitMountingManager.mm +946 -0
- package/native/appkit/AppKitRunLoopObserver.h +34 -0
- package/native/appkit/AppKitRunLoopObserver.mm +42 -0
- package/native/appkit/AppKitScrollView.h +168 -0
- package/native/appkit/AppKitScrollView.mm +550 -0
- package/native/appkit/AppKitSkiaContext.h +33 -0
- package/native/appkit/AppKitSkiaContext.mm +255 -0
- package/native/appkit/AppKitSkiaModule.h +53 -0
- package/native/appkit/AppKitSkiaModule.mm +81 -0
- package/native/appkit/AppKitSkiaPeer.h +37 -0
- package/native/appkit/AppKitSkiaPeer.mm +123 -0
- package/native/appkit/AppKitSkiaView.h +62 -0
- package/native/appkit/AppKitSnapshot.h +21 -0
- package/native/appkit/AppKitSnapshot.mm +91 -0
- package/native/appkit/AppKitTextInput.h +140 -0
- package/native/appkit/AppKitTextInput.mm +588 -0
- package/native/appkit/AppKitTextPeer.h +74 -0
- package/native/appkit/AppKitTextPeer.mm +353 -0
- package/native/appkit/AppKitTitleBar.h +90 -0
- package/native/appkit/AppKitTitleBar.mm +186 -0
- package/native/appkit/AppKitTouchDispatcher.h +138 -0
- package/native/appkit/AppKitTouchDispatcher.mm +514 -0
- package/native/appkit/AppKitWindowControl.mm +222 -0
- package/native/appkit/AppKitWindowModule.h +62 -0
- package/native/appkit/AppKitWindowModule.mm +194 -0
- package/native/appkit/ColorSchemeAppKit.mm +71 -0
- package/native/appkit/ComponentRegistryAppKit.mm +121 -0
- package/native/appkit/CoreTextLayout.h +43 -0
- package/native/appkit/CoreTextLayout.mm +252 -0
- package/native/appkit/CoreTextLayoutManager.mm +138 -0
- package/native/appkit/FontRegistryCoreText.mm +148 -0
- package/native/appkit/PlatformServicesAppKit.mm +540 -0
- package/native/appkit/RnAppKitView.h +417 -0
- package/native/appkit/RnAppKitView.mm +1605 -0
- package/native/appkit/RnTextLayout.h +38 -0
- package/native/appkit/RnTextLayout.mm +168 -0
- package/native/appkit/demo_layout_appkit.mm +93 -0
- package/native/appkit/main_appkit.mm +1860 -0
- package/native/appkit/mount_harness_appkit.mm +197 -0
- package/native/tests/test_appkit_accessibility.mm +170 -0
- package/native/tests/test_appkit_image.mm +237 -0
- package/native/tests/test_appkit_input.mm +852 -0
- package/native/tests/test_appkit_key_events.mm +150 -0
- package/native/tests/test_appkit_mounting.mm +430 -0
- package/native/tests/test_appkit_scrollbar.mm +132 -0
- package/native/tests/test_appkit_services.mm +57 -0
- package/native/tests/test_appkit_skia.mm +212 -0
- package/native/tests/test_appkit_text.mm +180 -0
- package/native/tests/test_appkit_textinput.mm +748 -0
- package/native/tests/test_appkit_titlebar.mm +107 -0
- package/native/tests/test_appkit_view.mm +237 -0
- package/package.json +26 -0
- package/react-native.config.js +22 -0
|
@@ -0,0 +1,1860 @@
|
|
|
1
|
+
// basalt-core — the macOS host process.
|
|
2
|
+
//
|
|
3
|
+
// Constructs a ReactHost, loads a script into Hermes, and runs one surface in
|
|
4
|
+
// an NSWindow. Everything on screen from here on is produced by Fabric: the
|
|
5
|
+
// mutation stream arrives through AppKitMountingManager exactly as it does on iOS
|
|
6
|
+
// and Android, and nothing hand-builds a ShadowViewMutation.
|
|
7
|
+
//
|
|
8
|
+
// The counterpart of gtk/main.cpp, and deliberately the same shape. The pieces
|
|
9
|
+
// a host has to supply, and where each comes from:
|
|
10
|
+
//
|
|
11
|
+
// IMountingManager AppKitMountingManager (this repo)
|
|
12
|
+
// Input AppKitTouchDispatcher (this repo)
|
|
13
|
+
// RunLoopObserverManager ReactCxxPlatform (event beat)
|
|
14
|
+
// AnimationChoreographer AppKitAnimationChoreographer (this repo, display link)
|
|
15
|
+
// ContextContainer http + websocket client factories, below
|
|
16
|
+
// ComponentRegistryFactory ComponentRegistryAppKit (via the mounting manager)
|
|
17
|
+
// FontRegistry FontRegistryCoreText (this repo, link-time seam)
|
|
18
|
+
//
|
|
19
|
+
// Threading: AppKit owns this thread. ReactHost spins up its own JS thread and
|
|
20
|
+
// every mount is marshalled back here by AppKitMountingManager. Nothing below
|
|
21
|
+
// touches a view off the main thread.
|
|
22
|
+
//
|
|
23
|
+
// What this host can render is what AppKitMountingManager can mount, which today
|
|
24
|
+
// is <View>. `js/demo.js` -- a surface driven straight through
|
|
25
|
+
// nativeFabricUIManager, with no React and no react-native JavaScript -- is
|
|
26
|
+
// therefore the bundle this runs, and is the same first light-up the GTK host
|
|
27
|
+
// had. A React app needs the JavaScript platform layer to answer to a name
|
|
28
|
+
// other than "linux", which is its own piece of work.
|
|
29
|
+
|
|
30
|
+
#import "AppKitAnimationChoreographer.h"
|
|
31
|
+
#import "AppKitMountingManager.h"
|
|
32
|
+
|
|
33
|
+
#include <react/io/ImageLoaderModule.h>
|
|
34
|
+
|
|
35
|
+
#include "AppIdentity.h"
|
|
36
|
+
#include "AppPaths.h"
|
|
37
|
+
#include "DevMenu.h"
|
|
38
|
+
#include "PointerButtons.h"
|
|
39
|
+
#include "WindowControl.h"
|
|
40
|
+
#include "WindowHost.h"
|
|
41
|
+
#include "DialogModule.h"
|
|
42
|
+
#include "MenuModel.h"
|
|
43
|
+
#include "MenuModule.h"
|
|
44
|
+
#include "WindowsModule.h"
|
|
45
|
+
#include "TestKeys.h"
|
|
46
|
+
#include "TestQuitFile.h"
|
|
47
|
+
#ifdef BASALT_HAS_SKIA
|
|
48
|
+
#include "AppKitSkiaModule.h"
|
|
49
|
+
#endif
|
|
50
|
+
#include "TestSettle.h"
|
|
51
|
+
#import "AppKitRunLoopObserver.h"
|
|
52
|
+
#import "AppKitFocus.h"
|
|
53
|
+
#import "AppKitTouchDispatcher.h"
|
|
54
|
+
#import "AppKitSnapshot.h"
|
|
55
|
+
#import "RnAppKitView.h"
|
|
56
|
+
|
|
57
|
+
#include "AppearanceModule.h"
|
|
58
|
+
#include "BlobModule.h"
|
|
59
|
+
#include "CoreModules.h"
|
|
60
|
+
#include "LogBoxSurface.h"
|
|
61
|
+
|
|
62
|
+
#include <react/renderer/animated/NativeAnimatedNodesManagerProvider.h>
|
|
63
|
+
#include "ColorScheme.h"
|
|
64
|
+
#include "DragAndDrop.h"
|
|
65
|
+
|
|
66
|
+
#import "AppKitDropTarget.h"
|
|
67
|
+
#import "AppKitTitleBar.h"
|
|
68
|
+
#import "AppKitWindowModule.h"
|
|
69
|
+
#include "DevBundle.h"
|
|
70
|
+
|
|
71
|
+
#include <algorithm>
|
|
72
|
+
#include <csignal>
|
|
73
|
+
#include "ExpoModules.h"
|
|
74
|
+
#include "GestureHandlerModule.h"
|
|
75
|
+
#include "ReanimatedModule.h"
|
|
76
|
+
#include "JsRuntimeAccess.h"
|
|
77
|
+
#include "KeyEvents.h"
|
|
78
|
+
#include "PackageModules.h"
|
|
79
|
+
#include "UIManagerAccess.h"
|
|
80
|
+
#include "WorkletsModule.h"
|
|
81
|
+
#include "ExpoRuntime.h"
|
|
82
|
+
#include "TurboModuleProxy.h"
|
|
83
|
+
#include "PlatformConstantsModule.h"
|
|
84
|
+
#include "SourceCodeModule.h"
|
|
85
|
+
#include "StatusBarModule.h"
|
|
86
|
+
|
|
87
|
+
#include <glog/logging.h>
|
|
88
|
+
#include <jsi/jsi.h>
|
|
89
|
+
#include <logger/react_native_log.h>
|
|
90
|
+
#include <react/featureflags/ReactNativeFeatureFlags.h>
|
|
91
|
+
#include <react/featureflags/ReactNativeFeatureFlagsDefaults.h>
|
|
92
|
+
#include <react/http/IHttpClient.h>
|
|
93
|
+
#include <react/http/IWebSocketClient.h>
|
|
94
|
+
#include <react/logging/DefaultOnJsErrorHandler.h>
|
|
95
|
+
#include <react/renderer/core/LayoutConstraints.h>
|
|
96
|
+
#include <react/renderer/core/LayoutContext.h>
|
|
97
|
+
#include <react/runtime/ReactHost.h>
|
|
98
|
+
#include <react/runtime/ReactInstanceConfig.h>
|
|
99
|
+
#include <react/utils/ContextContainer.h>
|
|
100
|
+
#include <react/utils/RunLoopObserverManager.h>
|
|
101
|
+
|
|
102
|
+
#include <cstdlib>
|
|
103
|
+
#include <exception>
|
|
104
|
+
#include <memory>
|
|
105
|
+
#include <string>
|
|
106
|
+
|
|
107
|
+
using facebook::react::ContextContainer;
|
|
108
|
+
using facebook::react::Float;
|
|
109
|
+
using facebook::react::LayoutConstraints;
|
|
110
|
+
using facebook::react::LayoutContext;
|
|
111
|
+
using facebook::react::LayoutDirection;
|
|
112
|
+
using facebook::react::ReactHost;
|
|
113
|
+
using facebook::react::ReactInstanceConfig;
|
|
114
|
+
using facebook::react::RunLoopObserverManager;
|
|
115
|
+
using facebook::react::SurfaceId;
|
|
116
|
+
|
|
117
|
+
namespace {
|
|
118
|
+
|
|
119
|
+
// In Fabric a SurfaceId *is* the root shadow node's tag, which is what lets the
|
|
120
|
+
// surface root live in the mounting manager's registry like any other view.
|
|
121
|
+
constexpr SurfaceId kSurfaceId = 1;
|
|
122
|
+
|
|
123
|
+
// React Native's error inspector, which is a surface of its own -- registered
|
|
124
|
+
// by AppRegistry under the name "LogBox", exactly as an app registers its own
|
|
125
|
+
// component. Started on top of the app's when NativeLogBox.show() asks; see
|
|
126
|
+
// core/LogBoxSurface.h.
|
|
127
|
+
constexpr SurfaceId kLogBoxSurfaceId = 2;
|
|
128
|
+
|
|
129
|
+
constexpr int kInitialWidth = 900;
|
|
130
|
+
constexpr int kInitialHeight = 700;
|
|
131
|
+
|
|
132
|
+
// The entry point for the *scriptless* mode, where the bundle is a hand-written
|
|
133
|
+
// script talking to nativeFabricUIManager directly rather than a React app.
|
|
134
|
+
// See js/demo.js. Only used when no module name is given.
|
|
135
|
+
constexpr const char *kRenderFunctionName = "basaltRender";
|
|
136
|
+
|
|
137
|
+
// One window, and the surface in it.
|
|
138
|
+
//
|
|
139
|
+
// A window is a surface: a surface is what has a size, a layout context and a
|
|
140
|
+
// root shadow node, and two windows sharing one would be two windows sharing a
|
|
141
|
+
// layout. So the id a window is known by *is* its surface id. See
|
|
142
|
+
// core/WindowHost.h.
|
|
143
|
+
//
|
|
144
|
+
// Everything here used to be a field on Host, because there was only ever one.
|
|
145
|
+
// What is still on Host is what is genuinely per-process, and what this project
|
|
146
|
+
// has not made per-window yet: the title bar and the error inspector are the
|
|
147
|
+
// main window's.
|
|
148
|
+
struct HostWindow {
|
|
149
|
+
facebook::react::SurfaceId surfaceId{0};
|
|
150
|
+
NSWindow *window{nil};
|
|
151
|
+
RnAppKitView *root{nil};
|
|
152
|
+
// The window's contentView, holding the app's surface root and -- when the
|
|
153
|
+
// error inspector is showing -- a second root above it.
|
|
154
|
+
//
|
|
155
|
+
// A container rather than making the app root the contentView and adding the
|
|
156
|
+
// inspector inside it: `insertRnChild:atIndex:` indexes into `subviews`, so a
|
|
157
|
+
// view the mutation stream did not put there would shift every later Insert
|
|
158
|
+
// by one.
|
|
159
|
+
NSView *container{nil};
|
|
160
|
+
std::unique_ptr<basalt::AppKitTouchDispatcher> touchDispatcher;
|
|
161
|
+
std::unique_ptr<basalt::AppKitFocusManager> focusManager;
|
|
162
|
+
int scaleFactor{1};
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
struct Host {
|
|
166
|
+
// Every window, main one first. Never empty once the app has started.
|
|
167
|
+
std::vector<std::unique_ptr<HostWindow>> windows;
|
|
168
|
+
|
|
169
|
+
// The window the app was started in. Shorthand for windows.front(), which is
|
|
170
|
+
// what every part of this host that is not yet per-window uses.
|
|
171
|
+
// An empty window with null fields before there is a real one, rather than
|
|
172
|
+
// dereferencing an empty vector. Several things here run before the window
|
|
173
|
+
// exists -- a repaint request, the spinner timer -- and each one already
|
|
174
|
+
// guards on `window == nullptr`, which is exactly what this keeps true.
|
|
175
|
+
HostWindow &main() {
|
|
176
|
+
static HostWindow none;
|
|
177
|
+
return windows.empty() ? none : *windows.front();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
HostWindow *windowFor(facebook::react::SurfaceId surfaceId) {
|
|
181
|
+
for (const auto &candidate : windows) {
|
|
182
|
+
if (candidate->surfaceId == surfaceId) {
|
|
183
|
+
return candidate.get();
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return nullptr;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
std::shared_ptr<basalt::AppKitMountingManager> mountingManager;
|
|
190
|
+
std::shared_ptr<RunLoopObserverManager> runLoopObserverManager;
|
|
191
|
+
std::shared_ptr<basalt::AppKitAnimationChoreographer> choreographer;
|
|
192
|
+
// The error inspector's own surface root, or nil when it is not showing.
|
|
193
|
+
RnAppKitView *logBoxRoot{nil};
|
|
194
|
+
std::unique_ptr<ReactHost> reactHost;
|
|
195
|
+
|
|
196
|
+
CFRunLoopObserverRef runLoopObserver{nullptr};
|
|
197
|
+
|
|
198
|
+
std::string bundlePath;
|
|
199
|
+
// Empty means the bundle is a raw Fabric script rather than a React app.
|
|
200
|
+
std::string moduleName;
|
|
201
|
+
// Metro's entry point, without the extension. Only meaningful in dev mode.
|
|
202
|
+
std::string sourcePath;
|
|
203
|
+
bool surfaceStarted{false};
|
|
204
|
+
// Whether this run is a development one, which is the only thing that decides
|
|
205
|
+
// whether Cmd+D opens anything. Kept because the ReactInstanceConfig it came
|
|
206
|
+
// from is not.
|
|
207
|
+
bool devMode{false};
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
Host gHost;
|
|
211
|
+
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
// Logging
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
|
|
216
|
+
// ReactHost's Logger is where console.log and RN's own logging come out.
|
|
217
|
+
void logToConsole(const std::string &message, unsigned int logLevel) {
|
|
218
|
+
switch (logLevel) {
|
|
219
|
+
case ReactNativeLogLevelFatal:
|
|
220
|
+
case ReactNativeLogLevelError:
|
|
221
|
+
NSLog(@"[js error] %s", message.c_str());
|
|
222
|
+
break;
|
|
223
|
+
case ReactNativeLogLevelWarning:
|
|
224
|
+
NSLog(@"[js warn] %s", message.c_str());
|
|
225
|
+
break;
|
|
226
|
+
default:
|
|
227
|
+
NSLog(@"[js] %s", message.c_str());
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ---------------------------------------------------------------------------
|
|
233
|
+
// Layout constraints
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
|
|
236
|
+
// Minimum == maximum pins the root to the window's content box, which is what
|
|
237
|
+
// a full-window surface wants: the root fills, and flex children divide it.
|
|
238
|
+
LayoutConstraints constraintsFor(int width, int height) {
|
|
239
|
+
// Fully qualified: Carbon's MacTypes.h defines a `Size` of its own -- a bare
|
|
240
|
+
// `long` -- and AppKit pulls it in, so an unqualified name here resolves to
|
|
241
|
+
// something that is not a size at all.
|
|
242
|
+
const facebook::react::Size size{static_cast<Float>(width), static_cast<Float>(height)};
|
|
243
|
+
return LayoutConstraints{
|
|
244
|
+
.minimumSize = size,
|
|
245
|
+
.maximumSize = size,
|
|
246
|
+
.layoutDirection = LayoutDirection::LeftToRight,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
LayoutContext layoutContextFor(int scaleFactor) {
|
|
251
|
+
return LayoutContext{.pointScaleFactor = static_cast<Float>(scaleFactor)};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ---------------------------------------------------------------------------
|
|
255
|
+
// Calling into JS
|
|
256
|
+
// ---------------------------------------------------------------------------
|
|
257
|
+
|
|
258
|
+
// Runs on the JS thread. `step` lets the script commit a second, different tree
|
|
259
|
+
// so an Update/Remove diff can be seen coming out of Fabric rather than only an
|
|
260
|
+
// initial mount.
|
|
261
|
+
void callRenderFunction(int step) {
|
|
262
|
+
if (gHost.reactHost == nullptr) {
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
gHost.reactHost->runOnRuntimeScheduler([step](facebook::jsi::Runtime &runtime) {
|
|
266
|
+
try {
|
|
267
|
+
auto value = runtime.global().getProperty(runtime, kRenderFunctionName);
|
|
268
|
+
if (!value.isObject() || !value.getObject(runtime).isFunction(runtime)) {
|
|
269
|
+
NSLog(@"script defines no global %s()", kRenderFunctionName);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
value.getObject(runtime).getFunction(runtime).call(
|
|
273
|
+
runtime,
|
|
274
|
+
facebook::jsi::Value(static_cast<int>(kSurfaceId)),
|
|
275
|
+
facebook::jsi::Value(step));
|
|
276
|
+
} catch (const facebook::jsi::JSError &error) {
|
|
277
|
+
NSLog(@"%s() threw: %s\n%s",
|
|
278
|
+
kRenderFunctionName,
|
|
279
|
+
error.getMessage().c_str(),
|
|
280
|
+
error.getStack().c_str());
|
|
281
|
+
} catch (const std::exception &error) {
|
|
282
|
+
NSLog(@"%s() failed: %s", kRenderFunctionName, error.what());
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// ---------------------------------------------------------------------------
|
|
288
|
+
// Startup
|
|
289
|
+
// ---------------------------------------------------------------------------
|
|
290
|
+
|
|
291
|
+
// Feature flags this platform states explicitly. Anything not listed keeps
|
|
292
|
+
// React Native's own default.
|
|
293
|
+
class DesktopFeatureFlags : public facebook::react::ReactNativeFeatureFlagsDefaults {
|
|
294
|
+
public:
|
|
295
|
+
bool enableBridgelessArchitecture() override {
|
|
296
|
+
return true;
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
// TurboModules this platform supplies itself. ReactCxxTurboModuleProvider
|
|
301
|
+
// consults these before its own, so naming a module it also provides replaces
|
|
302
|
+
// it. All three come from core/ and are shared with the GTK host.
|
|
303
|
+
facebook::react::TurboModuleProviders makeTurboModuleProviders(
|
|
304
|
+
std::string scriptURL,
|
|
305
|
+
bool devMode,
|
|
306
|
+
std::weak_ptr<basalt::AppKitImageLoader> imageLoader) {
|
|
307
|
+
facebook::react::TurboModuleProviders providers;
|
|
308
|
+
// Whatever the discovered capability packages supply, first: a package naming
|
|
309
|
+
// a module this host also provides is replacing it deliberately, and the same
|
|
310
|
+
// rule already applies to this list against ReactCxxPlatform's own. Answers
|
|
311
|
+
// nullptr for every name when no package supplies one, which is most builds.
|
|
312
|
+
// See core/PackageModules.h.
|
|
313
|
+
providers.emplace_back(basalt::packageTurboModule);
|
|
314
|
+
providers.emplace_back(
|
|
315
|
+
[scriptURL = std::move(scriptURL), devMode, imageLoader = std::move(imageLoader)](
|
|
316
|
+
const std::string &name,
|
|
317
|
+
const std::shared_ptr<facebook::react::CallInvoker> &jsInvoker)
|
|
318
|
+
-> std::shared_ptr<facebook::react::TurboModule> {
|
|
319
|
+
if (name == facebook::react::PlatformConstantsModule::kModuleName) {
|
|
320
|
+
return std::make_shared<basalt::DesktopPlatformConstantsModule>(jsInvoker);
|
|
321
|
+
}
|
|
322
|
+
// `Image.getSize` and `Image.prefetch`. Upstream builds this module
|
|
323
|
+
// with no loader at all and offers no way to supply one, so the answer
|
|
324
|
+
// is to build it here with the loader this host already has -- which
|
|
325
|
+
// is also the one holding the decoded images, so a size asked for
|
|
326
|
+
// something on screen costs nothing. See docs/backlog/upstream.md.
|
|
327
|
+
if (name == facebook::react::ImageLoaderModule::kModuleName) {
|
|
328
|
+
return std::make_shared<facebook::react::ImageLoaderModule>(jsInvoker, imageLoader);
|
|
329
|
+
}
|
|
330
|
+
if (name == basalt::DesktopAppModule::kModuleName) {
|
|
331
|
+
return std::make_shared<basalt::DesktopAppModule>(jsInvoker);
|
|
332
|
+
}
|
|
333
|
+
if (name == basalt::DesktopAppearanceModule::kModuleName) {
|
|
334
|
+
return std::make_shared<basalt::DesktopAppearanceModule>(jsInvoker);
|
|
335
|
+
}
|
|
336
|
+
if (name == basalt::DesktopBlobModule::kModuleName) {
|
|
337
|
+
return std::make_shared<basalt::DesktopBlobModule>(jsInvoker);
|
|
338
|
+
}
|
|
339
|
+
if (name == basalt::DesktopFileReaderModule::kModuleName) {
|
|
340
|
+
return std::make_shared<basalt::DesktopFileReaderModule>(jsInvoker);
|
|
341
|
+
}
|
|
342
|
+
if (name == basalt::DesktopClipboardModule::kModuleName) {
|
|
343
|
+
return std::make_shared<basalt::DesktopClipboardModule>(jsInvoker);
|
|
344
|
+
}
|
|
345
|
+
if (name == basalt::DesktopVibrationModule::kModuleName) {
|
|
346
|
+
return std::make_shared<basalt::DesktopVibrationModule>(jsInvoker);
|
|
347
|
+
}
|
|
348
|
+
if (name == basalt::DesktopAlertModule::kModuleName) {
|
|
349
|
+
return std::make_shared<basalt::DesktopAlertModule>(jsInvoker);
|
|
350
|
+
}
|
|
351
|
+
if (name == basalt::DesktopLinkingModule::kModuleName) {
|
|
352
|
+
return std::make_shared<basalt::DesktopLinkingModule>(jsInvoker);
|
|
353
|
+
}
|
|
354
|
+
if (name == basalt::DesktopShareModule::kModuleName) {
|
|
355
|
+
return std::make_shared<basalt::DesktopShareModule>(jsInvoker);
|
|
356
|
+
}
|
|
357
|
+
// The native file dialogs, which React Native has no API for and which
|
|
358
|
+
// are the first thing a desktop app reaches for. See core/DialogModule.h.
|
|
359
|
+
if (name == basalt::DesktopDialogModule::kModuleName) {
|
|
360
|
+
return std::make_shared<basalt::DesktopDialogModule>(jsInvoker);
|
|
361
|
+
}
|
|
362
|
+
// The application menu, which on macOS is also what makes Cmd-C reach
|
|
363
|
+
// a text field. See core/MenuModel.h.
|
|
364
|
+
if (name == basalt::DesktopMenuModule::kModuleName) {
|
|
365
|
+
return std::make_shared<basalt::DesktopMenuModule>(jsInvoker);
|
|
366
|
+
}
|
|
367
|
+
// More than one window, which is more than one surface. See
|
|
368
|
+
// core/WindowHost.h.
|
|
369
|
+
if (name == basalt::DesktopWindowsModule::kModuleName) {
|
|
370
|
+
return std::make_shared<basalt::DesktopWindowsModule>(jsInvoker);
|
|
371
|
+
}
|
|
372
|
+
if (name == basalt::DesktopI18nManagerModule::kModuleName) {
|
|
373
|
+
return std::make_shared<basalt::DesktopI18nManagerModule>(jsInvoker);
|
|
374
|
+
}
|
|
375
|
+
if (name == basalt::DesktopAccessibilityManagerModule::kModuleName) {
|
|
376
|
+
return std::make_shared<basalt::DesktopAccessibilityManagerModule>(jsInvoker);
|
|
377
|
+
}
|
|
378
|
+
if (name == basalt::DesktopAccessibilityInfoModule::kModuleName) {
|
|
379
|
+
return std::make_shared<basalt::DesktopAccessibilityInfoModule>(jsInvoker);
|
|
380
|
+
}
|
|
381
|
+
if (name == basalt::DesktopToastModule::kModuleName) {
|
|
382
|
+
return std::make_shared<basalt::DesktopToastModule>(jsInvoker);
|
|
383
|
+
}
|
|
384
|
+
// react-native-gesture-handler's module. Offered whether or not the app
|
|
385
|
+
// has the library: it costs a name comparison, and the alternative is a
|
|
386
|
+
// host that has to be rebuilt to run an app that uses gestures.
|
|
387
|
+
if (name == basalt::DesktopGestureHandlerModule::kModuleName) {
|
|
388
|
+
return std::make_shared<basalt::DesktopGestureHandlerModule>(jsInvoker);
|
|
389
|
+
}
|
|
390
|
+
#ifdef BASALT_HAS_SKIA
|
|
391
|
+
// @shopify/react-native-skia, when the build was pointed at an app that
|
|
392
|
+
// has it. Its `install` is what puts Skia's whole JavaScript API on the
|
|
393
|
+
// global object; see appkit/AppKitSkiaModule.h.
|
|
394
|
+
if (name == basalt::AppKitSkiaModule::kModuleName) {
|
|
395
|
+
return std::make_shared<basalt::AppKitSkiaModule>(jsInvoker);
|
|
396
|
+
}
|
|
397
|
+
#endif
|
|
398
|
+
#ifdef BASALT_HAS_WORKLETS
|
|
399
|
+
// react-native-worklets, when the build was pointed at one. Reanimated
|
|
400
|
+
// is built on it; see core/WorkletsModule.h.
|
|
401
|
+
if (name == basalt::DesktopWorkletsModule::kModuleName) {
|
|
402
|
+
return std::make_shared<basalt::DesktopWorkletsModule>(jsInvoker);
|
|
403
|
+
}
|
|
404
|
+
#endif
|
|
405
|
+
#ifdef BASALT_HAS_REANIMATED
|
|
406
|
+
if (name == basalt::DesktopReanimatedModule::kModuleName) {
|
|
407
|
+
return std::make_shared<basalt::DesktopReanimatedModule>(jsInvoker);
|
|
408
|
+
}
|
|
409
|
+
#endif
|
|
410
|
+
// Only outside dev mode. ReactCxxPlatform provides a real DevSettings
|
|
411
|
+
// when a dev server exists, and this provider is consulted first -- so
|
|
412
|
+
// offering ours unconditionally would shadow the one that works.
|
|
413
|
+
if (!devMode && name == basalt::DesktopDevSettingsModule::kModuleName) {
|
|
414
|
+
return std::make_shared<basalt::DesktopDevSettingsModule>(jsInvoker);
|
|
415
|
+
}
|
|
416
|
+
if (name == basalt::DesktopStatusBarModule::kModuleName) {
|
|
417
|
+
return std::make_shared<basalt::DesktopStatusBarModule>(jsInvoker);
|
|
418
|
+
}
|
|
419
|
+
// The title bar. Reaches the window through the one AppKitTitleBar,
|
|
420
|
+
// which the host attached at startup -- the same arrangement the
|
|
421
|
+
// Windows host has, and for the same reason: a module cannot be handed
|
|
422
|
+
// a window it is created before.
|
|
423
|
+
if (name == basalt::AppKitWindowModule::kModuleName) {
|
|
424
|
+
return std::make_shared<basalt::AppKitWindowModule>(jsInvoker);
|
|
425
|
+
}
|
|
426
|
+
// Only outside dev mode, for the same reason DevSettings is above.
|
|
427
|
+
//
|
|
428
|
+
// This module's scriptURL is what HMRClient registers with Metro as its
|
|
429
|
+
// entry point, and Metro resolves that URL to a *graph*. The URL
|
|
430
|
+
// synthesised here is not the one DevServerHelper actually fetched --
|
|
431
|
+
// that one carries lazy, minify, runModule and app -- so it names a
|
|
432
|
+
// graph nobody built, Metro answers GraphNotFoundError, and Fast
|
|
433
|
+
// Refresh silently never starts. ReactCxxPlatform's own SourceCode
|
|
434
|
+
// module reports the URL the bundle really came from, which is the one
|
|
435
|
+
// whose graph exists.
|
|
436
|
+
if (!devMode && name == basalt::DesktopSourceCodeModule::kModuleName) {
|
|
437
|
+
return std::make_shared<basalt::DesktopSourceCodeModule>(jsInvoker, scriptURL);
|
|
438
|
+
}
|
|
439
|
+
return nullptr;
|
|
440
|
+
});
|
|
441
|
+
return providers;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Whether the window's first responder is one of this project's views, or sits
|
|
445
|
+
// inside one.
|
|
446
|
+
//
|
|
447
|
+
// Inside one matters: a focused `<TextInput>` is an NSTextField the mounting
|
|
448
|
+
// manager put *inside* an RnAppKitView, so asking only whether the responder is
|
|
449
|
+
// an RnAppKitView would answer no for every text field and let a window-level
|
|
450
|
+
// shortcut eat what the field was typing.
|
|
451
|
+
static BOOL basaltViewInResponderChain(NSWindow *window) {
|
|
452
|
+
NSResponder *responder = window.firstResponder;
|
|
453
|
+
if (![responder isKindOfClass:[NSView class]]) {
|
|
454
|
+
return NO;
|
|
455
|
+
}
|
|
456
|
+
for (NSView *view = (NSView *)responder; view != nil; view = view.superview) {
|
|
457
|
+
if ([view isKindOfClass:[RnAppKitView class]]) {
|
|
458
|
+
return YES;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return NO;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Runs against the JavaScript runtime before the bundle is evaluated, which is
|
|
465
|
+
// the only moment early enough for what goes in here.
|
|
466
|
+
void installBindings(facebook::jsi::Runtime &runtime) {
|
|
467
|
+
// Before Expo's, and before anything else that might look a module up: this is
|
|
468
|
+
// what makes TurboModuleRegistry's first question answerable on a bridgeless
|
|
469
|
+
// runtime. See core/TurboModuleProxy.h.
|
|
470
|
+
basalt::installTurboModuleProxy(runtime);
|
|
471
|
+
basalt::installExpoRuntime(runtime);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
std::shared_ptr<const ContextContainer> makeContextContainer() {
|
|
475
|
+
auto contextContainer = std::make_shared<ContextContainer>();
|
|
476
|
+
|
|
477
|
+
// ReactHost throws without these two. Both are real implementations --
|
|
478
|
+
// curl-backed http from core/HttpClient.cpp, and React Native's own
|
|
479
|
+
// boost::beast websocket client -- and neither is about a toolkit, which is
|
|
480
|
+
// why the macOS host gets them for free.
|
|
481
|
+
contextContainer->insert(facebook::react::HttpClientFactoryKey,
|
|
482
|
+
facebook::react::getHttpClientFactory());
|
|
483
|
+
contextContainer->insert(facebook::react::WebSocketClientFactoryKey,
|
|
484
|
+
facebook::react::getWebSocketClientFactory());
|
|
485
|
+
|
|
486
|
+
// MessageQueueThreadFactoryKey is deliberately left unset: ReactHost then
|
|
487
|
+
// installs MessageQueueThreadImpl, a real threaded queue.
|
|
488
|
+
return contextContainer;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// BASALT_DUMP_TREE: write the view tree to a file on the way out, so an
|
|
492
|
+
// automated run can assert on what React actually produced rather than on a
|
|
493
|
+
// screenshot. The same idea as BASALT_DUMP_TREE, and the prefixes should
|
|
494
|
+
// eventually be one.
|
|
495
|
+
// BASALT_DUMP_MENU: write the application menu that is actually installed to a
|
|
496
|
+
// file on the way out.
|
|
497
|
+
//
|
|
498
|
+
// Read back from AppKit rather than from the model that was sent, which is the
|
|
499
|
+
// point: it says a description became a real NSMenu with the shortcuts the
|
|
500
|
+
// platform attached to its roles -- and it is the only way an automated run can
|
|
501
|
+
// see a menu bar at all, since a menu cannot be opened without a person.
|
|
502
|
+
void dumpMenuIfRequested() {
|
|
503
|
+
const char *path = getenv("BASALT_DUMP_MENU");
|
|
504
|
+
if (path == nullptr) {
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
const std::string described = basalt::describeApplicationMenu();
|
|
508
|
+
NSString *text = [NSString stringWithUTF8String:described.c_str()];
|
|
509
|
+
NSError *error = nil;
|
|
510
|
+
[text != nil ? text : @"" writeToFile:[NSString stringWithUTF8String:path]
|
|
511
|
+
atomically:YES
|
|
512
|
+
encoding:NSUTF8StringEncoding
|
|
513
|
+
error:&error];
|
|
514
|
+
if (error != nil) {
|
|
515
|
+
NSLog(@"could not write the menu dump: %@", error.localizedDescription);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
void dumpTreeIfRequested() {
|
|
520
|
+
const char *path = getenv("BASALT_DUMP_TREE");
|
|
521
|
+
if (path == nullptr || gHost.main().root == nil) {
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
NSError *error = nil;
|
|
525
|
+
NSMutableString *description = [[gHost.main().root describeTree] mutableCopy];
|
|
526
|
+
// Every other window, each under a header naming its surface. Appended rather
|
|
527
|
+
// than merged for the same reason the inspector is: they are separate trees
|
|
528
|
+
// on screen, and nesting one inside another would say something untrue.
|
|
529
|
+
for (const auto &other : gHost.windows) {
|
|
530
|
+
if (other->surfaceId == kSurfaceId || other->root == nil) {
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
[description appendFormat:@"--- window %d ---\n", (int)other->surfaceId];
|
|
534
|
+
[description appendString:[other->root describeTree]];
|
|
535
|
+
}
|
|
536
|
+
// The error inspector is a second surface with a root of its own, so it is
|
|
537
|
+
// invisible to a dump of the app's. Appended rather than merged, because the
|
|
538
|
+
// two are siblings on screen and nesting one inside the other would say
|
|
539
|
+
// something untrue about the tree.
|
|
540
|
+
if (gHost.logBoxRoot != nil) {
|
|
541
|
+
[description appendString:@"--- LogBox ---\n"];
|
|
542
|
+
[description appendString:[gHost.logBoxRoot describeTree]];
|
|
543
|
+
}
|
|
544
|
+
if (![description writeToFile:[NSString stringWithUTF8String:path]
|
|
545
|
+
atomically:YES
|
|
546
|
+
encoding:NSUTF8StringEncoding
|
|
547
|
+
error:&error]) {
|
|
548
|
+
NSLog(@"could not write %s: %@", path, error);
|
|
549
|
+
} else {
|
|
550
|
+
NSLog(@"wrote view tree to %s", path);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Starts or stops React Native's error inspector.
|
|
555
|
+
//
|
|
556
|
+
// It is `LogBoxInspectorContainer`, registered by AppRegistry under the name
|
|
557
|
+
// "LogBox" exactly as an app registers its own component -- so this is a second
|
|
558
|
+
// surface rather than an overlay this host draws, and everything in it is React
|
|
559
|
+
// Native's own JavaScript. See core/LogBoxSurface.h.
|
|
560
|
+
//
|
|
561
|
+
// Started and stopped rather than kept and hidden: a surface that exists is a
|
|
562
|
+
// React tree that renders and re-renders, and the inspector is showing for a
|
|
563
|
+
// vanishingly small part of a session.
|
|
564
|
+
void hideLogBoxSurface() {
|
|
565
|
+
if (gHost.reactHost == nullptr || gHost.logBoxRoot == nil) {
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
gHost.reactHost->stopSurface(kLogBoxSurfaceId);
|
|
569
|
+
[gHost.logBoxRoot removeFromSuperview];
|
|
570
|
+
gHost.mountingManager->destroySurfaceRoot(kLogBoxSurfaceId);
|
|
571
|
+
gHost.logBoxRoot = nil;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
void showLogBoxSurface(const std::string &appKey) {
|
|
575
|
+
if (gHost.reactHost == nullptr || gHost.main().container == nil || gHost.logBoxRoot != nil) {
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const NSSize size = gHost.main().container.bounds.size;
|
|
580
|
+
const int width = (int)size.width;
|
|
581
|
+
const int height = (int)size.height;
|
|
582
|
+
|
|
583
|
+
gHost.logBoxRoot = gHost.mountingManager->createSurfaceRoot(kLogBoxSurfaceId);
|
|
584
|
+
[gHost.logBoxRoot setRnFrameX:0 y:0 width:width height:height];
|
|
585
|
+
// Added last, so it is above the app. AppKit paints subviews in order and
|
|
586
|
+
// hit-tests them in reverse, which is what makes the inspector take the
|
|
587
|
+
// presses that would otherwise reach the app behind it.
|
|
588
|
+
[gHost.main().container addSubview:gHost.logBoxRoot];
|
|
589
|
+
|
|
590
|
+
gHost.reactHost->startSurface(kLogBoxSurfaceId,
|
|
591
|
+
appKey,
|
|
592
|
+
folly::dynamic::object(),
|
|
593
|
+
constraintsFor(width, height),
|
|
594
|
+
layoutContextFor(gHost.main().scaleFactor));
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Closes any modal sheet before the application tries to go away.
|
|
598
|
+
//
|
|
599
|
+
// Not a testing concern, or not only one: `[NSApp terminate:]` does nothing at
|
|
600
|
+
// all while a sheet is up, so an app showing an `Alert.alert()` when a quit
|
|
601
|
+
// arrives simply does not quit -- no dialog dismissed, no callback, no exit.
|
|
602
|
+
// It looked like a hung automated run, which is how it was found, and it is the
|
|
603
|
+
// same hang a person would get from Cmd-Q.
|
|
604
|
+
//
|
|
605
|
+
// Ending the sheet runs its completion handler, so whatever was waiting on the
|
|
606
|
+
// answer is told the dialog closed rather than left holding a promise forever.
|
|
607
|
+
void endAnyOpenSheets() {
|
|
608
|
+
for (NSWindow *window in NSApp.windows) {
|
|
609
|
+
// A copy, because endSheet: mutates the array being walked.
|
|
610
|
+
for (NSWindow *sheet in [window.sheets copy]) {
|
|
611
|
+
[window endSheet:sheet];
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
void shutdown() {
|
|
617
|
+
// Before the surface stops, which tears the tree down.
|
|
618
|
+
dumpTreeIfRequested();
|
|
619
|
+
dumpMenuIfRequested();
|
|
620
|
+
|
|
621
|
+
if (gHost.choreographer != nullptr) {
|
|
622
|
+
gHost.choreographer->detach();
|
|
623
|
+
}
|
|
624
|
+
// Stop feeding the beat before the manager it points at is released.
|
|
625
|
+
basalt::removeRunLoopObserver(gHost.runLoopObserver);
|
|
626
|
+
gHost.runLoopObserver = nullptr;
|
|
627
|
+
|
|
628
|
+
// Every window, not only the app's own -- which is what this said when there
|
|
629
|
+
// was only ever one, and did not say when there stopped being.
|
|
630
|
+
//
|
|
631
|
+
// A dispatcher left alive past `mountingManager.reset()` below would read an
|
|
632
|
+
// event emitter out of a manager that has been freed, for any event AppKit
|
|
633
|
+
// still delivers while the windows come down. Clearing the back pointer in
|
|
634
|
+
// the dispatcher's own destructor does not cover this: the dispatcher is
|
|
635
|
+
// still there, and it is the manager that has gone.
|
|
636
|
+
for (const auto &window : gHost.windows) {
|
|
637
|
+
window->focusManager.reset();
|
|
638
|
+
window->touchDispatcher.reset();
|
|
639
|
+
}
|
|
640
|
+
gHost.logBoxRoot = nil;
|
|
641
|
+
// Before the host goes: work posted to a host on its way out must not run.
|
|
642
|
+
basalt::setRuntimeRunner(nullptr);
|
|
643
|
+
if (gHost.reactHost != nullptr) {
|
|
644
|
+
// Surfaces must stop before the host goes away, or teardown asserts.
|
|
645
|
+
gHost.reactHost->stopAllSurfaces();
|
|
646
|
+
gHost.surfaceStarted = false;
|
|
647
|
+
// Destroying the host joins the JS thread, so it has to happen while the
|
|
648
|
+
// mounting manager it holds is still alive.
|
|
649
|
+
gHost.reactHost.reset();
|
|
650
|
+
}
|
|
651
|
+
gHost.choreographer.reset();
|
|
652
|
+
gHost.runLoopObserverManager.reset();
|
|
653
|
+
// The window records before the manager that made their roots: each holds a
|
|
654
|
+
// borrowed root, and a record outliving the manager is the same hazard.
|
|
655
|
+
gHost.windows.clear();
|
|
656
|
+
gHost.mountingManager.reset();
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
} // namespace
|
|
660
|
+
|
|
661
|
+
// The window's delegate, and the application's. Resizing has to reach the
|
|
662
|
+
// surface or the root keeps the size it started with while the window changes
|
|
663
|
+
// around it; quitting has to run shutdown() or the JS thread is dropped rather
|
|
664
|
+
// than joined.
|
|
665
|
+
@interface RnAppKitHostDelegate : NSObject <NSWindowDelegate, NSApplicationDelegate>
|
|
666
|
+
@end
|
|
667
|
+
|
|
668
|
+
@implementation RnAppKitHostDelegate
|
|
669
|
+
|
|
670
|
+
// A move changes the bounds `useWindow()` reports and nothing else, so unlike a
|
|
671
|
+
// resize there is no surface to reconstrain -- which is exactly why it needs
|
|
672
|
+
// its own handler: the resize one would never run, and `center()` would look
|
|
673
|
+
// like it had done nothing.
|
|
674
|
+
// Closed by the person rather than by the app: its own close button, or the
|
|
675
|
+
// window manager. Without this the host keeps a record whose NSWindow is gone
|
|
676
|
+
// and the `<Window>` that opened it goes on believing it is open.
|
|
677
|
+
//
|
|
678
|
+
// Only for a window an app opened: the main window closing ends the process,
|
|
679
|
+
// which applicationShouldTerminateAfterLastWindowClosed already arranges.
|
|
680
|
+
// Somebody is trying to close a window: its close button, Cmd-W, or the window
|
|
681
|
+
// manager. AppKit asks before it acts, which is exactly the question an app with
|
|
682
|
+
// unsaved work wants to answer -- so a window that asked to be asked refuses
|
|
683
|
+
// here and tells the app instead.
|
|
684
|
+
//
|
|
685
|
+
// Every window, including the app's own. What this does *not* cover is Cmd-Q:
|
|
686
|
+
// terminating goes through applicationShouldTerminate:, which never asks a
|
|
687
|
+
// window whether it minds -- that is the handler further down.
|
|
688
|
+
- (BOOL)windowShouldClose:(NSWindow *)sender {
|
|
689
|
+
for (const auto &candidate : gHost.windows) {
|
|
690
|
+
if (candidate->window != sender) {
|
|
691
|
+
continue;
|
|
692
|
+
}
|
|
693
|
+
if (basalt::hostWindowCloseIntercepted(candidate->surfaceId)) {
|
|
694
|
+
// What happens next is the app's decision, and if that is "go ahead" it
|
|
695
|
+
// calls close() itself. See core/WindowHost.h for why the answer has to
|
|
696
|
+
// be available before the question is asked.
|
|
697
|
+
basalt::hostWindowCloseRequested(candidate->surfaceId);
|
|
698
|
+
return NO;
|
|
699
|
+
}
|
|
700
|
+
break;
|
|
701
|
+
}
|
|
702
|
+
return YES;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
- (void)windowWillClose:(NSNotification *)notification {
|
|
706
|
+
for (const auto &candidate : gHost.windows) {
|
|
707
|
+
if (candidate->window != notification.object || candidate->surfaceId == kSurfaceId) {
|
|
708
|
+
continue;
|
|
709
|
+
}
|
|
710
|
+
// Tell JavaScript, then take the window down the same way an app closing it
|
|
711
|
+
// would -- so there is one teardown path rather than two.
|
|
712
|
+
basalt::hostWindowClosed(candidate->surfaceId);
|
|
713
|
+
basalt::closeHostWindow(candidate->surfaceId);
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
- (void)windowDidMove:(NSNotification *)notification {
|
|
719
|
+
(void)notification;
|
|
720
|
+
basalt::notifyWindowBoundsChanged();
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
- (void)windowDidResize:(NSNotification *)notification {
|
|
724
|
+
if (!gHost.surfaceStarted) {
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
// Which window resized, which is the whole of what more than one changes
|
|
728
|
+
// here. Constraining `kSurfaceId` from every window would lay the *first*
|
|
729
|
+
// window's tree out to the second window's size, and the symptom would be a
|
|
730
|
+
// first window whose content jumped whenever a second one was dragged.
|
|
731
|
+
HostWindow *resized = nullptr;
|
|
732
|
+
for (const auto &candidate : gHost.windows) {
|
|
733
|
+
if (candidate->window == notification.object) {
|
|
734
|
+
resized = candidate.get();
|
|
735
|
+
break;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
if (resized == nullptr) {
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
const NSSize size = resized->window.contentView.bounds.size;
|
|
743
|
+
const int width = (int)size.width;
|
|
744
|
+
const int height = (int)size.height;
|
|
745
|
+
if (width <= 0 || height <= 0) {
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
[resized->root setRnFrameX:0 y:0 width:width height:height];
|
|
749
|
+
gHost.reactHost->setSurfaceConstraints(
|
|
750
|
+
resized->surfaceId, constraintsFor(width, height), layoutContextFor(resized->scaleFactor));
|
|
751
|
+
|
|
752
|
+
// A <Modal> is sized from its own shadow-node state rather than from a style,
|
|
753
|
+
// and React Native's C++ platform answers "what size is the screen" with
|
|
754
|
+
// zero. Told here, where the window's size is already being handed to Fabric.
|
|
755
|
+
gHost.mountingManager->setSurfaceSize((float)width, (float)height);
|
|
756
|
+
// The window moved or was resized, which `useWindow()` hands an app as live
|
|
757
|
+
// bounds. Here rather than in a watcher of its own: this is already the one
|
|
758
|
+
// place that learns it.
|
|
759
|
+
basalt::notifyWindowBoundsChanged();
|
|
760
|
+
|
|
761
|
+
// The inspector covers the main window, so it resizes with it -- and only
|
|
762
|
+
// with it: an error is about the app rather than about a window.
|
|
763
|
+
if (gHost.logBoxRoot != nil && resized->surfaceId == kSurfaceId) {
|
|
764
|
+
[gHost.logBoxRoot setRnFrameX:0 y:0 width:width height:height];
|
|
765
|
+
gHost.reactHost->setSurfaceConstraints(
|
|
766
|
+
kLogBoxSurfaceId, constraintsFor(width, height), layoutContextFor(gHost.main().scaleFactor));
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// The backing scale factor changes when a window moves between a Retina display
|
|
771
|
+
// and an external one. Yoga rounds to physical pixels, so telling it the wrong
|
|
772
|
+
// factor puts half-pixel seams between adjacent views.
|
|
773
|
+
- (void)windowDidChangeBackingProperties:(NSNotification *)notification {
|
|
774
|
+
(void)notification;
|
|
775
|
+
if (!gHost.surfaceStarted) {
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
gHost.main().scaleFactor = (int)gHost.main().window.backingScaleFactor;
|
|
779
|
+
const NSSize size = gHost.main().window.contentView.bounds.size;
|
|
780
|
+
gHost.reactHost->setSurfaceConstraints(kSurfaceId,
|
|
781
|
+
constraintsFor((int)size.width, (int)size.height),
|
|
782
|
+
layoutContextFor(gHost.main().scaleFactor));
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// Set by the BASALT_QUIT_AFTER_MS timer, read by applicationShouldTerminate:.
|
|
786
|
+
// A file-scope bool rather than anything in core: it is about this host's
|
|
787
|
+
// automation and means nothing to the platform.
|
|
788
|
+
static bool gQuittingForTest = false;
|
|
789
|
+
|
|
790
|
+
- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender {
|
|
791
|
+
(void)sender;
|
|
792
|
+
return YES;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// Somebody is trying to quit: Cmd-Q, the Quit menu item, or a logout. AppKit
|
|
796
|
+
// asks the delegate and asks no window, which is why guarding every window is
|
|
797
|
+
// not enough -- see core/WindowHost.h.
|
|
798
|
+
//
|
|
799
|
+
// `NSTerminateCancel` rather than `NSTerminateLater`. Later is the shape this
|
|
800
|
+
// looks like it wants, and it obliges the app to call
|
|
801
|
+
// `replyToApplicationShouldTerminate:` -- from the JavaScript thread, having
|
|
802
|
+
// woken React, decided, and hopped back -- with AppKit holding a modal run
|
|
803
|
+
// loop in the meantime. An app that is slow to answer, or that never answers
|
|
804
|
+
// because its handler threw, leaves a process that cannot be quit by any
|
|
805
|
+
// means short of kill. Cancel is a complete answer now, and the app quits
|
|
806
|
+
// itself when it is ready, which is exactly the arrangement the window half
|
|
807
|
+
// already uses.
|
|
808
|
+
// A monitor plugged in, unplugged, or rearranged -- and also a resolution
|
|
809
|
+
// change or the menu bar moving to another screen, which is why the name says
|
|
810
|
+
// "screen parameters" rather than "screens".
|
|
811
|
+
- (void)applicationDidChangeScreenParameters:(NSNotification *)notification {
|
|
812
|
+
(void)notification;
|
|
813
|
+
basalt::notifyDisplaysChanged();
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender {
|
|
817
|
+
(void)sender;
|
|
818
|
+
// The automation timer is not refusable. BASALT_QUIT_AFTER_MS quits by
|
|
819
|
+
// calling terminate:, which arrives here -- so a demo that registers a quit
|
|
820
|
+
// handler would refuse the harness, and every scenario that runs it would
|
|
821
|
+
// hang until its own timeout and report something other than what it is
|
|
822
|
+
// testing. The scenario for this feature is exactly such a demo.
|
|
823
|
+
if (gQuittingForTest) {
|
|
824
|
+
return NSTerminateNow;
|
|
825
|
+
}
|
|
826
|
+
if (!basalt::hostQuitIntercepted()) {
|
|
827
|
+
return NSTerminateNow;
|
|
828
|
+
}
|
|
829
|
+
basalt::hostQuitRequested();
|
|
830
|
+
return NSTerminateCancel;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
- (void)applicationWillTerminate:(NSNotification *)notification {
|
|
834
|
+
(void)notification;
|
|
835
|
+
shutdown();
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
@end
|
|
839
|
+
|
|
840
|
+
// The application menu an app gets before it has asked for one.
|
|
841
|
+
//
|
|
842
|
+
// Not a courtesy, and not only about Quit. AppKit gives the main menu every key
|
|
843
|
+
// event before the responder chain sees it, so a host with no Edit menu has no
|
|
844
|
+
// working Cmd-C: `copy:` never reaches the field editor of whichever text field
|
|
845
|
+
// has focus. This host had exactly one Quit item until core/MenuModel.h
|
|
846
|
+
// existed, and copy, cut, paste, undo and select-all did nothing in every
|
|
847
|
+
// <TextInput> on macOS as a result.
|
|
848
|
+
//
|
|
849
|
+
// An empty model is the default; see appkit/AppKitMenuBar.mm for what it
|
|
850
|
+
// contains and why each item is in it.
|
|
851
|
+
static void installMainMenu(void) {
|
|
852
|
+
basalt::setApplicationMenu(basalt::MenuModel{}, nullptr);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// Makes a window, a surface root for it, and the input that drives them.
|
|
856
|
+
//
|
|
857
|
+
// Called for the main window and for every one an app opens afterwards, which
|
|
858
|
+
// is the point: the second window is not a special case of the first, it is the
|
|
859
|
+
// same function with a different surface id. See core/WindowHost.h.
|
|
860
|
+
//
|
|
861
|
+
// The window is not shown and the surface is not started here; both are the
|
|
862
|
+
// caller's business.
|
|
863
|
+
HostWindow *createHostWindow(facebook::react::SurfaceId surfaceId,
|
|
864
|
+
NSString *title,
|
|
865
|
+
int width,
|
|
866
|
+
int height,
|
|
867
|
+
id<NSWindowDelegate> delegate) {
|
|
868
|
+
// The first window through here is the app's own, and the title bar belongs
|
|
869
|
+
// to it alone: it is a process-wide seam -- one title, one style -- and
|
|
870
|
+
// making it per-window is its own piece of work. See docs/BACKLOG.md.
|
|
871
|
+
const bool isMainWindow = gHost.windows.empty();
|
|
872
|
+
|
|
873
|
+
auto owned = std::make_unique<HostWindow>();
|
|
874
|
+
HostWindow *made = owned.get();
|
|
875
|
+
made->surfaceId = surfaceId;
|
|
876
|
+
|
|
877
|
+
made->window = [[NSWindow alloc]
|
|
878
|
+
initWithContentRect:NSMakeRect(0, 0, width, height)
|
|
879
|
+
styleMask:NSWindowStyleMaskTitled | NSWindowStyleMaskClosable |
|
|
880
|
+
NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable
|
|
881
|
+
backing:NSBackingStoreBuffered
|
|
882
|
+
defer:NO];
|
|
883
|
+
made->window.title = title;
|
|
884
|
+
made->window.delegate = delegate;
|
|
885
|
+
// Released when closed would free the window out from under this record. The
|
|
886
|
+
// host owns every window it makes and closes them through closeHostWindow.
|
|
887
|
+
made->window.releasedWhenClosed = NO;
|
|
888
|
+
if (isMainWindow) {
|
|
889
|
+
// The title bar talks to this window from here on; see AppKitTitleBar.h.
|
|
890
|
+
basalt::titleBar().attach(made->window);
|
|
891
|
+
}
|
|
892
|
+
[made->window center];
|
|
893
|
+
made->scaleFactor = (int)made->window.backingScaleFactor;
|
|
894
|
+
|
|
895
|
+
// Fabric emits no Create for a surface root -- the root shadow node is the
|
|
896
|
+
// base of every diff, so it has to exist before the surface starts.
|
|
897
|
+
made->root = gHost.mountingManager->createSurfaceRoot(surfaceId);
|
|
898
|
+
[made->root setRnFrameX:0 y:0 width:width height:height];
|
|
899
|
+
made->container = [[NSView alloc] initWithFrame:NSMakeRect(0, 0, width, height)];
|
|
900
|
+
made->container.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
|
|
901
|
+
[made->container addSubview:made->root];
|
|
902
|
+
made->window.contentView = made->container;
|
|
903
|
+
|
|
904
|
+
// Input, per window. Attached to this window's root, which is where its hit
|
|
905
|
+
// testing starts -- a second window with the first one's dispatcher would
|
|
906
|
+
// deliver every press to the wrong tree.
|
|
907
|
+
made->touchDispatcher =
|
|
908
|
+
std::make_unique<basalt::AppKitTouchDispatcher>(gHost.mountingManager.get(), made->root);
|
|
909
|
+
// The keyboard half: Tab reaching a <Pressable>, and Enter activating it.
|
|
910
|
+
made->focusManager =
|
|
911
|
+
std::make_unique<basalt::AppKitFocusManager>(gHost.mountingManager.get(), made->root);
|
|
912
|
+
|
|
913
|
+
gHost.windows.push_back(std::move(owned));
|
|
914
|
+
return made;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
namespace basalt {
|
|
918
|
+
|
|
919
|
+
// core/WindowHost.h, on macOS.
|
|
920
|
+
//
|
|
921
|
+
// The surface id is allocated here rather than by JavaScript: it is Fabric's
|
|
922
|
+
// number, the mounting manager keys its roots by it, and two windows racing to
|
|
923
|
+
// pick one would be two windows sharing a root. Above the ids this host
|
|
924
|
+
// reserves -- the app's surface is 1 and the error inspector's is 2.
|
|
925
|
+
facebook::react::SurfaceId openHostWindow(const NewWindowOptions &options) {
|
|
926
|
+
if (gHost.reactHost == nullptr || !gHost.surfaceStarted || options.component.empty() ||
|
|
927
|
+
gHost.windows.empty()) {
|
|
928
|
+
return 0;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
@autoreleasepool {
|
|
932
|
+
static facebook::react::SurfaceId nextSurfaceId = kLogBoxSurfaceId + 1;
|
|
933
|
+
const facebook::react::SurfaceId surfaceId = nextSurfaceId++;
|
|
934
|
+
|
|
935
|
+
const int width = (int)options.width;
|
|
936
|
+
const int height = (int)options.height;
|
|
937
|
+
NSString *title = [NSString stringWithUTF8String:options.title.c_str()];
|
|
938
|
+
// The same delegate the main window has: it answers windowDidResize: and
|
|
939
|
+
// windowDidMove:, both of which every window needs.
|
|
940
|
+
HostWindow *made = createHostWindow(surfaceId,
|
|
941
|
+
title != nil ? title : @"",
|
|
942
|
+
width,
|
|
943
|
+
height,
|
|
944
|
+
(id<NSWindowDelegate>)NSApp.delegate);
|
|
945
|
+
|
|
946
|
+
gHost.reactHost->startSurface(surfaceId,
|
|
947
|
+
options.component,
|
|
948
|
+
options.props,
|
|
949
|
+
constraintsFor(width, height),
|
|
950
|
+
layoutContextFor(made->scaleFactor));
|
|
951
|
+
[made->window makeKeyAndOrderFront:nil];
|
|
952
|
+
NSLog(@"opened window %d for module %s", (int)surfaceId, options.component.c_str());
|
|
953
|
+
return surfaceId;
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// Ends the application.
|
|
958
|
+
//
|
|
959
|
+
// `terminate:` rather than `exit()`: it runs
|
|
960
|
+
// `applicationWillTerminate:`, which is where shutdown() is, and it is what
|
|
961
|
+
// the Quit menu item would have done had the app not refused it. The refusal
|
|
962
|
+
// is the app's own flag, and an app calling this has already cleared it --
|
|
963
|
+
// see the hook in src/quitRequest.ts -- so this is not refused a second time.
|
|
964
|
+
void quitHost() {
|
|
965
|
+
[NSApp terminate:nil];
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
void closeHostWindow(facebook::react::SurfaceId surfaceId) {
|
|
969
|
+
// The main window is not closed this way: destroying the surface an app is
|
|
970
|
+
// running in is not the same thing as closing its window, and an app that
|
|
971
|
+
// means the second should say so through `close()` on the window itself.
|
|
972
|
+
if (gHost.reactHost == nullptr || surfaceId == kSurfaceId || surfaceId == kLogBoxSurfaceId) {
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
if (gHost.windowFor(surfaceId) == nullptr) {
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
// Stopping a surface unmounts its React tree, which produces one last
|
|
980
|
+
// transaction of Remove and Delete mutations. Those arrive on the main thread
|
|
981
|
+
// afterwards, and the views they name have to still be there when they do.
|
|
982
|
+
gHost.reactHost->stopSurface(surfaceId);
|
|
983
|
+
|
|
984
|
+
// So the window goes a round trip later: out to the JavaScript thread, which
|
|
985
|
+
// is where the teardown runs, and back to this one, which is where its
|
|
986
|
+
// mutations are applied. Both queues are ordered, so anything the stop
|
|
987
|
+
// produced is ahead of this.
|
|
988
|
+
gHost.reactHost->runOnRuntimeScheduler([surfaceId](facebook::jsi::Runtime &) {
|
|
989
|
+
basalt::postToUiThread([surfaceId] {
|
|
990
|
+
HostWindow *going = gHost.windowFor(surfaceId);
|
|
991
|
+
if (going == nullptr) {
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
// The dispatchers before the views they hold: both keep a borrowed root.
|
|
995
|
+
going->focusManager.reset();
|
|
996
|
+
going->touchDispatcher.reset();
|
|
997
|
+
going->window.delegate = nil;
|
|
998
|
+
[going->window close];
|
|
999
|
+
gHost.mountingManager->destroySurfaceRoot(surfaceId);
|
|
1000
|
+
|
|
1001
|
+
for (auto it = gHost.windows.begin(); it != gHost.windows.end(); ++it) {
|
|
1002
|
+
if (it->get() == going) {
|
|
1003
|
+
gHost.windows.erase(it);
|
|
1004
|
+
break;
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
NSLog(@"closed window %d", (int)surfaceId);
|
|
1008
|
+
});
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
std::vector<facebook::react::SurfaceId> hostWindows() {
|
|
1013
|
+
std::vector<facebook::react::SurfaceId> open;
|
|
1014
|
+
open.reserve(gHost.windows.size());
|
|
1015
|
+
for (const auto &candidate : gHost.windows) {
|
|
1016
|
+
open.push_back(candidate->surfaceId);
|
|
1017
|
+
}
|
|
1018
|
+
return open;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
} // namespace basalt
|
|
1022
|
+
|
|
1023
|
+
int main(int argc, const char *argv[]) {
|
|
1024
|
+
// glog, before anything logs through it.
|
|
1025
|
+
//
|
|
1026
|
+
// Without this every run opens with "WARNING: Logging before
|
|
1027
|
+
// InitGoogleLogging() is written to STDERR" -- glog saying it has not been
|
|
1028
|
+
// told where to write, in a line that reads like something went wrong on
|
|
1029
|
+
// start-up. It goes to stderr either way; this only stops it apologising for
|
|
1030
|
+
// it. React Native's C++ logs through glog throughout, so the first message
|
|
1031
|
+
// arrives before anything this project writes.
|
|
1032
|
+
FLAGS_logtostderr = true;
|
|
1033
|
+
google::InitGoogleLogging(argv[0]);
|
|
1034
|
+
|
|
1035
|
+
@autoreleasepool {
|
|
1036
|
+
gHost.bundlePath = argc > 1 ? argv[1] : "build/main.jsbundle.js";
|
|
1037
|
+
|
|
1038
|
+
// A URL the desktop launched this with, for `Linking.getInitialURL()`. Any
|
|
1039
|
+
// argument after the bundle and the module that carries a scheme: a
|
|
1040
|
+
// `.desktop` entry's `%%u`, a registered scheme, a shell association. Matched
|
|
1041
|
+
// rather than positional, because a launcher appends it and does not know
|
|
1042
|
+
// what came before.
|
|
1043
|
+
for (int i = 1; i < argc; i++) {
|
|
1044
|
+
const std::string argument(argv[i]);
|
|
1045
|
+
if (argument.find("://") != std::string::npos) {
|
|
1046
|
+
basalt::setInitialUrl(argument);
|
|
1047
|
+
break;
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
// Defaults to the raw-Fabric script, because that is what this host can
|
|
1051
|
+
// currently render: a React app needs components macOS cannot mount yet.
|
|
1052
|
+
gHost.moduleName = argc > 2 ? argv[2] : "";
|
|
1053
|
+
const char *sourcePath = getenv("BASALT_DEV_ENTRY");
|
|
1054
|
+
gHost.sourcePath = sourcePath != nullptr ? sourcePath : "index";
|
|
1055
|
+
|
|
1056
|
+
// The app's Expo config, if this is an Expo app and it has been bundled.
|
|
1057
|
+
// Before anything installs the runtime. See core/ExpoModules.h.
|
|
1058
|
+
basalt::loadExpoAppConfigBeside(gHost.bundlePath);
|
|
1059
|
+
|
|
1060
|
+
[NSApplication sharedApplication];
|
|
1061
|
+
[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
|
|
1062
|
+
installMainMenu();
|
|
1063
|
+
|
|
1064
|
+
RnAppKitHostDelegate *delegate = [[RnAppKitHostDelegate alloc] init];
|
|
1065
|
+
NSApp.delegate = delegate;
|
|
1066
|
+
|
|
1067
|
+
// Constructed here, on the main thread: AppKitMountingManager records this
|
|
1068
|
+
// thread and asserts that every mutation lands back on it.
|
|
1069
|
+
gHost.mountingManager = std::make_shared<basalt::AppKitMountingManager>();
|
|
1070
|
+
|
|
1071
|
+
// The application's own window, which is windows.front() from here on and
|
|
1072
|
+
// is what everything not yet per-window means by "the window".
|
|
1073
|
+
createHostWindow(kSurfaceId,
|
|
1074
|
+
@"basalt-core — macOS",
|
|
1075
|
+
kInitialWidth,
|
|
1076
|
+
kInitialHeight,
|
|
1077
|
+
delegate);
|
|
1078
|
+
|
|
1079
|
+
|
|
1080
|
+
gHost.runLoopObserverManager = std::make_shared<RunLoopObserverManager>();
|
|
1081
|
+
gHost.choreographer = std::make_shared<basalt::AppKitAnimationChoreographer>();
|
|
1082
|
+
|
|
1083
|
+
// Light or dark, and any change to it. Before ReactHost, so the module can
|
|
1084
|
+
// answer the first `Appearance.getColorScheme()` a bundle makes, which for an
|
|
1085
|
+
// Expo app is during its first import.
|
|
1086
|
+
basalt::startObservingColorScheme();
|
|
1087
|
+
|
|
1088
|
+
// Before ReactHost, so the beat is being induced from the first event on.
|
|
1089
|
+
gHost.runLoopObserver = basalt::installRunLoopObserver(gHost.runLoopObserverManager);
|
|
1090
|
+
|
|
1091
|
+
// Say what this host needs rather than inheriting a default that moves.
|
|
1092
|
+
// HermesInstance gives the runtime a microtask queue only when
|
|
1093
|
+
// enableBridgelessArchitecture() is true, and React's scheduler enqueues a
|
|
1094
|
+
// microtask on its first render. This host is bridgeless -- there is no
|
|
1095
|
+
// bridge here to be the alternative -- so it asserts that.
|
|
1096
|
+
facebook::react::ReactNativeFeatureFlags::override(
|
|
1097
|
+
std::make_unique<DesktopFeatureFlags>());
|
|
1098
|
+
|
|
1099
|
+
ReactInstanceConfig config;
|
|
1100
|
+
// The appId is how this host tells Metro which platform it is; see the
|
|
1101
|
+
// matching comment in gtk/main.cpp and APP_ID_PREFIX in
|
|
1102
|
+
// packages/basalt-core/metro-config.js.
|
|
1103
|
+
config.appId = "basalt-macos";
|
|
1104
|
+
config.deviceName = "macos";
|
|
1105
|
+
|
|
1106
|
+
// Dev mode changes three things at once: loadScript tries Metro before the
|
|
1107
|
+
// on-disk bundle; DevServerHelper exists, which is the only condition under
|
|
1108
|
+
// which ReactCxxTurboModuleProvider serves the DevSettings module a __DEV__
|
|
1109
|
+
// bundle requires; and ReactHost opens a packager connection whose reload
|
|
1110
|
+
// message reloads the instance.
|
|
1111
|
+
// What this app calls itself. Nothing on macOS reads it yet -- the bundle's
|
|
1112
|
+
// Info.plist is what the system goes by there, and cli/packageApp.js writes
|
|
1113
|
+
// that -- but reading it here keeps `basalt::appIdentity()` answering on all
|
|
1114
|
+
// three hosts rather than on one. See core/AppIdentity.h.
|
|
1115
|
+
basalt::appIdentity(gHost.bundlePath);
|
|
1116
|
+
|
|
1117
|
+
config.enableDevMode = getenv("BASALT_DEV") != nullptr;
|
|
1118
|
+
gHost.devMode = config.enableDevMode;
|
|
1119
|
+
config.enableInspector = config.enableDevMode;
|
|
1120
|
+
if (const char *devHost = getenv("BASALT_DEV_HOST")) {
|
|
1121
|
+
config.devServerHost = devHost;
|
|
1122
|
+
}
|
|
1123
|
+
if (const char *devPort = getenv("BASALT_DEV_PORT")) {
|
|
1124
|
+
config.devServerPort = (uint32_t)strtoul(devPort, nullptr, 10);
|
|
1125
|
+
}
|
|
1126
|
+
// So the http client can tell the bundle request apart from an app's own
|
|
1127
|
+
// fetch, and refuse to feed a Metro error page to the JS engine. See
|
|
1128
|
+
// core/DevBundle.h.
|
|
1129
|
+
if (config.enableDevMode) {
|
|
1130
|
+
basalt::setDevServerOrigin(config.devServerHost, config.devServerPort);
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
try {
|
|
1134
|
+
gHost.reactHost = std::make_unique<ReactHost>(
|
|
1135
|
+
config,
|
|
1136
|
+
gHost.mountingManager,
|
|
1137
|
+
gHost.runLoopObserverManager,
|
|
1138
|
+
makeContextContainer(),
|
|
1139
|
+
facebook::react::getDefaultOnJsErrorFunc(),
|
|
1140
|
+
logToConsole,
|
|
1141
|
+
nullptr,
|
|
1142
|
+
makeTurboModuleProviders(basalt::scriptURLFor(
|
|
1143
|
+
gHost.bundlePath,
|
|
1144
|
+
config.enableDevMode,
|
|
1145
|
+
config.devServerHost,
|
|
1146
|
+
config.devServerPort,
|
|
1147
|
+
gHost.sourcePath.empty() ? "index" : gHost.sourcePath,
|
|
1148
|
+
"macos"),
|
|
1149
|
+
config.enableDevMode,
|
|
1150
|
+
gHost.mountingManager->imageLoader()),
|
|
1151
|
+
// React Native's error inspector. ReactCxxPlatform implements the
|
|
1152
|
+
// LogBox TurboModule itself and only provides it when a host hands
|
|
1153
|
+
// over one of these; passing null, as this did, left
|
|
1154
|
+
// NativeLogBox.show() a call into nothing. See core/LogBoxSurface.h.
|
|
1155
|
+
std::make_shared<basalt::LogBoxSurfaceDelegate>(
|
|
1156
|
+
[](const std::string &appKey) { showLogBoxSurface(appKey); },
|
|
1157
|
+
[]() { hideLogBoxSurface(); }),
|
|
1158
|
+
// `useNativeDriver: true`. ReactCommon has a C++ implementation of
|
|
1159
|
+
// the whole animated graph, and ReactCxxPlatform provides the module
|
|
1160
|
+
// for it -- but only when a host asks, by handing over a provider.
|
|
1161
|
+
// Without it every native-driven Animated call throws "Native
|
|
1162
|
+
// animated module is not available", which is also what kept
|
|
1163
|
+
// LogBox's own spinner from rendering.
|
|
1164
|
+
std::make_shared<facebook::react::NativeAnimatedNodesManagerProvider>(),
|
|
1165
|
+
installBindings,
|
|
1166
|
+
gHost.choreographer);
|
|
1167
|
+
} catch (const std::exception &error) {
|
|
1168
|
+
NSLog(@"could not construct ReactHost: %s", error.what());
|
|
1169
|
+
return 1;
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
// How anything in the core reaches the scheduler, which only the host has.
|
|
1173
|
+
// Reanimated is the one caller; see core/UIManagerAccess.h.
|
|
1174
|
+
basalt::setEventListenerInstaller(
|
|
1175
|
+
[](std::shared_ptr<const facebook::react::EventListener> listener) {
|
|
1176
|
+
if (gHost.reactHost == nullptr) {
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1179
|
+
gHost.reactHost->runOnScheduler([listener = std::move(listener)](
|
|
1180
|
+
facebook::react::Scheduler &scheduler) {
|
|
1181
|
+
scheduler.addEventListener(listener);
|
|
1182
|
+
});
|
|
1183
|
+
});
|
|
1184
|
+
|
|
1185
|
+
// And how anything in the core reaches the JavaScript thread. An Expo
|
|
1186
|
+
// module is given no call invoker, so this is the only way one can emit an
|
|
1187
|
+
// event or settle a promise later; see core/JsRuntimeAccess.h.
|
|
1188
|
+
basalt::setRuntimeRunner(
|
|
1189
|
+
[](std::function<void(facebook::jsi::Runtime &)> work) {
|
|
1190
|
+
if (gHost.reactHost == nullptr) {
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
1193
|
+
gHost.reactHost->runOnRuntimeScheduler(std::move(work));
|
|
1194
|
+
});
|
|
1195
|
+
|
|
1196
|
+
// `loadScript` falls back to the on-disk bundle whenever the Metro fetch
|
|
1197
|
+
// fails, which is right when nothing is listening and wrong when Metro
|
|
1198
|
+
// answered with an error: running the last bundle that built, while the
|
|
1199
|
+
// developer looks at source that is not what is executing, hides the very
|
|
1200
|
+
// thing they need to see. So the fallback is allowed only for the first
|
|
1201
|
+
// case, and the second stops here with Metro's own message.
|
|
1202
|
+
const bool loaded = gHost.reactHost->loadScript(gHost.bundlePath, gHost.sourcePath);
|
|
1203
|
+
if (const auto devError = basalt::devBundleError()) {
|
|
1204
|
+
NSLog(@"Metro could not build the bundle (HTTP %ld):\n%s",
|
|
1205
|
+
devError->status,
|
|
1206
|
+
devError->message.c_str());
|
|
1207
|
+
// Through shutdown() rather than straight out: the fallback bundle is
|
|
1208
|
+
// already running on the JS thread, and dropping the host without
|
|
1209
|
+
// joining it aborts in a destructor instead of exiting.
|
|
1210
|
+
shutdown();
|
|
1211
|
+
return 1;
|
|
1212
|
+
}
|
|
1213
|
+
if (!loaded) {
|
|
1214
|
+
NSLog(@"could not load script: %s", gHost.bundlePath.c_str());
|
|
1215
|
+
shutdown();
|
|
1216
|
+
return 1;
|
|
1217
|
+
}
|
|
1218
|
+
// Where it came from, not where it might have. See core/DevBundle.h.
|
|
1219
|
+
if (const auto from = basalt::devBundleFetched()) {
|
|
1220
|
+
NSLog(@"loaded script from the packager: %s", from->c_str());
|
|
1221
|
+
} else {
|
|
1222
|
+
NSLog(@"loaded script: %s", gHost.bundlePath.c_str());
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
// The module name decides who drives the surface. Non-empty:
|
|
1226
|
+
// SurfaceHandler::start calls AppRegistry.runApplication. Empty: the
|
|
1227
|
+
// surface is registered without JS being called at all, leaving it for a
|
|
1228
|
+
// script to commit into through nativeFabricUIManager by hand.
|
|
1229
|
+
gHost.reactHost->startSurface(kSurfaceId,
|
|
1230
|
+
gHost.moduleName,
|
|
1231
|
+
folly::dynamic::object(),
|
|
1232
|
+
constraintsFor(kInitialWidth, kInitialHeight),
|
|
1233
|
+
layoutContextFor(gHost.main().scaleFactor));
|
|
1234
|
+
gHost.mountingManager->setSurfaceSize((float)kInitialWidth, (float)kInitialHeight);
|
|
1235
|
+
gHost.surfaceStarted = true;
|
|
1236
|
+
NSLog(@"started surface %d%s%s",
|
|
1237
|
+
(int)kSurfaceId,
|
|
1238
|
+
gHost.moduleName.empty() ? " (no module; raw Fabric script)" : " for module ",
|
|
1239
|
+
gHost.moduleName.c_str());
|
|
1240
|
+
|
|
1241
|
+
[gHost.main().window makeKeyAndOrderFront:nil];
|
|
1242
|
+
[NSApp activateIgnoringOtherApps:YES];
|
|
1243
|
+
// Prime the bounds cache, which `getBounds()` answers from: without this an
|
|
1244
|
+
// app's first render sees a window of no size, and only a later resize
|
|
1245
|
+
// corrects it. See core/WindowBoundsCache.cpp.
|
|
1246
|
+
basalt::notifyWindowBoundsChanged();
|
|
1247
|
+
|
|
1248
|
+
// Accepting what the desktop drags onto this window. On the surface root
|
|
1249
|
+
// rather than per view: AppKit asks the destination under the pointer,
|
|
1250
|
+
// and which *app* view that is is core/DragAndDrop.h's question.
|
|
1251
|
+
basalt::attachDropTarget(gHost.main().root);
|
|
1252
|
+
|
|
1253
|
+
// And the display list, which `getDisplays()` answers from. Same reason,
|
|
1254
|
+
// and the same trap: the first version of this primed from
|
|
1255
|
+
// windowDidResize, which is not startup -- an app that never resized its
|
|
1256
|
+
// window was told the desktop had no screens.
|
|
1257
|
+
basalt::notifyDisplaysChanged();
|
|
1258
|
+
|
|
1259
|
+
// The two keys this host answers before anything else sees them.
|
|
1260
|
+
//
|
|
1261
|
+
// Escape closes the topmost <Modal> -- or rather, asks the app to. React
|
|
1262
|
+
// Native's `onRequestClose` is the hardware back button on Android and the
|
|
1263
|
+
// swipe-down on iOS; on a desktop it is Escape.
|
|
1264
|
+
//
|
|
1265
|
+
// Cmd+D opens React Native's developer menu, which is the shortcut it uses
|
|
1266
|
+
// on a simulator and the closest thing a desktop has to a phone's shake.
|
|
1267
|
+
//
|
|
1268
|
+
// A local event monitor rather than `keyDown:` on a view, because neither
|
|
1269
|
+
// is about the focused view and nothing in the window need have focus. The
|
|
1270
|
+
// monitor swallows a key only when it actually did something, so both
|
|
1271
|
+
// still do whatever they did before everywhere else.
|
|
1272
|
+
[NSEvent addLocalMonitorForEventsMatchingMask:NSEventMaskKeyDown
|
|
1273
|
+
handler:^NSEvent *(NSEvent *event) {
|
|
1274
|
+
const unichar first =
|
|
1275
|
+
event.charactersIgnoringModifiers.length > 0
|
|
1276
|
+
? [event.charactersIgnoringModifiers
|
|
1277
|
+
characterAtIndex:0]
|
|
1278
|
+
: 0;
|
|
1279
|
+
const BOOL command =
|
|
1280
|
+
(event.modifierFlags &
|
|
1281
|
+
NSEventModifierFlagCommand) != 0;
|
|
1282
|
+
if (command && (first == 'd' || first == 'D') &&
|
|
1283
|
+
gHost.devMode && gHost.reactHost != nullptr) {
|
|
1284
|
+
basalt::showDevMenu(gHost.reactHost.get());
|
|
1285
|
+
return nil;
|
|
1286
|
+
}
|
|
1287
|
+
if (first == 0x1B &&
|
|
1288
|
+
gHost.mountingManager != nullptr &&
|
|
1289
|
+
gHost.mountingManager->requestCloseTopModal()) {
|
|
1290
|
+
return nil;
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
// An app's declared shortcut, when
|
|
1294
|
+
// the responder chain will not
|
|
1295
|
+
// deliver it.
|
|
1296
|
+
//
|
|
1297
|
+
// `RnAppKitView keyDown:` is only
|
|
1298
|
+
// called on the first responder, so
|
|
1299
|
+
// an app whose shortcut holder is
|
|
1300
|
+
// the root -- nothing focuses it,
|
|
1301
|
+
// `focusable` only makes it
|
|
1302
|
+
// eligible -- never sees a press at
|
|
1303
|
+
// all. Only when nothing of ours is
|
|
1304
|
+
// in the chain: with a view focused
|
|
1305
|
+
// the ordinary path runs and this
|
|
1306
|
+
// must not run again, or a text
|
|
1307
|
+
// field would lose keys to a
|
|
1308
|
+
// window-level claim.
|
|
1309
|
+
if (basaltViewInResponderChain(event.window)) {
|
|
1310
|
+
return event;
|
|
1311
|
+
}
|
|
1312
|
+
if (gHost.main().focusManager != nullptr &&
|
|
1313
|
+
gHost.main().focusManager->deliverWindowKey(
|
|
1314
|
+
event)) {
|
|
1315
|
+
return nil;
|
|
1316
|
+
}
|
|
1317
|
+
return event;
|
|
1318
|
+
}];
|
|
1319
|
+
|
|
1320
|
+
// The display link needs a window, which the root now has.
|
|
1321
|
+
gHost.choreographer->attachToView(gHost.main().root);
|
|
1322
|
+
|
|
1323
|
+
if (gHost.moduleName.empty()) {
|
|
1324
|
+
NSLog(@"--- committing tree 1 from JS ---");
|
|
1325
|
+
callRenderFunction(1);
|
|
1326
|
+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)),
|
|
1327
|
+
dispatch_get_main_queue(),
|
|
1328
|
+
^{
|
|
1329
|
+
NSLog(@"--- committing tree 2 from JS ---");
|
|
1330
|
+
callRenderFunction(2);
|
|
1331
|
+
});
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
// One clock for every instrument below, rather than one each.
|
|
1335
|
+
//
|
|
1336
|
+
// They used to start at 1500ms independently, which was invisible for as
|
|
1337
|
+
// long as no scenario used two at once -- and wrong the moment one did: a
|
|
1338
|
+
// run with both BASALT_TEST_TAP and BASALT_TEST_FOCUS interleaved them
|
|
1339
|
+
// instead of running the taps and then the keys. The GTK and Win32 hosts
|
|
1340
|
+
// have always threaded a single delay through; this is the third.
|
|
1341
|
+
__block int64_t scriptedDelayMs = 1500;
|
|
1342
|
+
|
|
1343
|
+
// BASALT_TEST_TAP: "x,y;x,y" -- synthesise taps a second apart, in
|
|
1344
|
+
// surface-root coordinates. Enters where AppKit's mouse handler would, so
|
|
1345
|
+
// it exercises hit testing and event delivery but not AppKit itself.
|
|
1346
|
+
//
|
|
1347
|
+
// It exists because the alternative is CGEvent, which needs accessibility
|
|
1348
|
+
// permission an automated run does not have. The GTK host has the same
|
|
1349
|
+
// escape hatch for the same reason, and the end-to-end suite on Linux uses
|
|
1350
|
+
// xdotool instead where it can.
|
|
1351
|
+
//
|
|
1352
|
+
// A point may name a window: "x,y@3" taps in the window whose surface is 3
|
|
1353
|
+
// rather than in the app's own. Without it there would be no way to reach a
|
|
1354
|
+
// second window at all -- each has its own touch dispatcher, which is the
|
|
1355
|
+
// whole point of them, and the main window's would happily hit-test a tree
|
|
1356
|
+
// that is not on screen.
|
|
1357
|
+
//
|
|
1358
|
+
// BASALT_TEST_SECONDARY_TAP takes the same spec and clicks the other
|
|
1359
|
+
// button. Its own variable rather than a suffix on a point, because the two
|
|
1360
|
+
// assert opposite things -- one presses what it lands on and the other must
|
|
1361
|
+
// not -- and a scenario that mixed them in one string would be harder to
|
|
1362
|
+
// read than to write.
|
|
1363
|
+
void (^scheduleTaps)(const char *, basalt::PointerButton) =
|
|
1364
|
+
^(const char *taps, basalt::PointerButton button) {
|
|
1365
|
+
NSString *spec = [NSString stringWithUTF8String:taps];
|
|
1366
|
+
const BOOL secondary = button != basalt::PointerButton::Primary;
|
|
1367
|
+
NSString *name = secondary ? @"BASALT_TEST_SECONDARY_TAP" : @"BASALT_TEST_TAP";
|
|
1368
|
+
int64_t delayMs = scriptedDelayMs;
|
|
1369
|
+
for (NSString *entry in [spec componentsSeparatedByString:@";"]) {
|
|
1370
|
+
NSArray<NSString *> *halves = [entry componentsSeparatedByString:@"@"];
|
|
1371
|
+
// Defaults to the app's own window, so every spec written before
|
|
1372
|
+
// windows existed still means what it did.
|
|
1373
|
+
const facebook::react::SurfaceId surfaceId =
|
|
1374
|
+
halves.count > 1 ? (facebook::react::SurfaceId)halves[1].intValue : kSurfaceId;
|
|
1375
|
+
NSArray<NSString *> *parts = [halves[0] componentsSeparatedByString:@","];
|
|
1376
|
+
if (parts.count != 2) {
|
|
1377
|
+
continue;
|
|
1378
|
+
}
|
|
1379
|
+
const double x = parts[0].doubleValue;
|
|
1380
|
+
const double y = parts[1].doubleValue;
|
|
1381
|
+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, delayMs * NSEC_PER_MSEC),
|
|
1382
|
+
dispatch_get_main_queue(),
|
|
1383
|
+
^{
|
|
1384
|
+
NSLog(@"%@: tapping (%.0f, %.0f) in window %d",
|
|
1385
|
+
name, x, y, (int)surfaceId);
|
|
1386
|
+
HostWindow *target = gHost.windowFor(surfaceId);
|
|
1387
|
+
if (target == nullptr) {
|
|
1388
|
+
NSLog(@"%@: no window %d", name, (int)surfaceId);
|
|
1389
|
+
return;
|
|
1390
|
+
}
|
|
1391
|
+
if (target->touchDispatcher != nullptr) {
|
|
1392
|
+
target->touchDispatcher->synthesiseTap(x, y, button);
|
|
1393
|
+
}
|
|
1394
|
+
});
|
|
1395
|
+
delayMs += 1000;
|
|
1396
|
+
}
|
|
1397
|
+
scriptedDelayMs = delayMs;
|
|
1398
|
+
};
|
|
1399
|
+
|
|
1400
|
+
if (const char *taps = getenv("BASALT_TEST_TAP")) {
|
|
1401
|
+
scheduleTaps(taps, basalt::PointerButton::Primary);
|
|
1402
|
+
}
|
|
1403
|
+
// The other button, which presses nothing and arrives as a pointer event.
|
|
1404
|
+
// See core/PointerButtons.h.
|
|
1405
|
+
if (const char *taps = getenv("BASALT_TEST_SECONDARY_TAP")) {
|
|
1406
|
+
scheduleTaps(taps, basalt::PointerButton::Secondary);
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
// BASALT_TEST_HOVER: "x,y;x,y" -- the pointer moving with no button down,
|
|
1410
|
+
// a second apart, in surface-root coordinates. The only way to reach the
|
|
1411
|
+
// hover path from automation: a real cursor move is CGEvent again, and a
|
|
1412
|
+
// posted NSEventTypeMouseMoved does not drive a tracking area. A negative
|
|
1413
|
+
// point means the pointer left the surface.
|
|
1414
|
+
if (const char *hovers = getenv("BASALT_TEST_HOVER")) {
|
|
1415
|
+
NSString *spec = [NSString stringWithUTF8String:hovers];
|
|
1416
|
+
int64_t delayMs = scriptedDelayMs;
|
|
1417
|
+
for (NSString *point in [spec componentsSeparatedByString:@";"]) {
|
|
1418
|
+
NSArray<NSString *> *parts = [point componentsSeparatedByString:@","];
|
|
1419
|
+
if (parts.count != 2) {
|
|
1420
|
+
continue;
|
|
1421
|
+
}
|
|
1422
|
+
const double x = parts[0].doubleValue;
|
|
1423
|
+
const double y = parts[1].doubleValue;
|
|
1424
|
+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, delayMs * NSEC_PER_MSEC),
|
|
1425
|
+
dispatch_get_main_queue(),
|
|
1426
|
+
^{
|
|
1427
|
+
NSLog(@"BASALT_TEST_HOVER: hovering (%.0f, %.0f)", x, y);
|
|
1428
|
+
if (gHost.main().touchDispatcher != nullptr) {
|
|
1429
|
+
gHost.main().touchDispatcher->synthesiseHover(x, y);
|
|
1430
|
+
}
|
|
1431
|
+
});
|
|
1432
|
+
delayMs += 1000;
|
|
1433
|
+
}
|
|
1434
|
+
scriptedDelayMs = delayMs;
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
// BASALT_TEST_CLOSE_WINDOW: the surface id of a window to close the way a
|
|
1438
|
+
// person would -- its own close button, not the app asking. The two take
|
|
1439
|
+
// different paths through the host and only one of them can leave a record
|
|
1440
|
+
// whose window is gone, which is why it is worth being able to drive.
|
|
1441
|
+
if (const char *closing = getenv("BASALT_TEST_CLOSE_WINDOW")) {
|
|
1442
|
+
const auto surfaceId = (facebook::react::SurfaceId)atoi(closing);
|
|
1443
|
+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, scriptedDelayMs * NSEC_PER_MSEC),
|
|
1444
|
+
dispatch_get_main_queue(),
|
|
1445
|
+
^{
|
|
1446
|
+
NSLog(@"BASALT_TEST_CLOSE_WINDOW: closing window %d", (int)surfaceId);
|
|
1447
|
+
HostWindow *target = gHost.windowFor(surfaceId);
|
|
1448
|
+
if (target != nullptr) {
|
|
1449
|
+
// `performClose:`, not `close`: this is what a close
|
|
1450
|
+
// button sends, so windowWillClose: runs exactly as it
|
|
1451
|
+
// would for a person.
|
|
1452
|
+
[target->window performClose:nil];
|
|
1453
|
+
}
|
|
1454
|
+
});
|
|
1455
|
+
scriptedDelayMs += 1000;
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
// BASALT_TEST_DROP: "x,y:path" -- a file dropped at that point, reported
|
|
1459
|
+
// the way the toolkit would report it. Entered below AppKit for the reason
|
|
1460
|
+
// the GTK host gives: a real drag needs a source outside this process.
|
|
1461
|
+
if (const char *drop = getenv("BASALT_TEST_DROP")) {
|
|
1462
|
+
const std::string spec(drop);
|
|
1463
|
+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, scriptedDelayMs * NSEC_PER_MSEC),
|
|
1464
|
+
dispatch_get_main_queue(),
|
|
1465
|
+
^{
|
|
1466
|
+
const size_t comma = spec.find(',');
|
|
1467
|
+
const size_t colon = spec.find(':', comma == std::string::npos ? 0 : comma);
|
|
1468
|
+
if (comma == std::string::npos || colon == std::string::npos) {
|
|
1469
|
+
NSLog(@"BASALT_TEST_DROP: expected x,y:path");
|
|
1470
|
+
return;
|
|
1471
|
+
}
|
|
1472
|
+
const double x = std::stod(spec.substr(0, comma));
|
|
1473
|
+
const double y = std::stod(spec.substr(comma + 1, colon - comma - 1));
|
|
1474
|
+
const std::string path = spec.substr(colon + 1);
|
|
1475
|
+
|
|
1476
|
+
const facebook::react::Tag found = basalt::dropTargetAt(
|
|
1477
|
+
gHost.main().root, x, y, basalt::DropAcceptsFiles);
|
|
1478
|
+
NSLog(@"BASALT_TEST_DROP: %s at %g,%g onto tag %d",
|
|
1479
|
+
path.c_str(), x, y, (int)found);
|
|
1480
|
+
if (found == 0) {
|
|
1481
|
+
return;
|
|
1482
|
+
}
|
|
1483
|
+
basalt::DropEvent event;
|
|
1484
|
+
event.tag = found;
|
|
1485
|
+
event.phase = basalt::DropPhase::Drop;
|
|
1486
|
+
event.x = x;
|
|
1487
|
+
event.y = y;
|
|
1488
|
+
event.payload.files.push_back(path);
|
|
1489
|
+
basalt::reportDrop(event);
|
|
1490
|
+
});
|
|
1491
|
+
scriptedDelayMs += 1000;
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
// BASALT_TEST_QUIT: ask the application to quit the way Cmd-Q does, so a
|
|
1495
|
+
// quit an app refuses can be driven from a script. Not the same thing as
|
|
1496
|
+
// BASALT_QUIT_AFTER_MS, which is the harness ending the process and is
|
|
1497
|
+
// deliberately not refusable -- this one goes through
|
|
1498
|
+
// applicationShouldTerminate: and is.
|
|
1499
|
+
//
|
|
1500
|
+
// A count rather than a flag, because one ask cannot show the interesting
|
|
1501
|
+
// half: an app that refuses is proved by the process still being here,
|
|
1502
|
+
// and an app that then agrees is proved by it going. Asks are a second
|
|
1503
|
+
// apart, like every other scripted instrument.
|
|
1504
|
+
if (const char *asks = getenv("BASALT_TEST_QUIT")) {
|
|
1505
|
+
const long times = std::max(1L, strtol(asks, nullptr, 10));
|
|
1506
|
+
for (long ask = 0; ask < times; ask++) {
|
|
1507
|
+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, scriptedDelayMs * NSEC_PER_MSEC),
|
|
1508
|
+
dispatch_get_main_queue(),
|
|
1509
|
+
^{
|
|
1510
|
+
NSLog(@"BASALT_TEST_QUIT: asking the application to quit");
|
|
1511
|
+
[NSApp terminate:nil];
|
|
1512
|
+
});
|
|
1513
|
+
scriptedDelayMs += 1000;
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
// BASALT_TEST_FOCUS: keyboard actions separated by ';' -- `tab`,
|
|
1518
|
+
// `shift-tab`, `activate`, `escape` and `devmenu`, each fired a second
|
|
1519
|
+
// apart.
|
|
1520
|
+
//
|
|
1521
|
+
// The same reason the other instruments exist, one step further out. A real
|
|
1522
|
+
// Tab needs a window the window server considers key, which an automated
|
|
1523
|
+
// run does not reliably have. This enters at AppKitFocusManager, so it
|
|
1524
|
+
// exercises AppKit's own key-view loop, the focus and blur events and the
|
|
1525
|
+
// click dispatch, and skips only the delivery of the keystroke itself.
|
|
1526
|
+
// BASALT_TEST_KEY: a declared shortcut, pressed. Enters after the NSEvent
|
|
1527
|
+
// translation rather than before it -- see core/TestKeys.h for what that
|
|
1528
|
+
// does and does not prove.
|
|
1529
|
+
for (const basalt::KeyCombination &pressed : basalt::scriptedKeyPresses()) {
|
|
1530
|
+
const basalt::KeyCombination combination = pressed;
|
|
1531
|
+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, scriptedDelayMs * NSEC_PER_MSEC),
|
|
1532
|
+
dispatch_get_main_queue(),
|
|
1533
|
+
^{
|
|
1534
|
+
NSLog(@"BASALT_TEST_KEY: %s", combination.key.c_str());
|
|
1535
|
+
if (gHost.main().focusManager != nullptr) {
|
|
1536
|
+
gHost.main().focusManager->deliverTestKey(combination);
|
|
1537
|
+
}
|
|
1538
|
+
});
|
|
1539
|
+
scriptedDelayMs += 400;
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
if (const char *focus = getenv("BASALT_TEST_FOCUS")) {
|
|
1543
|
+
NSString *spec = [NSString stringWithUTF8String:focus];
|
|
1544
|
+
int64_t delayMs = scriptedDelayMs;
|
|
1545
|
+
for (NSString *raw in [spec componentsSeparatedByString:@";"]) {
|
|
1546
|
+
NSString *action = [raw stringByTrimmingCharactersInSet:
|
|
1547
|
+
[NSCharacterSet whitespaceCharacterSet]];
|
|
1548
|
+
if (action.length == 0) {
|
|
1549
|
+
continue;
|
|
1550
|
+
}
|
|
1551
|
+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, delayMs * NSEC_PER_MSEC),
|
|
1552
|
+
dispatch_get_main_queue(),
|
|
1553
|
+
^{
|
|
1554
|
+
NSLog(@"BASALT_TEST_FOCUS: %@", action);
|
|
1555
|
+
if (gHost.main().focusManager == nullptr &&
|
|
1556
|
+
![action isEqualToString:@"escape"] &&
|
|
1557
|
+
![action isEqualToString:@"devmenu"]) {
|
|
1558
|
+
return;
|
|
1559
|
+
}
|
|
1560
|
+
if ([action isEqualToString:@"tab"]) {
|
|
1561
|
+
gHost.main().focusManager->moveFocus(true);
|
|
1562
|
+
} else if ([action isEqualToString:@"shift-tab"]) {
|
|
1563
|
+
gHost.main().focusManager->moveFocus(false);
|
|
1564
|
+
} else if ([action isEqualToString:@"activate"]) {
|
|
1565
|
+
gHost.main().focusManager->activateFocused();
|
|
1566
|
+
} else if ([action isEqualToString:@"devmenu"]) {
|
|
1567
|
+
// Also not a focus action. Same instrument for the
|
|
1568
|
+
// same reason: a key that needs nothing focused to
|
|
1569
|
+
// arrive. What it opens is answered by
|
|
1570
|
+
// BASALT_TEST_MENU; see core/TestDialog.h.
|
|
1571
|
+
basalt::showDevMenu(gHost.reactHost.get());
|
|
1572
|
+
} else if ([action isEqualToString:@"escape"]) {
|
|
1573
|
+
// Not a focus action, and here anyway: this is the
|
|
1574
|
+
// instrument for "a key was pressed and nothing on
|
|
1575
|
+
// the window has to be focused for it to arrive",
|
|
1576
|
+
// which is exactly what Escape closing a <Modal> is.
|
|
1577
|
+
gHost.mountingManager->requestCloseTopModal();
|
|
1578
|
+
} else {
|
|
1579
|
+
NSLog(@"BASALT_TEST_FOCUS: unknown action \"%@\"", action);
|
|
1580
|
+
}
|
|
1581
|
+
});
|
|
1582
|
+
delayMs += 1000;
|
|
1583
|
+
}
|
|
1584
|
+
scriptedDelayMs = delayMs;
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
// BASALT_TEST_DRAG: "x1,y1,x2,y2" -- one press, twenty moves and a release,
|
|
1588
|
+
// for the gestures a tap cannot reach. See synthesiseDrag.
|
|
1589
|
+
if (const char *drag = getenv("BASALT_TEST_DRAG")) {
|
|
1590
|
+
NSArray<NSString *> *parts =
|
|
1591
|
+
[[NSString stringWithUTF8String:drag] componentsSeparatedByString:@","];
|
|
1592
|
+
if (parts.count == 4) {
|
|
1593
|
+
const double fromX = parts[0].doubleValue;
|
|
1594
|
+
const double fromY = parts[1].doubleValue;
|
|
1595
|
+
const double toX = parts[2].doubleValue;
|
|
1596
|
+
const double toY = parts[3].doubleValue;
|
|
1597
|
+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)1500 * NSEC_PER_MSEC),
|
|
1598
|
+
dispatch_get_main_queue(),
|
|
1599
|
+
^{
|
|
1600
|
+
NSLog(@"BASALT_TEST_DRAG: (%.0f, %.0f) -> (%.0f, %.0f)",
|
|
1601
|
+
fromX,
|
|
1602
|
+
fromY,
|
|
1603
|
+
toX,
|
|
1604
|
+
toY);
|
|
1605
|
+
if (gHost.main().touchDispatcher != nullptr) {
|
|
1606
|
+
gHost.main().touchDispatcher->synthesiseDrag(fromX, fromY, toX, toY, 20);
|
|
1607
|
+
}
|
|
1608
|
+
});
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
// BASALT_TEST_CLICK: the same "x,y;x,y", but as real NSEvents posted to the
|
|
1613
|
+
// window rather than as calls into the dispatcher.
|
|
1614
|
+
//
|
|
1615
|
+
// The difference from BASALT_TEST_TAP is the whole point: a synthesised tap
|
|
1616
|
+
// proves hit testing and delivery to JavaScript, and proves nothing about
|
|
1617
|
+
// whether AppKit routes a click to these views at all. This posts events
|
|
1618
|
+
// through the application's own queue, which exercises NSView's hit
|
|
1619
|
+
// testing, the responder chain and mouseDown:/mouseUp:.
|
|
1620
|
+
//
|
|
1621
|
+
// In-process, so it needs none of the accessibility permission CGEvent
|
|
1622
|
+
// would -- which is what makes it usable in automation where a real click
|
|
1623
|
+
// is not.
|
|
1624
|
+
if (const char *clicks = getenv("BASALT_TEST_CLICK")) {
|
|
1625
|
+
NSString *spec = [NSString stringWithUTF8String:clicks];
|
|
1626
|
+
int64_t delayMs = scriptedDelayMs;
|
|
1627
|
+
for (NSString *point in [spec componentsSeparatedByString:@";"]) {
|
|
1628
|
+
NSArray<NSString *> *parts = [point componentsSeparatedByString:@","];
|
|
1629
|
+
if (parts.count != 2) {
|
|
1630
|
+
continue;
|
|
1631
|
+
}
|
|
1632
|
+
const CGFloat x = parts[0].doubleValue;
|
|
1633
|
+
const CGFloat y = parts[1].doubleValue;
|
|
1634
|
+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, delayMs * NSEC_PER_MSEC),
|
|
1635
|
+
dispatch_get_main_queue(),
|
|
1636
|
+
^{
|
|
1637
|
+
NSLog(@"BASALT_TEST_CLICK: clicking (%.0f, %.0f)", x, y);
|
|
1638
|
+
// Through the root, which converts out of the flipped
|
|
1639
|
+
// top-left space every coordinate here is in and into
|
|
1640
|
+
// the window's bottom-left one.
|
|
1641
|
+
const NSPoint inWindow = [gHost.main().root convertPoint:NSMakePoint(x, y)
|
|
1642
|
+
toView:nil];
|
|
1643
|
+
for (NSEventType type :
|
|
1644
|
+
{NSEventTypeLeftMouseDown, NSEventTypeLeftMouseUp}) {
|
|
1645
|
+
NSEvent *event =
|
|
1646
|
+
[NSEvent mouseEventWithType:type
|
|
1647
|
+
location:inWindow
|
|
1648
|
+
modifierFlags:0
|
|
1649
|
+
timestamp:NSProcessInfo.processInfo.systemUptime
|
|
1650
|
+
windowNumber:gHost.main().window.windowNumber
|
|
1651
|
+
context:nil
|
|
1652
|
+
eventNumber:0
|
|
1653
|
+
clickCount:1
|
|
1654
|
+
pressure:type == NSEventTypeLeftMouseDown ? 1 : 0];
|
|
1655
|
+
[NSApp postEvent:event atStart:NO];
|
|
1656
|
+
}
|
|
1657
|
+
});
|
|
1658
|
+
delayMs += 1000;
|
|
1659
|
+
}
|
|
1660
|
+
scriptedDelayMs = delayMs;
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
// BASALT_TEST_SCROLL: "x,y,lines;x,y,lines" -- scroll wheel notches over a
|
|
1664
|
+
// point, a second apart, in surface-root coordinates. Positive scrolls
|
|
1665
|
+
// down, as a contentOffset does.
|
|
1666
|
+
//
|
|
1667
|
+
// The event is a real NSEvent, built the way a wheel builds one, and it is
|
|
1668
|
+
// handed to the view this project's own hit test finds. So it proves the
|
|
1669
|
+
// whole path below AppKit -- scrollWheel:, the handler lookup, the offset,
|
|
1670
|
+
// the clamp, the state write and onScroll -- and does not prove that AppKit
|
|
1671
|
+
// routes a wheel to that view, which is ordinary responder-chain behaviour
|
|
1672
|
+
// that `[super scrollWheel:]` takes part in.
|
|
1673
|
+
if (const char *scrolls = getenv("BASALT_TEST_SCROLL")) {
|
|
1674
|
+
NSString *spec = [NSString stringWithUTF8String:scrolls];
|
|
1675
|
+
int64_t delayMs = scriptedDelayMs;
|
|
1676
|
+
for (NSString *step in [spec componentsSeparatedByString:@";"]) {
|
|
1677
|
+
NSArray<NSString *> *parts = [step componentsSeparatedByString:@","];
|
|
1678
|
+
if (parts.count != 3) {
|
|
1679
|
+
continue;
|
|
1680
|
+
}
|
|
1681
|
+
const CGFloat x = parts[0].doubleValue;
|
|
1682
|
+
const CGFloat y = parts[1].doubleValue;
|
|
1683
|
+
const int32_t lines = (int32_t)parts[2].intValue;
|
|
1684
|
+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, delayMs * NSEC_PER_MSEC),
|
|
1685
|
+
dispatch_get_main_queue(),
|
|
1686
|
+
^{
|
|
1687
|
+
RnAppKitView *target = RnAppKitHitTest(gHost.main().root, x, y);
|
|
1688
|
+
if (target == nil) {
|
|
1689
|
+
NSLog(@"BASALT_TEST_SCROLL: nothing at (%.0f, %.0f)", x, y);
|
|
1690
|
+
return;
|
|
1691
|
+
}
|
|
1692
|
+
NSLog(@"BASALT_TEST_SCROLL: %d lines over tag %ld",
|
|
1693
|
+
lines, (long)target.rnTag);
|
|
1694
|
+
// Negative because AppKit's positive Y is a scroll up,
|
|
1695
|
+
// and this is spelled the way contentOffset reads.
|
|
1696
|
+
CGEventRef wheel = CGEventCreateScrollWheelEvent(
|
|
1697
|
+
nullptr, kCGScrollEventUnitLine, 1, -lines);
|
|
1698
|
+
if (wheel == nullptr) {
|
|
1699
|
+
return;
|
|
1700
|
+
}
|
|
1701
|
+
NSEvent *event = [NSEvent eventWithCGEvent:wheel];
|
|
1702
|
+
CFRelease(wheel);
|
|
1703
|
+
if (event != nil) {
|
|
1704
|
+
[target scrollWheel:event];
|
|
1705
|
+
}
|
|
1706
|
+
});
|
|
1707
|
+
delayMs += 1000;
|
|
1708
|
+
}
|
|
1709
|
+
scriptedDelayMs = delayMs;
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
// BASALT_TEST_TYPE: text to insert into whatever field has focus, for the
|
|
1713
|
+
// same reason BASALT_TEST_TAP exists -- synthesising a real key event means
|
|
1714
|
+
// CGEvent and accessibility permission an automated run does not have.
|
|
1715
|
+
//
|
|
1716
|
+
// It inserts through the field editor, so it skips the key-down path and
|
|
1717
|
+
// the input method and exercises everything above them: the delegate, the
|
|
1718
|
+
// event emitter, React's re-render, and the controlled value coming back
|
|
1719
|
+
// down. The GTK host's version enters through GtkEditable for the same
|
|
1720
|
+
// reason, and the Linux end-to-end suite types with xdotool where it can.
|
|
1721
|
+
if (const char *text = getenv("BASALT_TEST_TYPE")) {
|
|
1722
|
+
NSString *typed = [NSString stringWithUTF8String:text];
|
|
1723
|
+
const char *afterMs = getenv("BASALT_TEST_TYPE_AFTER_MS");
|
|
1724
|
+
const int64_t delayMs = afterMs != nullptr ? strtoll(afterMs, nullptr, 10) : 2500;
|
|
1725
|
+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, delayMs * NSEC_PER_MSEC),
|
|
1726
|
+
dispatch_get_main_queue(),
|
|
1727
|
+
^{
|
|
1728
|
+
NSResponder *responder = gHost.main().window.firstResponder;
|
|
1729
|
+
if (![responder isKindOfClass:[NSText class]]) {
|
|
1730
|
+
NSLog(@"BASALT_TEST_TYPE: no text field has focus");
|
|
1731
|
+
return;
|
|
1732
|
+
}
|
|
1733
|
+
NSLog(@"BASALT_TEST_TYPE: typing \"%@\"", typed);
|
|
1734
|
+
NSText *editor = (NSText *)responder;
|
|
1735
|
+
[editor insertText:typed];
|
|
1736
|
+
});
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
// What the schedule above came to, so BASALT_QUIT_WHEN_SETTLED waits for the
|
|
1740
|
+
// last of it rather than only for the first mount. The typing is the one
|
|
1741
|
+
// instrument here that does not fold into scriptedDelayMs -- it has its own
|
|
1742
|
+
// default and its own override -- so the total is the later of the two.
|
|
1743
|
+
// See core/TestSettle.h.
|
|
1744
|
+
{
|
|
1745
|
+
int64_t scriptedEndsIn = scriptedDelayMs;
|
|
1746
|
+
if (getenv("BASALT_TEST_TYPE") != nullptr) {
|
|
1747
|
+
const char *typeAfter = getenv("BASALT_TEST_TYPE_AFTER_MS");
|
|
1748
|
+
const int64_t typeAt = typeAfter != nullptr ? strtoll(typeAfter, nullptr, 10) : 2500;
|
|
1749
|
+
scriptedEndsIn = std::max(scriptedEndsIn, typeAt);
|
|
1750
|
+
}
|
|
1751
|
+
basalt::noteScriptedInputEndsInMs(scriptedEndsIn);
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
// BASALT_SNAPSHOT: render what is actually on screen to a PNG on the way
|
|
1755
|
+
// out. The tree dump above says what was mounted; this says what it looks
|
|
1756
|
+
// like, and the two fail differently -- a correct tree can still paint
|
|
1757
|
+
// nothing if the layer or the window is wrong.
|
|
1758
|
+
if (const char *snapshot = getenv("BASALT_SNAPSHOT")) {
|
|
1759
|
+
NSString *path = [NSString stringWithUTF8String:snapshot];
|
|
1760
|
+
// BASALT_SNAPSHOT_AFTER_MS: when to take it. A snapshot is only worth
|
|
1761
|
+
// anything if it lands after whatever is being tested, and the taps above
|
|
1762
|
+
// start at 1500ms, so the default would catch the app before its first
|
|
1763
|
+
// press.
|
|
1764
|
+
const char *after = getenv("BASALT_SNAPSHOT_AFTER_MS");
|
|
1765
|
+
const int64_t delayMs = after != nullptr ? strtoll(after, nullptr, 10) : 1000;
|
|
1766
|
+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, delayMs * NSEC_PER_MSEC),
|
|
1767
|
+
dispatch_get_main_queue(),
|
|
1768
|
+
^{
|
|
1769
|
+
if (RnAppKitWriteSnapshot(gHost.main().root, path)) {
|
|
1770
|
+
NSLog(@"wrote a snapshot to %@", path);
|
|
1771
|
+
} else {
|
|
1772
|
+
NSLog(@"could not write a snapshot to %@", path);
|
|
1773
|
+
}
|
|
1774
|
+
});
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
// Same clean exit on SIGINT or SIGTERM, so Ctrl-C and `kill` shut the
|
|
1778
|
+
// runtime down instead of dropping it -- the GTK host has had this since
|
|
1779
|
+
// phase 32 and this one never did. The default disposition kills the
|
|
1780
|
+
// process where it stands, skipping applicationWillTerminate and so
|
|
1781
|
+
// stopAllSurfaces, the teardown ordering and the tree dump.
|
|
1782
|
+
//
|
|
1783
|
+
// A dispatch source is the safe form, and the counterpart of GTK's
|
|
1784
|
+
// g_unix_signal_add: the handler does not run in signal context, it wakes
|
|
1785
|
+
// the main queue and runs there, so ordinary Cocoa calls are allowed. The
|
|
1786
|
+
// signal must be ignored first, because the dispatch source observes the
|
|
1787
|
+
// signal rather than replacing its disposition -- without this the process
|
|
1788
|
+
// still dies before the block runs.
|
|
1789
|
+
static NSMutableArray *sSignalSources = [NSMutableArray array];
|
|
1790
|
+
for (int signo : {SIGINT, SIGTERM}) {
|
|
1791
|
+
signal(signo, SIG_IGN);
|
|
1792
|
+
dispatch_source_t source =
|
|
1793
|
+
dispatch_source_create(DISPATCH_SOURCE_TYPE_SIGNAL, (uintptr_t)signo, 0,
|
|
1794
|
+
dispatch_get_main_queue());
|
|
1795
|
+
dispatch_source_set_event_handler(source, ^{
|
|
1796
|
+
NSLog(@"signal received; quitting");
|
|
1797
|
+
[NSApp terminate:nil];
|
|
1798
|
+
});
|
|
1799
|
+
dispatch_resume(source);
|
|
1800
|
+
// Held in a static, because under ARC a dispatch_source_t is an object
|
|
1801
|
+
// like any other: let it go out of scope at the end of this iteration and
|
|
1802
|
+
// it is released and torn down before it can ever fire, which looks
|
|
1803
|
+
// exactly like the signal not being caught at all.
|
|
1804
|
+
[sSignalSources addObject:source];
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
// BASALT_QUIT_AFTER_MS: quitting on a timer is the only way to exercise the
|
|
1808
|
+
// shutdown path in automation. The window cannot be closed from a script,
|
|
1809
|
+
// and killing the process skips applicationWillTerminate entirely, so
|
|
1810
|
+
// stopAllSurfaces and the teardown ordering below it would never run.
|
|
1811
|
+
if (const char *quitAfter = getenv("BASALT_QUIT_AFTER_MS")) {
|
|
1812
|
+
const long ms = strtol(quitAfter, nullptr, 10);
|
|
1813
|
+
if (ms > 0) {
|
|
1814
|
+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)ms * NSEC_PER_MSEC),
|
|
1815
|
+
dispatch_get_main_queue(),
|
|
1816
|
+
^{
|
|
1817
|
+
NSLog(@"BASALT_QUIT_AFTER_MS elapsed; quitting");
|
|
1818
|
+
endAnyOpenSheets();
|
|
1819
|
+
gQuittingForTest = true;
|
|
1820
|
+
[NSApp terminate:nil];
|
|
1821
|
+
});
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
// BASALT_TEST_QUIT_FILE: the same quit, at a moment the harness picks
|
|
1826
|
+
// rather than on a budget fixed before launch. `terminate:` either way, so
|
|
1827
|
+
// applicationWillTerminate runs and the view tree is dumped -- which is the
|
|
1828
|
+
// whole reason for asking rather than killing. See core/TestQuitFile.h.
|
|
1829
|
+
//
|
|
1830
|
+
// gQuittingForTest for the reason the timed quit sets it: this is the
|
|
1831
|
+
// harness ending the run, and an app that intercepts its own quit would
|
|
1832
|
+
// otherwise refuse it and hang. See applicationShouldTerminate:.
|
|
1833
|
+
//
|
|
1834
|
+
// Two instruments, one timer, because they want the same thing at different
|
|
1835
|
+
// moments: the quit file is the harness deciding, BASALT_QUIT_WHEN_SETTLED is
|
|
1836
|
+
// the app noticing it has nothing left to do. See core/TestSettle.h.
|
|
1837
|
+
if (basalt::testQuitFilePath().has_value() || basalt::quitWhenSettledMs().has_value()) {
|
|
1838
|
+
[NSTimer scheduledTimerWithTimeInterval:(double)basalt::kTestQuitFilePollMs / 1000.0
|
|
1839
|
+
repeats:YES
|
|
1840
|
+
block:^(NSTimer *timer) {
|
|
1841
|
+
const char *why = nullptr;
|
|
1842
|
+
if (basalt::testQuitFileAppeared()) {
|
|
1843
|
+
why = basalt::kTestQuitFileMessage;
|
|
1844
|
+
} else if (basalt::hasSettled()) {
|
|
1845
|
+
why = basalt::kQuitWhenSettledMessage;
|
|
1846
|
+
} else {
|
|
1847
|
+
return;
|
|
1848
|
+
}
|
|
1849
|
+
[timer invalidate];
|
|
1850
|
+
NSLog(@"%s", why);
|
|
1851
|
+
endAnyOpenSheets();
|
|
1852
|
+
gQuittingForTest = true;
|
|
1853
|
+
[NSApp terminate:nil];
|
|
1854
|
+
}];
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
[NSApp run];
|
|
1858
|
+
}
|
|
1859
|
+
return 0;
|
|
1860
|
+
}
|