basalt-gtk 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/runLinux.js +29 -0
- package/cli/shared.js +75 -0
- package/native/CMakeLists.txt +362 -0
- package/native/gtk/ColorSchemeGtk.cpp +58 -0
- package/native/gtk/ComponentRegistryGtk.cpp +101 -0
- package/native/gtk/FontRegistryFontconfig.cpp +127 -0
- package/native/gtk/GtkAnimationChoreographer.cpp +73 -0
- package/native/gtk/GtkAnimationChoreographer.h +49 -0
- package/native/gtk/GtkDropTarget.cpp +269 -0
- package/native/gtk/GtkDropTarget.h +40 -0
- package/native/gtk/GtkFocus.cpp +296 -0
- package/native/gtk/GtkFocus.h +113 -0
- package/native/gtk/GtkImageLoader.cpp +128 -0
- package/native/gtk/GtkImageLoader.h +87 -0
- package/native/gtk/GtkKeyEvents.cpp +91 -0
- package/native/gtk/GtkKeyEvents.h +21 -0
- package/native/gtk/GtkMenuBar.cpp +45 -0
- package/native/gtk/GtkMountingManager.cpp +1027 -0
- package/native/gtk/GtkMountingManager.h +146 -0
- package/native/gtk/GtkRunLoopObserver.cpp +77 -0
- package/native/gtk/GtkRunLoopObserver.h +41 -0
- package/native/gtk/GtkScrollView.cpp +626 -0
- package/native/gtk/GtkScrollView.h +209 -0
- package/native/gtk/GtkSkiaContext.cpp +248 -0
- package/native/gtk/GtkSkiaContext.h +62 -0
- package/native/gtk/GtkSkiaModule.cpp +80 -0
- package/native/gtk/GtkSkiaModule.h +51 -0
- package/native/gtk/GtkSkiaPeer.cpp +285 -0
- package/native/gtk/GtkSkiaPeer.h +63 -0
- package/native/gtk/GtkTextInput.cpp +428 -0
- package/native/gtk/GtkTextInput.h +123 -0
- package/native/gtk/GtkTextPeer.cpp +425 -0
- package/native/gtk/GtkTextPeer.h +87 -0
- package/native/gtk/GtkTitleBar.cpp +273 -0
- package/native/gtk/GtkTitleBar.h +96 -0
- package/native/gtk/GtkTitleBarLayout.cpp +45 -0
- package/native/gtk/GtkTitleBarLayout.h +36 -0
- package/native/gtk/GtkTouchDispatcher.cpp +671 -0
- package/native/gtk/GtkTouchDispatcher.h +139 -0
- package/native/gtk/GtkWindowControl.cpp +223 -0
- package/native/gtk/GtkWindowModule.cpp +194 -0
- package/native/gtk/GtkWindowModule.h +62 -0
- package/native/gtk/PangoTextLayout.cpp +295 -0
- package/native/gtk/PangoTextLayout.h +54 -0
- package/native/gtk/PangoTextLayoutManager.cpp +140 -0
- package/native/gtk/PlatformServicesGtk.cpp +583 -0
- package/native/gtk/RnView.cpp +1407 -0
- package/native/gtk/RnView.h +339 -0
- package/native/gtk/demo_layout_gtk.cpp +57 -0
- package/native/gtk/main_gtk.cpp +1981 -0
- package/native/gtk/mount_harness_gtk.cpp +143 -0
- package/native/tests/TestMainGtk.cpp +25 -0
- package/native/tests/notification_stub.cpp +103 -0
- package/native/tests/test_accessibility.cpp +251 -0
- package/native/tests/test_focus.cpp +194 -0
- package/native/tests/test_hittest.cpp +242 -0
- package/native/tests/test_image.cpp +135 -0
- package/native/tests/test_key_events_gtk.cpp +112 -0
- package/native/tests/test_mounting.cpp +415 -0
- package/native/tests/test_scrollview.cpp +258 -0
- package/native/tests/test_skia_texture.cpp +120 -0
- package/native/tests/test_text.cpp +147 -0
- package/native/tests/test_textinput.cpp +598 -0
- package/native/tests/test_titlebar.cpp +137 -0
- package/native/tests/test_titlebar_drag.cpp +138 -0
- package/native/tests/test_version.cpp +49 -0
- package/native/tests/test_view.cpp +162 -0
- package/native/tests/test_viewprops.cpp +254 -0
- package/package.json +26 -0
- package/react-native.config.js +21 -0
|
@@ -0,0 +1,1981 @@
|
|
|
1
|
+
// basalt-core — the host process.
|
|
2
|
+
//
|
|
3
|
+
// Constructs a ReactHost, loads a script into Hermes, and runs one surface in
|
|
4
|
+
// a GtkWindow. Everything on screen from here on is produced by Fabric: the
|
|
5
|
+
// mutation stream arrives through GtkMountingManager exactly as it does on
|
|
6
|
+
// iOS and Android, and nothing hand-builds a ShadowViewMutation.
|
|
7
|
+
//
|
|
8
|
+
// The pieces a host has to supply, and where each one comes from:
|
|
9
|
+
//
|
|
10
|
+
// IMountingManager GtkMountingManager (this repo)
|
|
11
|
+
// RunLoopObserverManager ReactCxxPlatform (event beat)
|
|
12
|
+
// AnimationChoreographer GtkAnimationChoreographer (this repo, frame clock)
|
|
13
|
+
// ContextContainer http + websocket client factories, below
|
|
14
|
+
// ComponentRegistryFactory ComponentRegistry (via the mounting manager)
|
|
15
|
+
//
|
|
16
|
+
// Threading: GTK owns this thread. ReactHost spins up its own JS thread and
|
|
17
|
+
// every mount is marshalled back here by GtkMountingManager. Nothing below
|
|
18
|
+
// touches a widget off the main thread.
|
|
19
|
+
|
|
20
|
+
#include "GtkAnimationChoreographer.h"
|
|
21
|
+
#include "AppIdentity.h"
|
|
22
|
+
#include "AppPaths.h"
|
|
23
|
+
#include "DevMenu.h"
|
|
24
|
+
#include "WindowControl.h"
|
|
25
|
+
#include "WindowHost.h"
|
|
26
|
+
#include "DialogModule.h"
|
|
27
|
+
#include "MenuModel.h"
|
|
28
|
+
#include "MenuModule.h"
|
|
29
|
+
#include "WindowsModule.h"
|
|
30
|
+
#include "TestKeys.h"
|
|
31
|
+
#include "TestQuitFile.h"
|
|
32
|
+
#include "TestSettle.h"
|
|
33
|
+
#include "GtkMountingManager.h"
|
|
34
|
+
|
|
35
|
+
#include <react/io/ImageLoaderModule.h>
|
|
36
|
+
#include "GtkRunLoopObserver.h"
|
|
37
|
+
#include "GtkFocus.h"
|
|
38
|
+
#include "LogBoxSurface.h"
|
|
39
|
+
|
|
40
|
+
#include <react/renderer/animated/NativeAnimatedNodesManagerProvider.h>
|
|
41
|
+
#include "GtkTouchDispatcher.h"
|
|
42
|
+
#include "RnView.h"
|
|
43
|
+
|
|
44
|
+
#include <glog/logging.h>
|
|
45
|
+
#include <jsi/jsi.h>
|
|
46
|
+
#include <logger/react_native_log.h>
|
|
47
|
+
#include <react/http/IHttpClient.h>
|
|
48
|
+
#include <react/http/IWebSocketClient.h>
|
|
49
|
+
#include <react/logging/DefaultOnJsErrorHandler.h>
|
|
50
|
+
#include <react/renderer/core/LayoutConstraints.h>
|
|
51
|
+
#include <react/renderer/core/LayoutContext.h>
|
|
52
|
+
#include <react/runtime/ReactHost.h>
|
|
53
|
+
#include <react/runtime/ReactInstanceConfig.h>
|
|
54
|
+
#include <react/utils/ContextContainer.h>
|
|
55
|
+
#include <react/utils/RunLoopObserverManager.h>
|
|
56
|
+
|
|
57
|
+
#include <gtk/gtk.h>
|
|
58
|
+
|
|
59
|
+
#include <algorithm>
|
|
60
|
+
#include <cstdlib>
|
|
61
|
+
#include <exception>
|
|
62
|
+
#include "AppearanceModule.h"
|
|
63
|
+
#include "BlobModule.h"
|
|
64
|
+
#include "CoreModules.h"
|
|
65
|
+
#include "ColorScheme.h"
|
|
66
|
+
#include "DevBundle.h"
|
|
67
|
+
#include "DragAndDrop.h"
|
|
68
|
+
#include "GtkDropTarget.h"
|
|
69
|
+
#include "GtkTitleBar.h"
|
|
70
|
+
// Which parts of an app-drawn header drag the window; see the gesture below.
|
|
71
|
+
#include "GtkTitleBarLayout.h"
|
|
72
|
+
#include "GtkWindowModule.h"
|
|
73
|
+
|
|
74
|
+
#ifdef BASALT_HAS_SKIA
|
|
75
|
+
#include "GtkSkiaModule.h"
|
|
76
|
+
#endif
|
|
77
|
+
#include "ExpoModules.h"
|
|
78
|
+
#include "GestureHandlerModule.h"
|
|
79
|
+
#include "ReanimatedModule.h"
|
|
80
|
+
#include "JsRuntimeAccess.h"
|
|
81
|
+
#include "PackageModules.h"
|
|
82
|
+
#include "UIManagerAccess.h"
|
|
83
|
+
#include "WorkletsModule.h"
|
|
84
|
+
#include "ExpoRuntime.h"
|
|
85
|
+
#include "TurboModuleProxy.h"
|
|
86
|
+
#include "PlatformConstantsModule.h"
|
|
87
|
+
#include "SourceCodeModule.h"
|
|
88
|
+
#include "StatusBarModule.h"
|
|
89
|
+
|
|
90
|
+
#include <react/featureflags/ReactNativeFeatureFlags.h>
|
|
91
|
+
#include <react/featureflags/ReactNativeFeatureFlagsDefaults.h>
|
|
92
|
+
|
|
93
|
+
#include <glib-unix.h>
|
|
94
|
+
|
|
95
|
+
#include <csignal>
|
|
96
|
+
#include <memory>
|
|
97
|
+
#include <string>
|
|
98
|
+
|
|
99
|
+
using facebook::react::ContextContainer;
|
|
100
|
+
using facebook::react::Float;
|
|
101
|
+
using facebook::react::LayoutConstraints;
|
|
102
|
+
using facebook::react::LayoutContext;
|
|
103
|
+
using facebook::react::LayoutDirection;
|
|
104
|
+
using facebook::react::ReactHost;
|
|
105
|
+
using facebook::react::ReactInstanceConfig;
|
|
106
|
+
using facebook::react::RunLoopObserverManager;
|
|
107
|
+
using facebook::react::Size;
|
|
108
|
+
using facebook::react::SurfaceId;
|
|
109
|
+
|
|
110
|
+
namespace {
|
|
111
|
+
|
|
112
|
+
// In Fabric a SurfaceId *is* the root shadow node's tag, which is what lets the
|
|
113
|
+
// surface root live in the mounting manager's registry like any other view.
|
|
114
|
+
constexpr SurfaceId kSurfaceId = 1;
|
|
115
|
+
|
|
116
|
+
// React Native's error inspector, which is a surface of its own -- registered
|
|
117
|
+
// by AppRegistry under the name "LogBox", exactly as an app registers its own
|
|
118
|
+
// component. Started over the app's when NativeLogBox.show() asks; see
|
|
119
|
+
// core/LogBoxSurface.h.
|
|
120
|
+
constexpr SurfaceId kLogBoxSurfaceId = 2;
|
|
121
|
+
|
|
122
|
+
constexpr int kInitialWidth = 900;
|
|
123
|
+
constexpr int kInitialHeight = 700;
|
|
124
|
+
|
|
125
|
+
// The entry point for the *scriptless* mode, where the bundle is a hand-written
|
|
126
|
+
// script talking to nativeFabricUIManager directly rather than a React app.
|
|
127
|
+
// See js/demo.js. Only used when no module name is given.
|
|
128
|
+
constexpr const char *kRenderFunctionName = "basaltRender";
|
|
129
|
+
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
// Host state
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
// One window, and the surface in it.
|
|
135
|
+
//
|
|
136
|
+
// A window is a surface: a surface is what has a size, a layout context and a
|
|
137
|
+
// root shadow node, and two windows sharing one would be two windows sharing a
|
|
138
|
+
// layout. So the id a window is known by *is* its surface id, and there is no
|
|
139
|
+
// second numbering to keep in step. See core/WindowHost.h.
|
|
140
|
+
//
|
|
141
|
+
// Everything here used to be a field on Host, because there was only ever one.
|
|
142
|
+
// What is still on Host is what is genuinely per-process -- the ReactHost, the
|
|
143
|
+
// mounting manager, the run loop -- and what this project has not yet made
|
|
144
|
+
// per-window: the title bar and the error inspector are the main window's.
|
|
145
|
+
struct HostWindow {
|
|
146
|
+
facebook::react::SurfaceId surfaceId{0};
|
|
147
|
+
GtkWindow *window{nullptr};
|
|
148
|
+
RnView *root{nullptr};
|
|
149
|
+
// The surface root sits in this, with room above it for the window controls
|
|
150
|
+
// the host draws when the title bar is hidden.
|
|
151
|
+
GtkWidget *overlay{nullptr};
|
|
152
|
+
std::unique_ptr<basalt::GtkTouchDispatcher> touchDispatcher;
|
|
153
|
+
std::unique_ptr<basalt::GtkFocusManager> focusManager;
|
|
154
|
+
int scaleFactor{1};
|
|
155
|
+
// Back to the host, for the GTK callbacks that take one pointer.
|
|
156
|
+
struct Host *host{nullptr};
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
struct Host {
|
|
160
|
+
// Every window, main one first. Never empty after onActivate.
|
|
161
|
+
std::vector<std::unique_ptr<HostWindow>> windows;
|
|
162
|
+
|
|
163
|
+
// The window the app was started in. Shorthand for windows.front(), which is
|
|
164
|
+
// what every part of this host that is not yet per-window uses.
|
|
165
|
+
// An empty window with null fields before there is a real one, rather than
|
|
166
|
+
// dereferencing an empty vector. Several things here run before the window
|
|
167
|
+
// exists -- a repaint request, the spinner timer -- and each one already
|
|
168
|
+
// guards on `window == nullptr`, which is exactly what this keeps true.
|
|
169
|
+
HostWindow &main() {
|
|
170
|
+
static HostWindow none;
|
|
171
|
+
return windows.empty() ? none : *windows.front();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
HostWindow *windowFor(facebook::react::SurfaceId surfaceId) {
|
|
175
|
+
for (const auto &candidate : windows) {
|
|
176
|
+
if (candidate->surfaceId == surfaceId) {
|
|
177
|
+
return candidate.get();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return nullptr;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// The GtkApplication, so a window can be made after startup. A window must
|
|
184
|
+
// belong to one, or GTK will not manage it.
|
|
185
|
+
GtkApplication *application{nullptr};
|
|
186
|
+
// What `gtk_application_inhibit` handed back while an app was holding the
|
|
187
|
+
// session open, or 0. See the "query-end" handler.
|
|
188
|
+
guint quitInhibitCookie{0};
|
|
189
|
+
|
|
190
|
+
std::shared_ptr<basalt::GtkMountingManager> mountingManager;
|
|
191
|
+
std::shared_ptr<RunLoopObserverManager> runLoopObserverManager;
|
|
192
|
+
std::shared_ptr<basalt::GtkAnimationChoreographer> choreographer;
|
|
193
|
+
// The error inspector's own root when it is showing. It goes above the main
|
|
194
|
+
// window's controls, which is what a modal box should do -- and it is the
|
|
195
|
+
// main window's alone: an error is about the app rather than about a window.
|
|
196
|
+
RnView *logBoxRoot{nullptr};
|
|
197
|
+
std::unique_ptr<ReactHost> reactHost;
|
|
198
|
+
|
|
199
|
+
// Drives RunLoopObserverManager::onRender, without which no event an emitter
|
|
200
|
+
// produces ever reaches JavaScript.
|
|
201
|
+
GSource *runLoopObserver{nullptr};
|
|
202
|
+
|
|
203
|
+
std::string bundlePath;
|
|
204
|
+
// Empty means the bundle is a raw Fabric script rather than a React app; see
|
|
205
|
+
// startSurface below for what that changes.
|
|
206
|
+
std::string moduleName;
|
|
207
|
+
// Metro's entry point, without the extension. Only meaningful in dev mode:
|
|
208
|
+
// DevServerHelper builds "http://host:port/<sourcePath>.bundle?..." from it,
|
|
209
|
+
// and returns an empty URL if it is unset, which silently falls back to the
|
|
210
|
+
// on-disk bundle.
|
|
211
|
+
std::string sourcePath;
|
|
212
|
+
bool surfaceStarted{false};
|
|
213
|
+
// Whether this run is a development one, which is the only thing that
|
|
214
|
+
// decides whether Ctrl+D opens anything. Kept because the ReactInstanceConfig
|
|
215
|
+
// it came from is not.
|
|
216
|
+
bool devMode{false};
|
|
217
|
+
// Read by main() after the loop ends. GApplication has no exit status to set
|
|
218
|
+
// any more, and a startup that failed has to be tellable from one that ran.
|
|
219
|
+
int exitStatus{0};
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
// Logging
|
|
224
|
+
// ---------------------------------------------------------------------------
|
|
225
|
+
|
|
226
|
+
// ReactHost's Logger is where console.log and RN's own logging come out.
|
|
227
|
+
void logToGlib(const std::string &message, unsigned int logLevel) {
|
|
228
|
+
switch (logLevel) {
|
|
229
|
+
case ReactNativeLogLevelFatal:
|
|
230
|
+
case ReactNativeLogLevelError:
|
|
231
|
+
g_warning("[js] %s", message.c_str());
|
|
232
|
+
break;
|
|
233
|
+
case ReactNativeLogLevelWarning:
|
|
234
|
+
g_message("[js warn] %s", message.c_str());
|
|
235
|
+
break;
|
|
236
|
+
default:
|
|
237
|
+
g_message("[js] %s", message.c_str());
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ---------------------------------------------------------------------------
|
|
243
|
+
// Layout constraints
|
|
244
|
+
// ---------------------------------------------------------------------------
|
|
245
|
+
|
|
246
|
+
// Minimum == maximum pins the root to the window's content box, which is what
|
|
247
|
+
// a full-window surface wants: the root fills, and flex children divide it.
|
|
248
|
+
// A surface that sized itself to its content would leave minimumSize at zero.
|
|
249
|
+
LayoutConstraints constraintsFor(int width, int height) {
|
|
250
|
+
const Size size{.width = static_cast<Float>(width), .height = static_cast<Float>(height)};
|
|
251
|
+
return LayoutConstraints{
|
|
252
|
+
.minimumSize = size,
|
|
253
|
+
.maximumSize = size,
|
|
254
|
+
.layoutDirection = LayoutDirection::LeftToRight,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
LayoutContext layoutContextFor(int scaleFactor) {
|
|
259
|
+
return LayoutContext{.pointScaleFactor = static_cast<Float>(scaleFactor)};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ---------------------------------------------------------------------------
|
|
263
|
+
// Calling into JS
|
|
264
|
+
// ---------------------------------------------------------------------------
|
|
265
|
+
|
|
266
|
+
// Runs on the JS thread. `step` lets the script commit a second, different tree
|
|
267
|
+
// so an Update/Remove diff can be seen coming out of Fabric rather than only an
|
|
268
|
+
// initial mount.
|
|
269
|
+
void callRenderFunction(Host *host, int step) {
|
|
270
|
+
if (host->reactHost == nullptr) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
host->reactHost->runOnRuntimeScheduler([step](facebook::jsi::Runtime &runtime) {
|
|
274
|
+
try {
|
|
275
|
+
auto value = runtime.global().getProperty(runtime, kRenderFunctionName);
|
|
276
|
+
if (!value.isObject() || !value.getObject(runtime).isFunction(runtime)) {
|
|
277
|
+
g_warning("script defines no global %s()", kRenderFunctionName);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
value.getObject(runtime).getFunction(runtime).call(
|
|
281
|
+
runtime,
|
|
282
|
+
facebook::jsi::Value(static_cast<int>(kSurfaceId)),
|
|
283
|
+
facebook::jsi::Value(step));
|
|
284
|
+
} catch (const facebook::jsi::JSError &error) {
|
|
285
|
+
g_warning("%s() threw: %s\n%s",
|
|
286
|
+
kRenderFunctionName,
|
|
287
|
+
error.getMessage().c_str(),
|
|
288
|
+
error.getStack().c_str());
|
|
289
|
+
} catch (const std::exception &error) {
|
|
290
|
+
g_warning("%s() failed: %s", kRenderFunctionName, error.what());
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Quitting on a timer is the only way to exercise the shutdown path in
|
|
296
|
+
// automation: the window cannot be closed from a script, and killing the
|
|
297
|
+
// process skips GApplication::shutdown entirely, so stopAllSurfaces and the
|
|
298
|
+
// teardown ordering below it would never run under test. Opt in with
|
|
299
|
+
// BASALT_QUIT_AFTER_MS.
|
|
300
|
+
gboolean quitAfterTimeout(gpointer data) {
|
|
301
|
+
auto *app = static_cast<GApplication *>(data);
|
|
302
|
+
g_message("BASALT_QUIT_AFTER_MS elapsed; quitting");
|
|
303
|
+
g_application_quit(app);
|
|
304
|
+
return G_SOURCE_REMOVE;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// The same quit, when the harness asks for it rather than on a budget fixed
|
|
308
|
+
// before the process started. `g_application_quit` either way, so `onShutdown`
|
|
309
|
+
// runs and the widget tree is dumped -- which is the whole point of asking
|
|
310
|
+
// rather than killing. See core/TestQuitFile.h.
|
|
311
|
+
gboolean quitWhenTestFileAppears(gpointer data) {
|
|
312
|
+
// Two instruments, one poll, because they want the same thing at different
|
|
313
|
+
// moments: BASALT_TEST_QUIT_FILE is the harness deciding, and
|
|
314
|
+
// BASALT_QUIT_WHEN_SETTLED is the app noticing it has nothing left to do. A
|
|
315
|
+
// second timer would be a second id to collide and a second place to forget
|
|
316
|
+
// the shutdown ordering.
|
|
317
|
+
const char *why = nullptr;
|
|
318
|
+
if (basalt::testQuitFileAppeared()) {
|
|
319
|
+
why = basalt::kTestQuitFileMessage;
|
|
320
|
+
} else if (basalt::hasSettled()) {
|
|
321
|
+
why = basalt::kQuitWhenSettledMessage;
|
|
322
|
+
} else {
|
|
323
|
+
return G_SOURCE_CONTINUE;
|
|
324
|
+
}
|
|
325
|
+
auto *app = static_cast<GApplication *>(data);
|
|
326
|
+
g_message("%s", why);
|
|
327
|
+
g_application_quit(app);
|
|
328
|
+
return G_SOURCE_REMOVE;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Same clean exit, on SIGINT or SIGTERM, so Ctrl-C and `kill` shut the runtime
|
|
332
|
+
// down instead of dropping it. The default disposition would kill the process
|
|
333
|
+
// where it stands, skipping GApplication::shutdown and everything under it --
|
|
334
|
+
// stopAllSurfaces, the teardown ordering, and the widget tree dump.
|
|
335
|
+
//
|
|
336
|
+
// g_unix_signal_add is the safe form: it does not run in the signal handler,
|
|
337
|
+
// it wakes the main loop and dispatches from there, so ordinary GLib calls are
|
|
338
|
+
// allowed here.
|
|
339
|
+
gboolean quitOnSignal(gpointer data) {
|
|
340
|
+
auto *app = static_cast<GApplication *>(data);
|
|
341
|
+
g_message("signal received; quitting");
|
|
342
|
+
g_application_quit(app);
|
|
343
|
+
return G_SOURCE_REMOVE;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// BASALT_TEST_TAP: "x,y" pairs separated by ';', each fired a second apart.
|
|
347
|
+
// See GtkTouchDispatcher::synthesiseTap for why this exists.
|
|
348
|
+
//
|
|
349
|
+
// A point may name a window: "x,y@3" taps in the window whose surface is 3
|
|
350
|
+
// rather than in the app's own. Without it there would be no way to reach a
|
|
351
|
+
// second window at all -- each has its own touch dispatcher, which is the whole
|
|
352
|
+
// point of them, and the main window's would happily hit-test a tree that is
|
|
353
|
+
// not on screen.
|
|
354
|
+
//
|
|
355
|
+
// BASALT_TEST_SECONDARY_TAP takes the same spec and clicks the other button.
|
|
356
|
+
// Its own variable rather than a suffix on a point, because the two assert
|
|
357
|
+
// opposite things -- one presses what it lands on and the other must not -- and
|
|
358
|
+
// a scenario that mixed them in one string would be harder to read than to
|
|
359
|
+
// write.
|
|
360
|
+
struct PendingTap {
|
|
361
|
+
Host *host;
|
|
362
|
+
double x;
|
|
363
|
+
double y;
|
|
364
|
+
facebook::react::SurfaceId surfaceId;
|
|
365
|
+
basalt::PointerButton button;
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
gboolean fireTestTap(gpointer data) {
|
|
369
|
+
std::unique_ptr<PendingTap> tap{static_cast<PendingTap *>(data)};
|
|
370
|
+
const bool secondary = tap->button != basalt::PointerButton::Primary;
|
|
371
|
+
g_message("%s: tapping (%.0f, %.0f) in window %d",
|
|
372
|
+
secondary ? "BASALT_TEST_SECONDARY_TAP" : "BASALT_TEST_TAP",
|
|
373
|
+
tap->x,
|
|
374
|
+
tap->y,
|
|
375
|
+
static_cast<int>(tap->surfaceId));
|
|
376
|
+
HostWindow *target = tap->host->windowFor(tap->surfaceId);
|
|
377
|
+
if (target == nullptr) {
|
|
378
|
+
g_warning("BASALT_TEST_TAP: no window %d", static_cast<int>(tap->surfaceId));
|
|
379
|
+
return G_SOURCE_REMOVE;
|
|
380
|
+
}
|
|
381
|
+
if (target->touchDispatcher != nullptr) {
|
|
382
|
+
target->touchDispatcher->synthesiseTap(tap->x, tap->y, tap->button);
|
|
383
|
+
}
|
|
384
|
+
return G_SOURCE_REMOVE;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Splits "x,y@surface" into its point and its window. The window defaults to
|
|
388
|
+
// the app's own, so every spec written before windows existed still means what
|
|
389
|
+
// it did.
|
|
390
|
+
void parseTestPoint(const char *text,
|
|
391
|
+
double *x,
|
|
392
|
+
double *y,
|
|
393
|
+
facebook::react::SurfaceId *surfaceId) {
|
|
394
|
+
*surfaceId = kSurfaceId;
|
|
395
|
+
char **halves = g_strsplit(text, "@", 2);
|
|
396
|
+
if (halves[1] != nullptr) {
|
|
397
|
+
*surfaceId = static_cast<facebook::react::SurfaceId>(g_ascii_strtoll(halves[1], nullptr, 10));
|
|
398
|
+
}
|
|
399
|
+
char **parts = g_strsplit(halves[0], ",", 2);
|
|
400
|
+
*x = parts[0] != nullptr ? g_ascii_strtod(parts[0], nullptr) : 0.0;
|
|
401
|
+
*y = parts[1] != nullptr ? g_ascii_strtod(parts[1], nullptr) : 0.0;
|
|
402
|
+
const bool complete = parts[0] != nullptr && parts[1] != nullptr;
|
|
403
|
+
g_strfreev(parts);
|
|
404
|
+
g_strfreev(halves);
|
|
405
|
+
if (!complete) {
|
|
406
|
+
*x = -1.0;
|
|
407
|
+
*y = -1.0;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Returns the delay after the last tap, so typing can be scheduled behind it.
|
|
412
|
+
guint scheduleTestTaps(Host *host,
|
|
413
|
+
const char *spec,
|
|
414
|
+
guint delayMs,
|
|
415
|
+
basalt::PointerButton button) {
|
|
416
|
+
char **points = g_strsplit(spec, ";", -1);
|
|
417
|
+
for (char **point = points; *point != nullptr; ++point) {
|
|
418
|
+
if (**point == '\0') {
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
double x = 0.0;
|
|
422
|
+
double y = 0.0;
|
|
423
|
+
facebook::react::SurfaceId surfaceId = kSurfaceId;
|
|
424
|
+
parseTestPoint(*point, &x, &y, &surfaceId);
|
|
425
|
+
if (x >= 0.0 && y >= 0.0) {
|
|
426
|
+
g_timeout_add(delayMs, fireTestTap, new PendingTap{host, x, y, surfaceId, button});
|
|
427
|
+
delayMs += 1000;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
g_strfreev(points);
|
|
431
|
+
return delayMs;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// BASALT_TEST_HOVER: "x,y" points separated by ';', each fired a second apart.
|
|
435
|
+
// The pointer moving with no button down, which is the only way to reach the
|
|
436
|
+
// hover path from automation: a real cursor move needs a seat with input
|
|
437
|
+
// devices, and the whole point of these instruments is a run that has none. A
|
|
438
|
+
// negative point means the pointer left the surface.
|
|
439
|
+
//
|
|
440
|
+
// Reuses PendingTap: the payload is the same, and a second struct with the same
|
|
441
|
+
// three fields would be two things to keep in step.
|
|
442
|
+
gboolean fireTestHover(gpointer data) {
|
|
443
|
+
std::unique_ptr<PendingTap> hover{static_cast<PendingTap *>(data)};
|
|
444
|
+
g_message("BASALT_TEST_HOVER: hovering (%.0f, %.0f)", hover->x, hover->y);
|
|
445
|
+
if (hover->host->main().touchDispatcher != nullptr) {
|
|
446
|
+
hover->host->main().touchDispatcher->synthesiseHover(hover->x, hover->y);
|
|
447
|
+
}
|
|
448
|
+
return G_SOURCE_REMOVE;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Returns the delay after the last point.
|
|
452
|
+
guint scheduleTestHovers(Host *host, const char *spec, guint delayMs) {
|
|
453
|
+
char **points = g_strsplit(spec, ";", -1);
|
|
454
|
+
for (char **point = points; *point != nullptr; ++point) {
|
|
455
|
+
char **parts = g_strsplit(*point, ",", 2);
|
|
456
|
+
if (parts[0] != nullptr && parts[1] != nullptr) {
|
|
457
|
+
g_timeout_add(delayMs,
|
|
458
|
+
fireTestHover,
|
|
459
|
+
new PendingTap{host,
|
|
460
|
+
g_ascii_strtod(parts[0], nullptr),
|
|
461
|
+
g_ascii_strtod(parts[1], nullptr),
|
|
462
|
+
// Hover has no per-window form: the pointer
|
|
463
|
+
// is one thing, and a second window's hover
|
|
464
|
+
// is a feature nothing has asked for yet.
|
|
465
|
+
kSurfaceId,
|
|
466
|
+
// Nor a button: a hovering pointer presses
|
|
467
|
+
// nothing, and this field is only read by
|
|
468
|
+
// the tap path.
|
|
469
|
+
basalt::PointerButton::Primary});
|
|
470
|
+
delayMs += 1000;
|
|
471
|
+
}
|
|
472
|
+
g_strfreev(parts);
|
|
473
|
+
}
|
|
474
|
+
g_strfreev(points);
|
|
475
|
+
return delayMs;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// BASALT_TEST_FOCUS: keyboard actions separated by ';' -- `tab`, `shift-tab`,
|
|
479
|
+
// `activate`, `escape` and `devmenu`, each fired a second apart.
|
|
480
|
+
//
|
|
481
|
+
// The same reason the other instruments exist, one step further out. A real Tab
|
|
482
|
+
// needs a window the display server considers focused, and an automated run on
|
|
483
|
+
// a headless compositor has no such thing: the key event is delivered to
|
|
484
|
+
// nothing. This enters at GtkFocusManager, so it exercises GTK's own focus
|
|
485
|
+
// chain, the focus and blur events and the click dispatch, and skips only the
|
|
486
|
+
// delivery of the keystroke itself.
|
|
487
|
+
struct PendingFocus {
|
|
488
|
+
Host *host;
|
|
489
|
+
std::string action;
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
gboolean fireTestFocus(gpointer data) {
|
|
493
|
+
std::unique_ptr<PendingFocus> pending{static_cast<PendingFocus *>(data)};
|
|
494
|
+
g_message("BASALT_TEST_FOCUS: %s", pending->action.c_str());
|
|
495
|
+
if (pending->host->main().focusManager == nullptr) {
|
|
496
|
+
return G_SOURCE_REMOVE;
|
|
497
|
+
}
|
|
498
|
+
if (pending->action == "tab") {
|
|
499
|
+
pending->host->main().focusManager->moveFocus(true);
|
|
500
|
+
} else if (pending->action == "shift-tab") {
|
|
501
|
+
pending->host->main().focusManager->moveFocus(false);
|
|
502
|
+
} else if (pending->action == "activate") {
|
|
503
|
+
pending->host->main().focusManager->activateFocused();
|
|
504
|
+
} else if (pending->action == "devmenu") {
|
|
505
|
+
// Also not a focus action. Same instrument for the same reason: a key that
|
|
506
|
+
// needs nothing focused to arrive. What it opens is answered by
|
|
507
|
+
// BASALT_TEST_MENU; see core/TestDialog.h.
|
|
508
|
+
basalt::showDevMenu(pending->host->reactHost.get());
|
|
509
|
+
} else if (pending->action == "escape") {
|
|
510
|
+
// Not a focus action, and here anyway: this is the instrument for "a key
|
|
511
|
+
// was pressed and nothing on the window has to be focused for it to
|
|
512
|
+
// arrive", which is exactly what Escape closing a <Modal> is.
|
|
513
|
+
pending->host->mountingManager->requestCloseTopModal();
|
|
514
|
+
} else {
|
|
515
|
+
g_warning("BASALT_TEST_FOCUS: unknown action \"%s\"", pending->action.c_str());
|
|
516
|
+
}
|
|
517
|
+
return G_SOURCE_REMOVE;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
guint scheduleTestFocus(Host *host, const char *spec, guint delayMs) {
|
|
521
|
+
char **actions = g_strsplit(spec, ";", -1);
|
|
522
|
+
for (char **action = actions; *action != nullptr; ++action) {
|
|
523
|
+
if (**action == '\0') {
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
526
|
+
g_timeout_add(delayMs, fireTestFocus, new PendingFocus{host, *action});
|
|
527
|
+
delayMs += 1000;
|
|
528
|
+
}
|
|
529
|
+
g_strfreev(actions);
|
|
530
|
+
return delayMs;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// BASALT_TEST_SCROLL: "x,y,lines" triples separated by ';' -- a wheel over a
|
|
534
|
+
// point, a second apart, in surface-root coordinates. Positive lines scroll
|
|
535
|
+
// down, which is the direction `contentOffset` reads.
|
|
536
|
+
//
|
|
537
|
+
// The AppKit and Win32 hosts have had this since their scroll views did; this
|
|
538
|
+
// is the third, and it arrived with <RefreshControl>, whose gesture is a wheel
|
|
539
|
+
// that keeps asking to go up after the list has already reached its top.
|
|
540
|
+
struct PendingScroll {
|
|
541
|
+
Host *host;
|
|
542
|
+
double x;
|
|
543
|
+
double y;
|
|
544
|
+
double lines;
|
|
545
|
+
};
|
|
546
|
+
|
|
547
|
+
gboolean fireTestScroll(gpointer data) {
|
|
548
|
+
std::unique_ptr<PendingScroll> pending{static_cast<PendingScroll *>(data)};
|
|
549
|
+
Host *host = pending->host;
|
|
550
|
+
if (host->mountingManager == nullptr || host->main().root == nullptr) {
|
|
551
|
+
return G_SOURCE_REMOVE;
|
|
552
|
+
}
|
|
553
|
+
g_message("BASALT_TEST_SCROLL: %g lines at (%g, %g)", pending->lines, pending->x, pending->y);
|
|
554
|
+
// Notches into pixels with the same constant a real wheel uses, so the
|
|
555
|
+
// instrument and the hardware move a list by the same distance.
|
|
556
|
+
host->mountingManager->scrollViews().scrollAt(
|
|
557
|
+
host->main().root, pending->x, pending->y, 0.0, pending->lines * basalt::GtkScrollViewManager::kWheelStepPixels);
|
|
558
|
+
return G_SOURCE_REMOVE;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
guint scheduleTestScrolls(Host *host, const char *spec, guint delayMs) {
|
|
562
|
+
char **steps = g_strsplit(spec, ";", -1);
|
|
563
|
+
for (char **step = steps; *step != nullptr; ++step) {
|
|
564
|
+
char **parts = g_strsplit(*step, ",", -1);
|
|
565
|
+
if (g_strv_length(parts) == 3) {
|
|
566
|
+
g_timeout_add(delayMs,
|
|
567
|
+
fireTestScroll,
|
|
568
|
+
new PendingScroll{host,
|
|
569
|
+
g_ascii_strtod(parts[0], nullptr),
|
|
570
|
+
g_ascii_strtod(parts[1], nullptr),
|
|
571
|
+
g_ascii_strtod(parts[2], nullptr)});
|
|
572
|
+
delayMs += 1000;
|
|
573
|
+
}
|
|
574
|
+
g_strfreev(parts);
|
|
575
|
+
}
|
|
576
|
+
g_strfreev(steps);
|
|
577
|
+
return delayMs;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// BASALT_TEST_DRAG: "x1,y1,x2,y2" -- one press, twenty moves and a release, for
|
|
581
|
+
// the gestures a tap cannot reach. See GtkTouchDispatcher::synthesiseDrag.
|
|
582
|
+
struct PendingDrag {
|
|
583
|
+
Host *host;
|
|
584
|
+
double fromX;
|
|
585
|
+
double fromY;
|
|
586
|
+
double toX;
|
|
587
|
+
double toY;
|
|
588
|
+
};
|
|
589
|
+
|
|
590
|
+
gboolean fireTestDrag(gpointer data) {
|
|
591
|
+
std::unique_ptr<PendingDrag> drag{static_cast<PendingDrag *>(data)};
|
|
592
|
+
g_message("BASALT_TEST_DRAG: (%.0f, %.0f) -> (%.0f, %.0f)",
|
|
593
|
+
drag->fromX,
|
|
594
|
+
drag->fromY,
|
|
595
|
+
drag->toX,
|
|
596
|
+
drag->toY);
|
|
597
|
+
if (drag->host->main().touchDispatcher != nullptr) {
|
|
598
|
+
drag->host->main().touchDispatcher->synthesiseDrag(
|
|
599
|
+
drag->fromX, drag->fromY, drag->toX, drag->toY, 20);
|
|
600
|
+
}
|
|
601
|
+
return G_SOURCE_REMOVE;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
void scheduleTestDrag(Host *host, const char *spec, guint delayMs) {
|
|
605
|
+
char **parts = g_strsplit(spec, ",", 4);
|
|
606
|
+
if (g_strv_length(parts) == 4) {
|
|
607
|
+
g_timeout_add(delayMs,
|
|
608
|
+
fireTestDrag,
|
|
609
|
+
new PendingDrag{host,
|
|
610
|
+
g_ascii_strtod(parts[0], nullptr),
|
|
611
|
+
g_ascii_strtod(parts[1], nullptr),
|
|
612
|
+
g_ascii_strtod(parts[2], nullptr),
|
|
613
|
+
g_ascii_strtod(parts[3], nullptr)});
|
|
614
|
+
}
|
|
615
|
+
g_strfreev(parts);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// BASALT_TEST_TYPE: text to insert into whatever field has focus, for the
|
|
619
|
+
// same reason BASALT_TEST_TAP exists -- macOS cannot synthesise a real key
|
|
620
|
+
// event without accessibility permission an automated run does not have.
|
|
621
|
+
//
|
|
622
|
+
// It inserts through GtkEditable rather than through GDK, so it skips the key
|
|
623
|
+
// controller and the input method and exercises everything above them: the
|
|
624
|
+
// changed signal, the event emitter, React's re-render, and the controlled
|
|
625
|
+
// value coming back down. On Linux the end-to-end suite types with xdotool
|
|
626
|
+
// instead, which does go through GDK.
|
|
627
|
+
struct PendingType {
|
|
628
|
+
Host *host;
|
|
629
|
+
std::string text;
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
// BASALT_TEST_KEY's, for the same reason: a timeout carries one pointer and this
|
|
633
|
+
// needs the host as well as the press.
|
|
634
|
+
struct PendingKey {
|
|
635
|
+
Host *host;
|
|
636
|
+
basalt::KeyCombination pressed;
|
|
637
|
+
};
|
|
638
|
+
|
|
639
|
+
gboolean fireTestKey(gpointer data) {
|
|
640
|
+
auto *pending = static_cast<PendingKey *>(data);
|
|
641
|
+
g_message("BASALT_TEST_KEY: %s", pending->pressed.key.c_str());
|
|
642
|
+
if (pending->host->main().focusManager != nullptr) {
|
|
643
|
+
pending->host->main().focusManager->deliverTestKey(pending->pressed);
|
|
644
|
+
}
|
|
645
|
+
delete pending;
|
|
646
|
+
return G_SOURCE_REMOVE;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
gboolean fireTestType(gpointer data) {
|
|
650
|
+
std::unique_ptr<PendingType> pending{static_cast<PendingType *>(data)};
|
|
651
|
+
g_message("BASALT_TEST_TYPE: typing \"%s\"", pending->text.c_str());
|
|
652
|
+
|
|
653
|
+
GtkWidget *focus = pending->host->main().window != nullptr
|
|
654
|
+
? gtk_window_get_focus(pending->host->main().window)
|
|
655
|
+
: nullptr;
|
|
656
|
+
if (focus == nullptr || !GTK_IS_TEXT(focus)) {
|
|
657
|
+
g_warning("BASALT_TEST_TYPE: no text field has focus");
|
|
658
|
+
return G_SOURCE_REMOVE;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
int position = -1;
|
|
662
|
+
gtk_editable_insert_text(GTK_EDITABLE(focus),
|
|
663
|
+
pending->text.c_str(),
|
|
664
|
+
static_cast<int>(pending->text.size()),
|
|
665
|
+
&position);
|
|
666
|
+
return G_SOURCE_REMOVE;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
gboolean commitSecondTree(gpointer data) {
|
|
670
|
+
auto *host = static_cast<Host *>(data);
|
|
671
|
+
g_message("--- committing tree 2 from JS ---");
|
|
672
|
+
callRenderFunction(host, 2);
|
|
673
|
+
return G_SOURCE_REMOVE;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// Ctrl+D: React Native's developer menu.
|
|
677
|
+
//
|
|
678
|
+
// The shortcut is React Native's own on a simulator, which is the closest
|
|
679
|
+
// thing a desktop has to a phone's shake. It does nothing in a release run --
|
|
680
|
+
// there is no dev server to reload from and no debugger to open -- so the key
|
|
681
|
+
// is left to travel on rather than swallowed.
|
|
682
|
+
gboolean onDevMenuKey(GtkEventControllerKey * /*controller*/,
|
|
683
|
+
guint keyval,
|
|
684
|
+
guint /*keycode*/,
|
|
685
|
+
GdkModifierType state,
|
|
686
|
+
gpointer data) {
|
|
687
|
+
auto *host = static_cast<Host *>(data);
|
|
688
|
+
if (!host->devMode || host->reactHost == nullptr) {
|
|
689
|
+
return GDK_EVENT_PROPAGATE;
|
|
690
|
+
}
|
|
691
|
+
if ((keyval != GDK_KEY_d && keyval != GDK_KEY_D) ||
|
|
692
|
+
(state & GDK_CONTROL_MASK) == 0) {
|
|
693
|
+
return GDK_EVENT_PROPAGATE;
|
|
694
|
+
}
|
|
695
|
+
basalt::showDevMenu(host->reactHost.get());
|
|
696
|
+
return GDK_EVENT_STOP;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// ---------------------------------------------------------------------------
|
|
700
|
+
// Window size -> surface constraints
|
|
701
|
+
// ---------------------------------------------------------------------------
|
|
702
|
+
|
|
703
|
+
// Per window: `data` is the HostWindow whose root resized, not the Host.
|
|
704
|
+
//
|
|
705
|
+
// Which is the whole of what multiple windows changes here. Constraining
|
|
706
|
+
// `kSurfaceId` from every window would lay the *first* window's tree out to the
|
|
707
|
+
// second window's size, and the symptom would be a first window whose content
|
|
708
|
+
// jumped whenever a second one was dragged.
|
|
709
|
+
void onRootResized(RnView * /*view*/, int width, int height, gpointer data) {
|
|
710
|
+
auto *made = static_cast<HostWindow *>(data);
|
|
711
|
+
Host *host = made->host;
|
|
712
|
+
if (width <= 0 || height <= 0 || !host->surfaceStarted) {
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
g_debug("surface %d constraints -> %dx%d", static_cast<int>(made->surfaceId), width, height);
|
|
716
|
+
host->reactHost->setSurfaceConstraints(
|
|
717
|
+
made->surfaceId, constraintsFor(width, height), layoutContextFor(made->scaleFactor));
|
|
718
|
+
|
|
719
|
+
// A <Modal> is sized from its own shadow-node state rather than from a style,
|
|
720
|
+
// and React Native's C++ platform answers "what size is the screen" with
|
|
721
|
+
// zero. Told here, where the window's size is already being handed to Fabric.
|
|
722
|
+
host->mountingManager->setSurfaceSize(static_cast<float>(width), static_cast<float>(height));
|
|
723
|
+
// The window moved or was resized, which `useWindow()` hands an app as live
|
|
724
|
+
// bounds. Here rather than in a watcher of its own: this is already the one
|
|
725
|
+
// place that learns it.
|
|
726
|
+
basalt::notifyWindowBoundsChanged();
|
|
727
|
+
|
|
728
|
+
// The inspector covers the main window, so it resizes with it -- and only
|
|
729
|
+
// with it: an error is about the app rather than about a window, and it is
|
|
730
|
+
// shown in the one the app started in.
|
|
731
|
+
if (host->logBoxRoot != nullptr && made->surfaceId == kSurfaceId) {
|
|
732
|
+
rn_view_set_frame(host->logBoxRoot, 0, 0, static_cast<float>(width), static_cast<float>(height));
|
|
733
|
+
host->reactHost->setSurfaceConstraints(
|
|
734
|
+
kLogBoxSurfaceId, constraintsFor(width, height), layoutContextFor(made->scaleFactor));
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// Starts or stops React Native's error inspector.
|
|
739
|
+
//
|
|
740
|
+
// It is `LogBoxInspectorContainer`, registered by AppRegistry under the name
|
|
741
|
+
// "LogBox" exactly as an app registers its own component -- so this is a second
|
|
742
|
+
// surface rather than an overlay this host draws, and everything in it is React
|
|
743
|
+
// Native's own JavaScript. See core/LogBoxSurface.h.
|
|
744
|
+
//
|
|
745
|
+
// Started and stopped rather than kept and hidden: a surface that exists is a
|
|
746
|
+
// React tree that renders and re-renders, and the inspector is showing for a
|
|
747
|
+
// vanishingly small part of a session.
|
|
748
|
+
void hideLogBoxSurface(Host *host) {
|
|
749
|
+
if (host->reactHost == nullptr || host->logBoxRoot == nullptr) {
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
host->reactHost->stopSurface(kLogBoxSurfaceId);
|
|
753
|
+
gtk_overlay_remove_overlay(GTK_OVERLAY(host->main().overlay), GTK_WIDGET(host->logBoxRoot));
|
|
754
|
+
host->mountingManager->destroySurfaceRoot(kLogBoxSurfaceId);
|
|
755
|
+
host->logBoxRoot = nullptr;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
void showLogBoxSurface(Host *host, const std::string &appKey) {
|
|
759
|
+
if (host->reactHost == nullptr || host->main().overlay == nullptr || host->logBoxRoot != nullptr) {
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
const int width = gtk_widget_get_width(GTK_WIDGET(host->main().root));
|
|
764
|
+
const int height = gtk_widget_get_height(GTK_WIDGET(host->main().root));
|
|
765
|
+
if (width <= 0 || height <= 0) {
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
host->logBoxRoot = host->mountingManager->createSurfaceRoot(kLogBoxSurfaceId);
|
|
770
|
+
rn_view_set_frame(host->logBoxRoot, 0, 0, static_cast<float>(width), static_cast<float>(height));
|
|
771
|
+
// Added after the window controls, so it is above them: an error box that a
|
|
772
|
+
// close button sits on top of is one the user can close by accident.
|
|
773
|
+
gtk_widget_set_halign(GTK_WIDGET(host->logBoxRoot), GTK_ALIGN_FILL);
|
|
774
|
+
gtk_widget_set_valign(GTK_WIDGET(host->logBoxRoot), GTK_ALIGN_FILL);
|
|
775
|
+
gtk_overlay_add_overlay(GTK_OVERLAY(host->main().overlay), GTK_WIDGET(host->logBoxRoot));
|
|
776
|
+
|
|
777
|
+
host->reactHost->startSurface(kLogBoxSurfaceId,
|
|
778
|
+
appKey,
|
|
779
|
+
folly::dynamic::object(),
|
|
780
|
+
constraintsFor(width, height),
|
|
781
|
+
layoutContextFor(host->main().scaleFactor));
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// The frame clock only exists once a widget is realised, so the choreographer
|
|
785
|
+
// cannot be attached at construction time.
|
|
786
|
+
void onRootMapped(GtkWidget *widget, gpointer data) {
|
|
787
|
+
auto *host = static_cast<Host *>(data);
|
|
788
|
+
if (host->choreographer != nullptr) {
|
|
789
|
+
host->choreographer->attachToWidget(widget);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// ---------------------------------------------------------------------------
|
|
794
|
+
// Startup
|
|
795
|
+
// ---------------------------------------------------------------------------
|
|
796
|
+
|
|
797
|
+
// Feature flags this platform states explicitly. Anything not listed here keeps
|
|
798
|
+
// React Native's own default, which is what the base class provides.
|
|
799
|
+
class LinuxFeatureFlags : public facebook::react::ReactNativeFeatureFlagsDefaults {
|
|
800
|
+
public:
|
|
801
|
+
bool enableBridgelessArchitecture() override {
|
|
802
|
+
return true;
|
|
803
|
+
}
|
|
804
|
+
};
|
|
805
|
+
|
|
806
|
+
// TurboModules this platform supplies itself. ReactCxxTurboModuleProvider
|
|
807
|
+
// consults these before its own, so naming a module it also provides replaces
|
|
808
|
+
// it. Today that is only PlatformConstants; this is also the seam an Expo port
|
|
809
|
+
// would use.
|
|
810
|
+
facebook::react::TurboModuleProviders makeTurboModuleProviders(
|
|
811
|
+
std::string scriptURL,
|
|
812
|
+
bool devMode,
|
|
813
|
+
std::weak_ptr<basalt::GtkImageLoader> imageLoader) {
|
|
814
|
+
facebook::react::TurboModuleProviders providers;
|
|
815
|
+
// Whatever the discovered capability packages supply, first: a package naming
|
|
816
|
+
// a module this host also provides is replacing it deliberately, and the same
|
|
817
|
+
// rule already applies to this list against ReactCxxPlatform's own. Answers
|
|
818
|
+
// nullptr for every name when no package supplies one, which is most builds.
|
|
819
|
+
// See core/PackageModules.h.
|
|
820
|
+
providers.emplace_back(basalt::packageTurboModule);
|
|
821
|
+
providers.emplace_back(
|
|
822
|
+
[scriptURL = std::move(scriptURL), devMode, imageLoader = std::move(imageLoader)](
|
|
823
|
+
const std::string &name,
|
|
824
|
+
const std::shared_ptr<facebook::react::CallInvoker> &jsInvoker)
|
|
825
|
+
-> std::shared_ptr<facebook::react::TurboModule> {
|
|
826
|
+
if (name == facebook::react::PlatformConstantsModule::kModuleName) {
|
|
827
|
+
return std::make_shared<basalt::DesktopPlatformConstantsModule>(jsInvoker);
|
|
828
|
+
}
|
|
829
|
+
// `Image.getSize` and `Image.prefetch`. Upstream builds this module
|
|
830
|
+
// with no loader at all and offers no way to supply one, so the answer
|
|
831
|
+
// is to build it here with the loader this host already has -- which
|
|
832
|
+
// is also the one holding the decoded images, so a size asked for
|
|
833
|
+
// something on screen costs nothing. See docs/backlog/upstream.md.
|
|
834
|
+
if (name == facebook::react::ImageLoaderModule::kModuleName) {
|
|
835
|
+
return std::make_shared<facebook::react::ImageLoaderModule>(jsInvoker, imageLoader);
|
|
836
|
+
}
|
|
837
|
+
if (name == basalt::DesktopAppModule::kModuleName) {
|
|
838
|
+
return std::make_shared<basalt::DesktopAppModule>(jsInvoker);
|
|
839
|
+
}
|
|
840
|
+
if (name == basalt::DesktopAppearanceModule::kModuleName) {
|
|
841
|
+
return std::make_shared<basalt::DesktopAppearanceModule>(jsInvoker);
|
|
842
|
+
}
|
|
843
|
+
#ifdef BASALT_HAS_SKIA
|
|
844
|
+
// Skia, when the build was pointed at an app that has it. Its
|
|
845
|
+
// `install` is what puts Skia's whole JavaScript API on the global
|
|
846
|
+
// object, and an app that imports Skia does not start without it;
|
|
847
|
+
// see gtk/GtkSkiaModule.h.
|
|
848
|
+
if (name == basalt::GtkSkiaModule::kModuleName) {
|
|
849
|
+
return std::make_shared<basalt::GtkSkiaModule>(jsInvoker);
|
|
850
|
+
}
|
|
851
|
+
#endif
|
|
852
|
+
if (name == basalt::DesktopBlobModule::kModuleName) {
|
|
853
|
+
return std::make_shared<basalt::DesktopBlobModule>(jsInvoker);
|
|
854
|
+
}
|
|
855
|
+
if (name == basalt::DesktopFileReaderModule::kModuleName) {
|
|
856
|
+
return std::make_shared<basalt::DesktopFileReaderModule>(jsInvoker);
|
|
857
|
+
}
|
|
858
|
+
if (name == basalt::DesktopClipboardModule::kModuleName) {
|
|
859
|
+
return std::make_shared<basalt::DesktopClipboardModule>(jsInvoker);
|
|
860
|
+
}
|
|
861
|
+
if (name == basalt::DesktopVibrationModule::kModuleName) {
|
|
862
|
+
return std::make_shared<basalt::DesktopVibrationModule>(jsInvoker);
|
|
863
|
+
}
|
|
864
|
+
if (name == basalt::DesktopAlertModule::kModuleName) {
|
|
865
|
+
return std::make_shared<basalt::DesktopAlertModule>(jsInvoker);
|
|
866
|
+
}
|
|
867
|
+
if (name == basalt::DesktopLinkingModule::kModuleName) {
|
|
868
|
+
return std::make_shared<basalt::DesktopLinkingModule>(jsInvoker);
|
|
869
|
+
}
|
|
870
|
+
if (name == basalt::DesktopShareModule::kModuleName) {
|
|
871
|
+
return std::make_shared<basalt::DesktopShareModule>(jsInvoker);
|
|
872
|
+
}
|
|
873
|
+
// The native file dialogs, which React Native has no API for and which
|
|
874
|
+
// are the first thing a desktop app reaches for. See core/DialogModule.h.
|
|
875
|
+
if (name == basalt::DesktopDialogModule::kModuleName) {
|
|
876
|
+
return std::make_shared<basalt::DesktopDialogModule>(jsInvoker);
|
|
877
|
+
}
|
|
878
|
+
// The application menu, which on macOS is also what makes Cmd-C reach
|
|
879
|
+
// a text field. See core/MenuModel.h.
|
|
880
|
+
if (name == basalt::DesktopMenuModule::kModuleName) {
|
|
881
|
+
return std::make_shared<basalt::DesktopMenuModule>(jsInvoker);
|
|
882
|
+
}
|
|
883
|
+
// More than one window, which is more than one surface. See
|
|
884
|
+
// core/WindowHost.h.
|
|
885
|
+
if (name == basalt::DesktopWindowsModule::kModuleName) {
|
|
886
|
+
return std::make_shared<basalt::DesktopWindowsModule>(jsInvoker);
|
|
887
|
+
}
|
|
888
|
+
if (name == basalt::DesktopI18nManagerModule::kModuleName) {
|
|
889
|
+
return std::make_shared<basalt::DesktopI18nManagerModule>(jsInvoker);
|
|
890
|
+
}
|
|
891
|
+
if (name == basalt::DesktopAccessibilityManagerModule::kModuleName) {
|
|
892
|
+
return std::make_shared<basalt::DesktopAccessibilityManagerModule>(jsInvoker);
|
|
893
|
+
}
|
|
894
|
+
if (name == basalt::DesktopAccessibilityInfoModule::kModuleName) {
|
|
895
|
+
return std::make_shared<basalt::DesktopAccessibilityInfoModule>(jsInvoker);
|
|
896
|
+
}
|
|
897
|
+
if (name == basalt::DesktopToastModule::kModuleName) {
|
|
898
|
+
return std::make_shared<basalt::DesktopToastModule>(jsInvoker);
|
|
899
|
+
}
|
|
900
|
+
// react-native-gesture-handler's module. Offered whether or not the app
|
|
901
|
+
// has the library: it costs a name comparison, and the alternative is a
|
|
902
|
+
// host that has to be rebuilt to run an app that uses gestures.
|
|
903
|
+
if (name == basalt::DesktopGestureHandlerModule::kModuleName) {
|
|
904
|
+
return std::make_shared<basalt::DesktopGestureHandlerModule>(jsInvoker);
|
|
905
|
+
}
|
|
906
|
+
#ifdef BASALT_HAS_WORKLETS
|
|
907
|
+
// react-native-worklets, when the build was pointed at one. Reanimated
|
|
908
|
+
// is built on it; see core/WorkletsModule.h.
|
|
909
|
+
if (name == basalt::DesktopWorkletsModule::kModuleName) {
|
|
910
|
+
return std::make_shared<basalt::DesktopWorkletsModule>(jsInvoker);
|
|
911
|
+
}
|
|
912
|
+
#endif
|
|
913
|
+
#ifdef BASALT_HAS_REANIMATED
|
|
914
|
+
if (name == basalt::DesktopReanimatedModule::kModuleName) {
|
|
915
|
+
return std::make_shared<basalt::DesktopReanimatedModule>(jsInvoker);
|
|
916
|
+
}
|
|
917
|
+
#endif
|
|
918
|
+
// Only outside dev mode. ReactCxxPlatform provides a real DevSettings
|
|
919
|
+
// when a dev server exists, and this provider is consulted first -- so
|
|
920
|
+
// offering ours unconditionally would shadow the one that works.
|
|
921
|
+
if (!devMode && name == basalt::DesktopDevSettingsModule::kModuleName) {
|
|
922
|
+
return std::make_shared<basalt::DesktopDevSettingsModule>(jsInvoker);
|
|
923
|
+
}
|
|
924
|
+
if (name == basalt::DesktopStatusBarModule::kModuleName) {
|
|
925
|
+
return std::make_shared<basalt::DesktopStatusBarModule>(jsInvoker);
|
|
926
|
+
}
|
|
927
|
+
// The title bar. Reaches the window through the one GtkTitleBar, which
|
|
928
|
+
// the host attached at startup -- a module cannot be handed a window it
|
|
929
|
+
// is created before.
|
|
930
|
+
if (name == basalt::GtkWindowModule::kModuleName) {
|
|
931
|
+
return std::make_shared<basalt::GtkWindowModule>(jsInvoker);
|
|
932
|
+
}
|
|
933
|
+
// Only outside dev mode, for the same reason DevSettings is above.
|
|
934
|
+
//
|
|
935
|
+
// This module's scriptURL is what HMRClient registers with Metro as its
|
|
936
|
+
// entry point, and Metro resolves that URL to a *graph*. The URL
|
|
937
|
+
// synthesised here is not the one DevServerHelper actually fetched --
|
|
938
|
+
// that one carries lazy, minify, runModule and app -- so it names a
|
|
939
|
+
// graph nobody built, Metro answers GraphNotFoundError, and Fast
|
|
940
|
+
// Refresh silently never starts. ReactCxxPlatform's own SourceCode
|
|
941
|
+
// module reports the URL the bundle really came from, which is the one
|
|
942
|
+
// whose graph exists.
|
|
943
|
+
if (!devMode && name == basalt::DesktopSourceCodeModule::kModuleName) {
|
|
944
|
+
return std::make_shared<basalt::DesktopSourceCodeModule>(jsInvoker, scriptURL);
|
|
945
|
+
}
|
|
946
|
+
return nullptr;
|
|
947
|
+
});
|
|
948
|
+
return providers;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// Runs against the JavaScript runtime before the bundle is evaluated, which is
|
|
952
|
+
// the only moment early enough for what goes in here.
|
|
953
|
+
void installBindings(facebook::jsi::Runtime &runtime) {
|
|
954
|
+
// Before Expo's, and before anything else that might look a module up: this is
|
|
955
|
+
// what makes TurboModuleRegistry's first question answerable on a bridgeless
|
|
956
|
+
// runtime. See core/TurboModuleProxy.h.
|
|
957
|
+
basalt::installTurboModuleProxy(runtime);
|
|
958
|
+
basalt::installExpoRuntime(runtime);
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
std::shared_ptr<const ContextContainer> makeContextContainer() {
|
|
962
|
+
auto contextContainer = std::make_shared<ContextContainer>();
|
|
963
|
+
|
|
964
|
+
// ReactHost throws without these two. ReactCxxPlatform's own implementations
|
|
965
|
+
// are real ones -- curl-backed http and a websocket client -- so a host that
|
|
966
|
+
// is not a test harness should use them rather than stubs. The DevTools
|
|
967
|
+
// variants default to the same pair when left unset.
|
|
968
|
+
contextContainer->insert(facebook::react::HttpClientFactoryKey,
|
|
969
|
+
facebook::react::getHttpClientFactory());
|
|
970
|
+
contextContainer->insert(facebook::react::WebSocketClientFactoryKey,
|
|
971
|
+
facebook::react::getWebSocketClientFactory());
|
|
972
|
+
|
|
973
|
+
// MessageQueueThreadFactoryKey is deliberately left unset: ReactHost then
|
|
974
|
+
// installs MessageQueueThreadImpl, a real threaded queue, which is what we
|
|
975
|
+
// want. Fantom overrides it only to make test execution deterministic.
|
|
976
|
+
return contextContainer;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
// Makes a window, a surface root for it, and the input that drives them.
|
|
980
|
+
//
|
|
981
|
+
// Called for the main window and for every one an app opens afterwards, which
|
|
982
|
+
// is the point: the second window is not a special case of the first, it is the
|
|
983
|
+
// same function with a different surface id. See core/WindowHost.h.
|
|
984
|
+
//
|
|
985
|
+
// The window is not shown and the surface is not started here. The main window
|
|
986
|
+
// waits for the bundle to load and an app's window waits for its component to
|
|
987
|
+
// be named, and both of those are the caller's business.
|
|
988
|
+
HostWindow *createHostWindow(Host *host,
|
|
989
|
+
GtkApplication *app,
|
|
990
|
+
facebook::react::SurfaceId surfaceId,
|
|
991
|
+
const char *title,
|
|
992
|
+
int width,
|
|
993
|
+
int height) {
|
|
994
|
+
// The first window through here is the app's own, and a few things belong to
|
|
995
|
+
// it alone rather than to every window: the title bar, the frame clock the
|
|
996
|
+
// animation choreographer runs on, and the error inspector.
|
|
997
|
+
const bool isMainWindow = host->windows.empty();
|
|
998
|
+
|
|
999
|
+
auto owned = std::make_unique<HostWindow>();
|
|
1000
|
+
HostWindow *made = owned.get();
|
|
1001
|
+
made->surfaceId = surfaceId;
|
|
1002
|
+
made->host = host;
|
|
1003
|
+
|
|
1004
|
+
made->window = GTK_WINDOW(gtk_application_window_new(app));
|
|
1005
|
+
gtk_window_set_title(made->window, title);
|
|
1006
|
+
gtk_window_set_default_size(made->window, width, height);
|
|
1007
|
+
made->scaleFactor = gtk_widget_get_scale_factor(GTK_WIDGET(made->window));
|
|
1008
|
+
|
|
1009
|
+
// Fabric emits no Create for a surface root -- the root shadow node is the
|
|
1010
|
+
// base of every diff, so it has to exist before the surface starts.
|
|
1011
|
+
made->root = host->mountingManager->createSurfaceRoot(surfaceId);
|
|
1012
|
+
|
|
1013
|
+
// The surface root, with room above it for the window controls the host
|
|
1014
|
+
// draws when the title bar is hidden. GTK takes the whole titlebar away with
|
|
1015
|
+
// the decorations -- buttons included -- where Windows and macOS keep
|
|
1016
|
+
// drawing theirs over the app's content, so on this desktop the host has to
|
|
1017
|
+
// put them back. An overlay is how: the root fills the window and the
|
|
1018
|
+
// controls sit over its top corner.
|
|
1019
|
+
made->overlay = gtk_overlay_new();
|
|
1020
|
+
gtk_overlay_set_child(GTK_OVERLAY(made->overlay), GTK_WIDGET(made->root));
|
|
1021
|
+
|
|
1022
|
+
GtkWidget *controls = gtk_window_controls_new(GTK_PACK_END);
|
|
1023
|
+
gtk_widget_set_halign(controls, GTK_ALIGN_END);
|
|
1024
|
+
gtk_widget_set_valign(controls, GTK_ALIGN_START);
|
|
1025
|
+
// Hidden until an app asks for the hidden style; see GtkTitleBar.h.
|
|
1026
|
+
gtk_widget_set_visible(controls, FALSE);
|
|
1027
|
+
gtk_overlay_add_overlay(GTK_OVERLAY(made->overlay), controls);
|
|
1028
|
+
|
|
1029
|
+
gtk_window_set_child(made->window, made->overlay);
|
|
1030
|
+
// The title bar is the main window's. It is a process-wide seam -- one title,
|
|
1031
|
+
// one style -- and making it per-window is its own piece of work; see
|
|
1032
|
+
// docs/BACKLOG.md.
|
|
1033
|
+
if (isMainWindow) {
|
|
1034
|
+
basalt::titleBar().attach(made->window, controls);
|
|
1035
|
+
|
|
1036
|
+
// A press inside a region the app marked with <TitleBar.DragRegion> hands
|
|
1037
|
+
// the window to the window manager's own move loop.
|
|
1038
|
+
//
|
|
1039
|
+
// This is the host's job rather than a method JavaScript calls, because
|
|
1040
|
+
// gdk_toplevel_begin_move needs the device, button and timestamp of the
|
|
1041
|
+
// press it is taking over, and by the time a JS handler could run those
|
|
1042
|
+
// are gone. See GtkTitleBar::startDrag, which says so at more length.
|
|
1043
|
+
//
|
|
1044
|
+
// Windows needs none of this: it answers HTCAPTION from WM_NCHITTEST and
|
|
1045
|
+
// the system does the rest. GTK has no equivalent hook, so the gesture is
|
|
1046
|
+
// the equivalent -- and it asks the same question of the same nativeIDs,
|
|
1047
|
+
// which is what keeps the two desktops agreeing about what drags.
|
|
1048
|
+
GtkGesture *dragRegions = gtk_gesture_click_new();
|
|
1049
|
+
gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(dragRegions), GDK_BUTTON_PRIMARY);
|
|
1050
|
+
// Capture, so this is asked before the app's own views handle the press.
|
|
1051
|
+
// In the bubble phase a <Pressable> inside a drag region would have taken
|
|
1052
|
+
// it already, and the window would never move.
|
|
1053
|
+
gtk_event_controller_set_propagation_phase(GTK_EVENT_CONTROLLER(dragRegions),
|
|
1054
|
+
GTK_PHASE_CAPTURE);
|
|
1055
|
+
g_signal_connect(
|
|
1056
|
+
dragRegions,
|
|
1057
|
+
"pressed",
|
|
1058
|
+
G_CALLBACK(+[](GtkGestureClick *gesture,
|
|
1059
|
+
int /*nPress*/,
|
|
1060
|
+
double x,
|
|
1061
|
+
double y,
|
|
1062
|
+
gpointer data) {
|
|
1063
|
+
auto *self = static_cast<HostWindow *>(data);
|
|
1064
|
+
if (self == nullptr || self->root == nullptr || self->window == nullptr) {
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
// Only the hidden style. With GTK's own decorations the titlebar is
|
|
1068
|
+
// a real widget above the app, dragging it already works, and an
|
|
1069
|
+
// app's marked region is just a view.
|
|
1070
|
+
if (basalt::titleBar().metrics().style != basalt::TitleBarStyle::Hidden) {
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1073
|
+
if (!basalt::isTitleBarDragRegionAt(self->root, x, y)) {
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
GtkEventController *const controller = GTK_EVENT_CONTROLLER(gesture);
|
|
1078
|
+
GdkDevice *const device = gtk_event_controller_get_current_event_device(controller);
|
|
1079
|
+
const guint32 timestamp = gtk_event_controller_get_current_event_time(controller);
|
|
1080
|
+
const int button =
|
|
1081
|
+
static_cast<int>(gtk_gesture_single_get_current_button(GTK_GESTURE_SINGLE(gesture)));
|
|
1082
|
+
|
|
1083
|
+
// The gesture reports the root's coordinates; GDK wants the
|
|
1084
|
+
// surface's. They differ by the root's offset within the window and
|
|
1085
|
+
// again by the surface transform, which is where a compositor puts
|
|
1086
|
+
// client-side shadows -- ignore either and the window jumps by that
|
|
1087
|
+
// much as the drag starts.
|
|
1088
|
+
// compute_point rather than translate_coordinates, which GTK 4.12
|
|
1089
|
+
// deprecated and this build treats as an error. Brace-initialised
|
|
1090
|
+
// because GRAPHENE_POINT_INIT is a C99 compound literal that C++
|
|
1091
|
+
// rejects -- the same trap GtkMountingManager records for
|
|
1092
|
+
// GRAPHENE_SIZE_INIT -- and the result is checked because the
|
|
1093
|
+
// function is warn-unused-result: false means the two widgets share
|
|
1094
|
+
// no ancestry, which a window mid-teardown really can produce.
|
|
1095
|
+
// Assigned field by field rather than brace-initialised, because this
|
|
1096
|
+
// whole lambda is one argument to the G_CALLBACK macro below and the
|
|
1097
|
+
// preprocessor counts commas at paren depth without understanding
|
|
1098
|
+
// braces: `{a, b}` reads as two macro arguments and the expansion
|
|
1099
|
+
// fails with "too many arguments provided to function-like macro".
|
|
1100
|
+
// Empty braces are safe, which is why the same type in
|
|
1101
|
+
// GtkTouchDispatcher.cpp raises nothing.
|
|
1102
|
+
graphene_point_t inRoot{};
|
|
1103
|
+
inRoot.x = static_cast<float>(x);
|
|
1104
|
+
inRoot.y = static_cast<float>(y);
|
|
1105
|
+
graphene_point_t inWindow{};
|
|
1106
|
+
if (!gtk_widget_compute_point(
|
|
1107
|
+
GTK_WIDGET(self->root), GTK_WIDGET(self->window), &inRoot, &inWindow)) {
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
double originX = 0;
|
|
1111
|
+
double originY = 0;
|
|
1112
|
+
gtk_native_get_surface_transform(GTK_NATIVE(self->window), &originX, &originY);
|
|
1113
|
+
|
|
1114
|
+
basalt::titleBar().beginMoveDrag(device,
|
|
1115
|
+
button,
|
|
1116
|
+
inWindow.x + originX,
|
|
1117
|
+
inWindow.y + originY,
|
|
1118
|
+
timestamp);
|
|
1119
|
+
|
|
1120
|
+
// Claimed: the move loop now owns the pointer, and leaving the
|
|
1121
|
+
// sequence unclaimed would also deliver this press to the view under
|
|
1122
|
+
// it once the drag ended.
|
|
1123
|
+
gtk_gesture_set_state(GTK_GESTURE(gesture), GTK_EVENT_SEQUENCE_CLAIMED);
|
|
1124
|
+
}),
|
|
1125
|
+
made);
|
|
1126
|
+
gtk_widget_add_controller(GTK_WIDGET(made->root), GTK_EVENT_CONTROLLER(dragRegions));
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
rn_view_set_resize_callback(made->root, onRootResized, made);
|
|
1130
|
+
|
|
1131
|
+
// Maximised and full screen, which change the bounds and not the size of the
|
|
1132
|
+
// surface -- so the resize callback above never runs for them. GTK has no
|
|
1133
|
+
// move notification and no position to report; see GtkWindowControl.cpp.
|
|
1134
|
+
const auto onWindowState = +[](GObject * /*window*/, GParamSpec * /*spec*/, gpointer /*data*/) {
|
|
1135
|
+
basalt::notifyWindowBoundsChanged();
|
|
1136
|
+
};
|
|
1137
|
+
// Somebody is trying to close this window: its own close button, the window
|
|
1138
|
+
// manager, or Alt+F4. Every window, including the app's own -- an app that
|
|
1139
|
+
// wants to ask "are you sure" wants to ask hardest about the one it is
|
|
1140
|
+
// running in.
|
|
1141
|
+
g_signal_connect(made->window,
|
|
1142
|
+
"close-request",
|
|
1143
|
+
G_CALLBACK(+[](GtkWindow *window, gpointer data) -> gboolean {
|
|
1144
|
+
(void)window;
|
|
1145
|
+
auto *closing = static_cast<HostWindow *>(data);
|
|
1146
|
+
|
|
1147
|
+
// Asked to be asked first. Refuse, and tell the app; what
|
|
1148
|
+
// happens next is its decision, and if that is "go ahead"
|
|
1149
|
+
// it calls close() itself. See core/WindowHost.h for why
|
|
1150
|
+
// the answer has to be available before the question.
|
|
1151
|
+
if (basalt::hostWindowCloseIntercepted(closing->surfaceId)) {
|
|
1152
|
+
basalt::hostWindowCloseRequested(closing->surfaceId);
|
|
1153
|
+
return TRUE;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
// The main window closing ends the process, which
|
|
1157
|
+
// GtkApplication already arranges -- so nothing here has
|
|
1158
|
+
// to, and unhandled is how it gets to.
|
|
1159
|
+
if (closing->surfaceId == kSurfaceId) {
|
|
1160
|
+
return FALSE;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
// Closed by the person rather than by the app. Without
|
|
1164
|
+
// this the host keeps a record whose GtkWindow is gone --
|
|
1165
|
+
// a dangling pointer rather than a stale flag -- and the
|
|
1166
|
+
// `<Window>` that opened it goes on believing it is open.
|
|
1167
|
+
//
|
|
1168
|
+
// Tell JavaScript, then take the window down the same way
|
|
1169
|
+
// an app closing it would, so there is one teardown path
|
|
1170
|
+
// rather than two.
|
|
1171
|
+
basalt::hostWindowClosed(closing->surfaceId);
|
|
1172
|
+
basalt::closeHostWindow(closing->surfaceId);
|
|
1173
|
+
// Handled: closeHostWindow destroys the window once the
|
|
1174
|
+
// surface has stopped, and letting GTK destroy it now
|
|
1175
|
+
// would be the race that teardown exists to avoid.
|
|
1176
|
+
return TRUE;
|
|
1177
|
+
}),
|
|
1178
|
+
made);
|
|
1179
|
+
|
|
1180
|
+
g_signal_connect(made->window, "notify::maximized", G_CALLBACK(onWindowState), nullptr);
|
|
1181
|
+
g_signal_connect(made->window, "notify::fullscreened", G_CALLBACK(onWindowState), nullptr);
|
|
1182
|
+
// The animation choreographer runs on a frame clock, and a widget only has
|
|
1183
|
+
// one once it is realised -- hence the "map". The *main* window's, and only
|
|
1184
|
+
// its: attaching to every window's meant the last one opened stole the clock
|
|
1185
|
+
// from the first, so animations followed whichever window appeared most
|
|
1186
|
+
// recently and stopped when it closed. It also left a handler connected to a
|
|
1187
|
+
// frame clock that was about to be finalised, which GLib complained about at
|
|
1188
|
+
// shutdown and was how this was noticed at all.
|
|
1189
|
+
if (isMainWindow) {
|
|
1190
|
+
g_signal_connect(made->root, "map", G_CALLBACK(onRootMapped), host);
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
// Input, per window. Attached to this window's root, which is where its hit
|
|
1194
|
+
// testing starts -- a second window with the first one's dispatcher would
|
|
1195
|
+
// deliver every press to the wrong tree.
|
|
1196
|
+
made->touchDispatcher =
|
|
1197
|
+
std::make_unique<basalt::GtkTouchDispatcher>(host->mountingManager.get(), made->root);
|
|
1198
|
+
// The keyboard half: Tab reaching a <Pressable>, and Enter activating it.
|
|
1199
|
+
made->focusManager =
|
|
1200
|
+
std::make_unique<basalt::GtkFocusManager>(host->mountingManager.get(), made->root);
|
|
1201
|
+
|
|
1202
|
+
// Ctrl+D, which is React Native's developer menu. Its own controller rather
|
|
1203
|
+
// than another case in the focus manager's: this has nothing to do with
|
|
1204
|
+
// focus, and a shortcut that works wherever the caret is has to be on the
|
|
1205
|
+
// window in the capture phase anyway -- otherwise a <TextInput> with the
|
|
1206
|
+
// keyboard would eat it.
|
|
1207
|
+
GtkEventController *devKeys = gtk_event_controller_key_new();
|
|
1208
|
+
gtk_event_controller_set_propagation_phase(devKeys, GTK_PHASE_CAPTURE);
|
|
1209
|
+
g_signal_connect(devKeys, "key-pressed", G_CALLBACK(onDevMenuKey), host);
|
|
1210
|
+
gtk_widget_add_controller(GTK_WIDGET(made->window), devKeys);
|
|
1211
|
+
|
|
1212
|
+
host->windows.push_back(std::move(owned));
|
|
1213
|
+
return made;
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
// The host's half of core/WindowHost.h.
|
|
1217
|
+
//
|
|
1218
|
+
// A single global, because a process has one host and one GtkApplication and
|
|
1219
|
+
// the seam takes neither. The alternative -- threading a Host through a
|
|
1220
|
+
// TurboModule -- would mean the module knowing what a Host is.
|
|
1221
|
+
Host *gWindowHost = nullptr;
|
|
1222
|
+
|
|
1223
|
+
void onActivate(GtkApplication *app, gpointer data) {
|
|
1224
|
+
auto *host = static_cast<Host *>(data);
|
|
1225
|
+
|
|
1226
|
+
// Constructed here, on the GTK main thread: GtkMountingManager records this
|
|
1227
|
+
// thread and asserts that every widget mutation lands back on it.
|
|
1228
|
+
host->mountingManager = std::make_shared<basalt::GtkMountingManager>();
|
|
1229
|
+
|
|
1230
|
+
// The application's own window, which is windows.front() from here on and is
|
|
1231
|
+
// what everything not yet per-window means by "the window".
|
|
1232
|
+
host->application = app;
|
|
1233
|
+
gWindowHost = host;
|
|
1234
|
+
createHostWindow(host, app, kSurfaceId, "basalt-core", kInitialWidth, kInitialHeight);
|
|
1235
|
+
|
|
1236
|
+
// Prime the bounds cache, which `getBounds()` answers from: without this an
|
|
1237
|
+
// app's first render sees a window of no size, and only a later resize
|
|
1238
|
+
// corrects it. See core/WindowBoundsCache.cpp.
|
|
1239
|
+
basalt::notifyWindowBoundsChanged();
|
|
1240
|
+
|
|
1241
|
+
// And the display list, for the same reason and off the same seam: an app
|
|
1242
|
+
// asking which screens exist before anything has been plugged or unplugged
|
|
1243
|
+
// should not be told there are none.
|
|
1244
|
+
basalt::notifyDisplaysChanged();
|
|
1245
|
+
|
|
1246
|
+
// Accepting what the desktop drags onto this window. On the surface root
|
|
1247
|
+
// rather than per view: GTK delivers a drop to the controller under the
|
|
1248
|
+
// pointer, and which *app* view that is is core/DragAndDrop.h's question.
|
|
1249
|
+
basalt::attachDropTarget(host->main().root);
|
|
1250
|
+
|
|
1251
|
+
// And dragging out of it. A separate controller on the same widget: GTK
|
|
1252
|
+
// keeps the two directions apart and so does the marker -- a view may be
|
|
1253
|
+
// both, and most are neither.
|
|
1254
|
+
basalt::attachDragSource(host->main().root);
|
|
1255
|
+
|
|
1256
|
+
host->runLoopObserverManager = std::make_shared<RunLoopObserverManager>();
|
|
1257
|
+
host->choreographer = std::make_shared<basalt::GtkAnimationChoreographer>();
|
|
1258
|
+
|
|
1259
|
+
// Light or dark, and any change to it. Before ReactHost, so the module can
|
|
1260
|
+
// answer the first `Appearance.getColorScheme()` a bundle makes, which for an
|
|
1261
|
+
// Expo app is during its first import.
|
|
1262
|
+
basalt::startObservingColorScheme();
|
|
1263
|
+
|
|
1264
|
+
// Before ReactHost, so the beat is being induced from the first event on.
|
|
1265
|
+
host->runLoopObserver = basalt::installRunLoopObserver(host->runLoopObserverManager);
|
|
1266
|
+
|
|
1267
|
+
// Say what this host needs rather than inheriting a default that moves.
|
|
1268
|
+
//
|
|
1269
|
+
// HermesInstance gives the runtime a microtask queue only when
|
|
1270
|
+
// enableBridgelessArchitecture() is true, and React's scheduler enqueues a
|
|
1271
|
+
// microtask on its first render. That flag defaults to true on React Native
|
|
1272
|
+
// `main` and false on 0.87.1, so the same code that works against main throws
|
|
1273
|
+
// "Could not enqueue microtask because they are disabled in this runtime" on
|
|
1274
|
+
// a release, before anything appears on screen. This host is bridgeless --
|
|
1275
|
+
// there is no bridge here to be the alternative -- so it should have been
|
|
1276
|
+
// asserting that all along.
|
|
1277
|
+
//
|
|
1278
|
+
// iOS and Android override feature flags at startup too; relying on the
|
|
1279
|
+
// default was the anomaly.
|
|
1280
|
+
facebook::react::ReactNativeFeatureFlags::override(
|
|
1281
|
+
std::make_unique<LinuxFeatureFlags>());
|
|
1282
|
+
|
|
1283
|
+
ReactInstanceConfig config;
|
|
1284
|
+
// The appId is how this host tells Metro which platform it is. ReactCxxPlatform's
|
|
1285
|
+
// DevServerHelper builds its bundle URL from `constexpr DEFAULT_PLATFORM =
|
|
1286
|
+
// "android"` with no hook and no setting, and `app=` -- which comes from
|
|
1287
|
+
// here, and which Metro itself ignores -- is the only thing in that URL a
|
|
1288
|
+
// host controls. The Metro plugin reads it back and corrects the platform.
|
|
1289
|
+
// See APP_ID_PREFIX in packages/basalt-core/metro-config.js.
|
|
1290
|
+
config.appId = "basalt-linux";
|
|
1291
|
+
config.deviceName = "linux";
|
|
1292
|
+
|
|
1293
|
+
// Dev mode changes three things at once, which is worth being explicit about:
|
|
1294
|
+
// loadScript tries Metro before the on-disk bundle; DevServerHelper exists,
|
|
1295
|
+
// which is the only condition under which ReactCxxTurboModuleProvider serves
|
|
1296
|
+
// the DevSettings module a __DEV__ bundle requires; and ReactHost opens a
|
|
1297
|
+
// packager connection whose reload message reloads the instance.
|
|
1298
|
+
config.enableDevMode = g_getenv("BASALT_DEV") != nullptr;
|
|
1299
|
+
host->devMode = config.enableDevMode;
|
|
1300
|
+
config.enableInspector = config.enableDevMode;
|
|
1301
|
+
if (const char *devHost = g_getenv("BASALT_DEV_HOST")) {
|
|
1302
|
+
config.devServerHost = devHost;
|
|
1303
|
+
}
|
|
1304
|
+
if (const char *devPort = g_getenv("BASALT_DEV_PORT")) {
|
|
1305
|
+
config.devServerPort = static_cast<uint32_t>(g_ascii_strtoull(devPort, nullptr, 10));
|
|
1306
|
+
}
|
|
1307
|
+
if (config.enableDevMode) {
|
|
1308
|
+
g_message("dev mode: Metro at %s:%u, entry '%s'",
|
|
1309
|
+
config.devServerHost.c_str(),
|
|
1310
|
+
config.devServerPort,
|
|
1311
|
+
host->sourcePath.c_str());
|
|
1312
|
+
// So the http client can tell the bundle request apart from an app's own
|
|
1313
|
+
// fetch, and refuse to feed a Metro error page to the JS engine. See
|
|
1314
|
+
// core/DevBundle.h.
|
|
1315
|
+
basalt::setDevServerOrigin(config.devServerHost, config.devServerPort);
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
try {
|
|
1319
|
+
host->reactHost = std::make_unique<ReactHost>(config,
|
|
1320
|
+
host->mountingManager,
|
|
1321
|
+
host->runLoopObserverManager,
|
|
1322
|
+
makeContextContainer(),
|
|
1323
|
+
facebook::react::getDefaultOnJsErrorFunc(),
|
|
1324
|
+
logToGlib,
|
|
1325
|
+
nullptr,
|
|
1326
|
+
makeTurboModuleProviders(
|
|
1327
|
+
basalt::scriptURLFor(
|
|
1328
|
+
host->bundlePath,
|
|
1329
|
+
config.enableDevMode,
|
|
1330
|
+
config.devServerHost,
|
|
1331
|
+
config.devServerPort,
|
|
1332
|
+
host->sourcePath.empty() ? "index"
|
|
1333
|
+
: host->sourcePath,
|
|
1334
|
+
"linux"),
|
|
1335
|
+
config.enableDevMode,
|
|
1336
|
+
host->mountingManager
|
|
1337
|
+
->imageLoader()),
|
|
1338
|
+
// React Native's error
|
|
1339
|
+
// inspector. ReactCxxPlatform
|
|
1340
|
+
// implements the LogBox
|
|
1341
|
+
// TurboModule itself and only
|
|
1342
|
+
// provides it when a host
|
|
1343
|
+
// hands over one of these;
|
|
1344
|
+
// passing null, as this did,
|
|
1345
|
+
// left NativeLogBox.show() a
|
|
1346
|
+
// call into nothing. See
|
|
1347
|
+
// core/LogBoxSurface.h.
|
|
1348
|
+
std::make_shared<basalt::LogBoxSurfaceDelegate>(
|
|
1349
|
+
[host](const std::string &appKey) {
|
|
1350
|
+
showLogBoxSurface(host, appKey);
|
|
1351
|
+
},
|
|
1352
|
+
[host]() { hideLogBoxSurface(host); }),
|
|
1353
|
+
// `useNativeDriver: true`.
|
|
1354
|
+
// ReactCommon has a C++
|
|
1355
|
+
// implementation of the whole
|
|
1356
|
+
// animated graph and
|
|
1357
|
+
// ReactCxxPlatform provides
|
|
1358
|
+
// the module for it, but only
|
|
1359
|
+
// when a host asks by handing
|
|
1360
|
+
// over a provider. Without it
|
|
1361
|
+
// every native-driven
|
|
1362
|
+
// Animated call throws
|
|
1363
|
+
// "Native animated module is
|
|
1364
|
+
// not available".
|
|
1365
|
+
std::make_shared<
|
|
1366
|
+
facebook::react::
|
|
1367
|
+
NativeAnimatedNodesManagerProvider>(),
|
|
1368
|
+
installBindings,
|
|
1369
|
+
host->choreographer);
|
|
1370
|
+
} catch (const std::exception &error) {
|
|
1371
|
+
g_error("could not construct ReactHost: %s", error.what());
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
// How anything in the core reaches the scheduler, which only the host has.
|
|
1375
|
+
// Reanimated is the one caller; see core/UIManagerAccess.h.
|
|
1376
|
+
basalt::setEventListenerInstaller(
|
|
1377
|
+
[host](std::shared_ptr<const facebook::react::EventListener> listener) {
|
|
1378
|
+
if (host->reactHost == nullptr) {
|
|
1379
|
+
return;
|
|
1380
|
+
}
|
|
1381
|
+
host->reactHost->runOnScheduler(
|
|
1382
|
+
[listener = std::move(listener)](facebook::react::Scheduler &scheduler) {
|
|
1383
|
+
scheduler.addEventListener(listener);
|
|
1384
|
+
});
|
|
1385
|
+
});
|
|
1386
|
+
|
|
1387
|
+
// And how anything in the core reaches the JavaScript thread. An Expo
|
|
1388
|
+
// module is given no call invoker, so this is the only way one can emit an
|
|
1389
|
+
// event or settle a promise later; see core/JsRuntimeAccess.h.
|
|
1390
|
+
basalt::setRuntimeRunner(
|
|
1391
|
+
[host](std::function<void(facebook::jsi::Runtime &)> work) {
|
|
1392
|
+
if (host->reactHost == nullptr) {
|
|
1393
|
+
return;
|
|
1394
|
+
}
|
|
1395
|
+
host->reactHost->runOnRuntimeScheduler(std::move(work));
|
|
1396
|
+
});
|
|
1397
|
+
|
|
1398
|
+
// `loadScript` falls back to the on-disk bundle whenever the Metro fetch
|
|
1399
|
+
// fails, which is right when nothing is listening and wrong when Metro
|
|
1400
|
+
// answered with an error: running the last bundle that built, while the
|
|
1401
|
+
// developer looks at source that is not what is executing, hides the very
|
|
1402
|
+
// thing they need to see. So the fallback is allowed only for the first case,
|
|
1403
|
+
// and the second stops here with Metro's own message.
|
|
1404
|
+
const bool loaded = host->reactHost->loadScript(host->bundlePath, host->sourcePath);
|
|
1405
|
+
if (const auto devError = basalt::devBundleError()) {
|
|
1406
|
+
g_warning("Metro could not build the bundle (HTTP %ld):\n%s",
|
|
1407
|
+
devError->status,
|
|
1408
|
+
devError->message.c_str());
|
|
1409
|
+
// Non-zero, so a script that starts this host can tell a build failure from
|
|
1410
|
+
// a run that ended. The AppKit host returns 1 from main for the same reason.
|
|
1411
|
+
host->exitStatus = 1;
|
|
1412
|
+
g_application_quit(G_APPLICATION(gtk_window_get_application(host->main().window)));
|
|
1413
|
+
return;
|
|
1414
|
+
}
|
|
1415
|
+
if (!loaded) {
|
|
1416
|
+
g_warning("could not load script: %s", host->bundlePath.c_str());
|
|
1417
|
+
gtk_window_present(host->main().window);
|
|
1418
|
+
return;
|
|
1419
|
+
}
|
|
1420
|
+
// Where it came from, not where it might have. See core/DevBundle.h.
|
|
1421
|
+
if (const auto from = basalt::devBundleFetched()) {
|
|
1422
|
+
g_message("loaded script from the packager: %s", from->c_str());
|
|
1423
|
+
} else {
|
|
1424
|
+
g_message("loaded script: %s", host->bundlePath.c_str());
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
// The module name decides who drives the surface.
|
|
1428
|
+
//
|
|
1429
|
+
// Non-empty: SurfaceHandler::start calls AppRegistry.runApplication, React
|
|
1430
|
+
// mounts the registered component, and everything after this point is
|
|
1431
|
+
// ordinary React Native.
|
|
1432
|
+
//
|
|
1433
|
+
// Empty: the surface is registered without JS being called at all, leaving it
|
|
1434
|
+
// for a script to commit into through nativeFabricUIManager by hand. That is
|
|
1435
|
+
// how this host ran before there was a Metro bundle, and js/demo.js still
|
|
1436
|
+
// exercises it.
|
|
1437
|
+
host->reactHost->startSurface(kSurfaceId,
|
|
1438
|
+
host->moduleName,
|
|
1439
|
+
folly::dynamic::object(),
|
|
1440
|
+
constraintsFor(kInitialWidth, kInitialHeight),
|
|
1441
|
+
layoutContextFor(host->main().scaleFactor));
|
|
1442
|
+
host->surfaceStarted = true;
|
|
1443
|
+
g_message("started surface %d%s%s",
|
|
1444
|
+
static_cast<int>(kSurfaceId),
|
|
1445
|
+
host->moduleName.empty() ? " (no module; raw Fabric script)" : " for module ",
|
|
1446
|
+
host->moduleName.c_str());
|
|
1447
|
+
|
|
1448
|
+
gtk_window_present(host->main().window);
|
|
1449
|
+
|
|
1450
|
+
if (host->moduleName.empty()) {
|
|
1451
|
+
g_message("--- committing tree 1 from JS ---");
|
|
1452
|
+
callRenderFunction(host, 1);
|
|
1453
|
+
g_timeout_add(2000, commitSecondTree, host);
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
guint scriptedDelayMs = 1500;
|
|
1457
|
+
if (const char *taps = g_getenv("BASALT_TEST_TAP")) {
|
|
1458
|
+
scriptedDelayMs = scheduleTestTaps(host, taps, scriptedDelayMs, basalt::PointerButton::Primary);
|
|
1459
|
+
}
|
|
1460
|
+
// BASALT_TEST_SECONDARY_TAP: the other button, which presses nothing and
|
|
1461
|
+
// arrives as a pointer event. See core/PointerButtons.h.
|
|
1462
|
+
if (const char *taps = g_getenv("BASALT_TEST_SECONDARY_TAP")) {
|
|
1463
|
+
scriptedDelayMs =
|
|
1464
|
+
scheduleTestTaps(host, taps, scriptedDelayMs, basalt::PointerButton::Secondary);
|
|
1465
|
+
}
|
|
1466
|
+
if (const char *hovers = g_getenv("BASALT_TEST_HOVER")) {
|
|
1467
|
+
scriptedDelayMs = scheduleTestHovers(host, hovers, scriptedDelayMs);
|
|
1468
|
+
}
|
|
1469
|
+
if (const char *scrolls = g_getenv("BASALT_TEST_SCROLL")) {
|
|
1470
|
+
scriptedDelayMs = scheduleTestScrolls(host, scrolls, scriptedDelayMs);
|
|
1471
|
+
}
|
|
1472
|
+
// BASALT_TEST_CLOSE_WINDOW: the surface id of a window to close the way a
|
|
1473
|
+
// person would -- its own close button, not the app asking. The two take
|
|
1474
|
+
// different paths through the host and only one of them can leave a record
|
|
1475
|
+
// whose window is gone, which is why it is worth being able to drive.
|
|
1476
|
+
if (const char *closing = g_getenv("BASALT_TEST_CLOSE_WINDOW")) {
|
|
1477
|
+
const auto surfaceId = static_cast<facebook::react::SurfaceId>(g_ascii_strtoll(closing, nullptr, 10));
|
|
1478
|
+
struct PendingClose {
|
|
1479
|
+
Host *host;
|
|
1480
|
+
facebook::react::SurfaceId surfaceId;
|
|
1481
|
+
};
|
|
1482
|
+
g_timeout_add(
|
|
1483
|
+
scriptedDelayMs,
|
|
1484
|
+
+[](gpointer data) -> gboolean {
|
|
1485
|
+
std::unique_ptr<PendingClose> pending{static_cast<PendingClose *>(data)};
|
|
1486
|
+
g_message("BASALT_TEST_CLOSE_WINDOW: closing window %d",
|
|
1487
|
+
static_cast<int>(pending->surfaceId));
|
|
1488
|
+
HostWindow *target = pending->host->windowFor(pending->surfaceId);
|
|
1489
|
+
if (target != nullptr) {
|
|
1490
|
+
// `close`, not `destroy`: this is the request a close button makes,
|
|
1491
|
+
// so the close-request handler runs exactly as it would for a
|
|
1492
|
+
// person.
|
|
1493
|
+
gtk_window_close(target->window);
|
|
1494
|
+
}
|
|
1495
|
+
return G_SOURCE_REMOVE;
|
|
1496
|
+
},
|
|
1497
|
+
new PendingClose{host, surfaceId});
|
|
1498
|
+
scriptedDelayMs += 1000;
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
// BASALT_TEST_DROP: "x,y:path" -- a file dropped at that point, reported the
|
|
1502
|
+
// way the toolkit would report it.
|
|
1503
|
+
//
|
|
1504
|
+
// Entered below GTK rather than through it, and the reason is the same one
|
|
1505
|
+
// BASALT_TEST_TAP has: a real drag needs a source outside this process, and
|
|
1506
|
+
// there is no way to conjure one from inside a test. What this does exercise
|
|
1507
|
+
// is everything this platform owns -- the hit test, the ancestor walk, the
|
|
1508
|
+
// accept check, the event, and React's half -- which is all of the code
|
|
1509
|
+
// that could be wrong about *which view* is told and *what* it is told.
|
|
1510
|
+
// What it does not exercise is GtkDropTarget itself.
|
|
1511
|
+
if (const char *drop = g_getenv("BASALT_TEST_DROP")) {
|
|
1512
|
+
struct PendingDrop {
|
|
1513
|
+
Host *host;
|
|
1514
|
+
std::string spec;
|
|
1515
|
+
};
|
|
1516
|
+
g_timeout_add(
|
|
1517
|
+
scriptedDelayMs,
|
|
1518
|
+
+[](gpointer data) -> gboolean {
|
|
1519
|
+
std::unique_ptr<PendingDrop> pending{static_cast<PendingDrop *>(data)};
|
|
1520
|
+
const std::string &spec = pending->spec;
|
|
1521
|
+
const size_t comma = spec.find(',');
|
|
1522
|
+
const size_t colon = spec.find(':', comma == std::string::npos ? 0 : comma);
|
|
1523
|
+
if (comma == std::string::npos || colon == std::string::npos) {
|
|
1524
|
+
g_message("BASALT_TEST_DROP: expected x,y:path");
|
|
1525
|
+
return G_SOURCE_REMOVE;
|
|
1526
|
+
}
|
|
1527
|
+
const double x = g_ascii_strtod(spec.substr(0, comma).c_str(), nullptr);
|
|
1528
|
+
const double y = g_ascii_strtod(spec.substr(comma + 1, colon - comma - 1).c_str(), nullptr);
|
|
1529
|
+
const std::string path = spec.substr(colon + 1);
|
|
1530
|
+
|
|
1531
|
+
basalt::DragPayload payload;
|
|
1532
|
+
payload.files.push_back(path);
|
|
1533
|
+
|
|
1534
|
+
const facebook::react::Tag found =
|
|
1535
|
+
basalt::dropTargetAt(pending->host->main().root, x, y, basalt::DropAcceptsFiles);
|
|
1536
|
+
g_message("BASALT_TEST_DROP: %s at %g,%g onto tag %d",
|
|
1537
|
+
path.c_str(), x, y, static_cast<int>(found));
|
|
1538
|
+
if (found == 0) {
|
|
1539
|
+
return G_SOURCE_REMOVE;
|
|
1540
|
+
}
|
|
1541
|
+
basalt::DropEvent event;
|
|
1542
|
+
event.tag = found;
|
|
1543
|
+
event.phase = basalt::DropPhase::Drop;
|
|
1544
|
+
event.x = x;
|
|
1545
|
+
event.y = y;
|
|
1546
|
+
event.payload = payload;
|
|
1547
|
+
basalt::reportDrop(event);
|
|
1548
|
+
return G_SOURCE_REMOVE;
|
|
1549
|
+
},
|
|
1550
|
+
new PendingDrop{host, drop});
|
|
1551
|
+
scriptedDelayMs += 1000;
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
// BASALT_TEST_QUIT: the session ending, which is what quitting means on this
|
|
1555
|
+
// desktop -- there is no Cmd-Q for a GTK app to intercept. Emitting the
|
|
1556
|
+
// signal directly rather than arranging a real logout, for the obvious
|
|
1557
|
+
// reason; what it exercises is everything downstream of the signal, which is
|
|
1558
|
+
// all of this platform's half.
|
|
1559
|
+
//
|
|
1560
|
+
// A count rather than a flag, for the reason the AppKit host gives: one ask
|
|
1561
|
+
// cannot show both halves.
|
|
1562
|
+
if (const char *asks = g_getenv("BASALT_TEST_QUIT")) {
|
|
1563
|
+
const long times = std::max(1L, static_cast<long>(g_ascii_strtoll(asks, nullptr, 10)));
|
|
1564
|
+
for (long ask = 0; ask < times; ask++) {
|
|
1565
|
+
g_timeout_add(
|
|
1566
|
+
scriptedDelayMs,
|
|
1567
|
+
+[](gpointer data) -> gboolean {
|
|
1568
|
+
auto *self = static_cast<Host *>(data);
|
|
1569
|
+
g_message("BASALT_TEST_QUIT: ending the session");
|
|
1570
|
+
if (self != nullptr && self->application != nullptr) {
|
|
1571
|
+
g_signal_emit_by_name(self->application, "query-end");
|
|
1572
|
+
}
|
|
1573
|
+
return G_SOURCE_REMOVE;
|
|
1574
|
+
},
|
|
1575
|
+
host);
|
|
1576
|
+
scriptedDelayMs += 1000;
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
// BASALT_TEST_KEY: a declared shortcut, pressed. Enters after the keyval
|
|
1580
|
+
// translation; see core/TestKeys.h for what that does and does not prove.
|
|
1581
|
+
for (const basalt::KeyCombination &pressed : basalt::scriptedKeyPresses()) {
|
|
1582
|
+
g_timeout_add(scriptedDelayMs, fireTestKey, new PendingKey{host, pressed});
|
|
1583
|
+
scriptedDelayMs += 400;
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
if (const char *focus = g_getenv("BASALT_TEST_FOCUS")) {
|
|
1587
|
+
scriptedDelayMs = scheduleTestFocus(host, focus, scriptedDelayMs);
|
|
1588
|
+
}
|
|
1589
|
+
if (const char *drag = g_getenv("BASALT_TEST_DRAG")) {
|
|
1590
|
+
scheduleTestDrag(host, drag, scriptedDelayMs);
|
|
1591
|
+
scriptedDelayMs += 1000;
|
|
1592
|
+
}
|
|
1593
|
+
if (const char *text = g_getenv("BASALT_TEST_TYPE")) {
|
|
1594
|
+
g_timeout_add(scriptedDelayMs, fireTestType, new PendingType{host, text});
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
// What the schedule above came to, so that BASALT_QUIT_WHEN_SETTLED waits for
|
|
1598
|
+
// the last of it rather than only for the first mount. Said once, here, after
|
|
1599
|
+
// everything that moves the total. See core/TestSettle.h.
|
|
1600
|
+
basalt::noteScriptedInputEndsInMs(scriptedDelayMs);
|
|
1601
|
+
g_unix_signal_add(SIGINT, quitOnSignal, app);
|
|
1602
|
+
g_unix_signal_add(SIGTERM, quitOnSignal, app);
|
|
1603
|
+
|
|
1604
|
+
if (const char *quitAfter = g_getenv("BASALT_QUIT_AFTER_MS")) {
|
|
1605
|
+
const gint64 ms = g_ascii_strtoll(quitAfter, nullptr, 10);
|
|
1606
|
+
if (ms > 0) {
|
|
1607
|
+
g_timeout_add(static_cast<guint>(ms), quitAfterTimeout, app);
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
// BASALT_TEST_QUIT_FILE and BASALT_QUIT_WHEN_SETTLED, polled together.
|
|
1612
|
+
// Registered only when one of them is set, so a path whose file never appears
|
|
1613
|
+
// leaves a harmless timer rather than changing how the app behaves.
|
|
1614
|
+
if (basalt::testQuitFilePath().has_value() || basalt::quitWhenSettledMs().has_value()) {
|
|
1615
|
+
g_timeout_add(basalt::kTestQuitFilePollMs, quitWhenTestFileAppears, app);
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
// BASALT_DUMP_MENU: write the application menu that is actually installed to a
|
|
1620
|
+
// file on the way out.
|
|
1621
|
+
//
|
|
1622
|
+
// Read back from the platform rather than from the model that was sent, which
|
|
1623
|
+
// is the point: it says a description became a real menu -- and it is the only
|
|
1624
|
+
// way an automated run can see a menu bar at all, since a menu cannot be opened
|
|
1625
|
+
// without a person. Written even when it is empty, because "this platform has
|
|
1626
|
+
// no menu bar" is exactly what a test on Linux is asserting.
|
|
1627
|
+
void dumpMenuIfRequested() {
|
|
1628
|
+
const char *path = g_getenv("BASALT_DUMP_MENU");
|
|
1629
|
+
if (path == nullptr) {
|
|
1630
|
+
return;
|
|
1631
|
+
}
|
|
1632
|
+
const std::string described = basalt::describeApplicationMenu();
|
|
1633
|
+
GError *error = nullptr;
|
|
1634
|
+
if (!g_file_set_contents(path, described.c_str(), static_cast<gssize>(described.size()), &error)) {
|
|
1635
|
+
g_warning("could not write the menu dump: %s", error != nullptr ? error->message : "?");
|
|
1636
|
+
g_clear_error(&error);
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
// BASALT_DUMP_TREE: write the widget tree to a file on the way out, so an
|
|
1641
|
+
// automated run can assert on what React actually produced rather than on a
|
|
1642
|
+
// screenshot. See docs/TESTING.md.
|
|
1643
|
+
void dumpTreeIfRequested(Host *host) {
|
|
1644
|
+
const char *path = g_getenv("BASALT_DUMP_TREE");
|
|
1645
|
+
if (path == nullptr || host->main().root == nullptr) {
|
|
1646
|
+
return;
|
|
1647
|
+
}
|
|
1648
|
+
GString *out = g_string_new(nullptr);
|
|
1649
|
+
char *appTree = rn_view_describe_tree(host->main().root);
|
|
1650
|
+
g_string_append(out, appTree);
|
|
1651
|
+
g_free(appTree);
|
|
1652
|
+
|
|
1653
|
+
// Every other window, each under a header naming its surface. Appended rather
|
|
1654
|
+
// than merged for the same reason the inspector is: they are separate trees
|
|
1655
|
+
// on screen, and nesting one inside another would say something untrue.
|
|
1656
|
+
for (const auto &other : host->windows) {
|
|
1657
|
+
if (other->surfaceId == kSurfaceId || other->root == nullptr) {
|
|
1658
|
+
continue;
|
|
1659
|
+
}
|
|
1660
|
+
g_string_append_printf(out, "--- window %d ---\n", static_cast<int>(other->surfaceId));
|
|
1661
|
+
char *otherTree = rn_view_describe_tree(other->root);
|
|
1662
|
+
g_string_append(out, otherTree);
|
|
1663
|
+
g_free(otherTree);
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
// The error inspector is a second surface with a root of its own, so it is
|
|
1667
|
+
// invisible to a dump of the app's. Same separator the AppKit host writes.
|
|
1668
|
+
if (host->logBoxRoot != nullptr) {
|
|
1669
|
+
g_string_append(out, "--- LogBox ---\n");
|
|
1670
|
+
char *logBoxTree = rn_view_describe_tree(host->logBoxRoot);
|
|
1671
|
+
g_string_append(out, logBoxTree);
|
|
1672
|
+
g_free(logBoxTree);
|
|
1673
|
+
}
|
|
1674
|
+
char *description = g_string_free(out, FALSE);
|
|
1675
|
+
GError *error = nullptr;
|
|
1676
|
+
if (g_file_set_contents(path, description, -1, &error) == FALSE) {
|
|
1677
|
+
g_warning("could not write %s: %s", path, error != nullptr ? error->message : "unknown error");
|
|
1678
|
+
g_clear_error(&error);
|
|
1679
|
+
} else {
|
|
1680
|
+
g_message("wrote widget tree to %s", path);
|
|
1681
|
+
}
|
|
1682
|
+
g_free(description);
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
void onShutdown(GApplication * /*app*/, gpointer data) {
|
|
1686
|
+
auto *host = static_cast<Host *>(data);
|
|
1687
|
+
|
|
1688
|
+
// Before the surface stops, which tears the tree down.
|
|
1689
|
+
dumpTreeIfRequested(host);
|
|
1690
|
+
dumpMenuIfRequested();
|
|
1691
|
+
|
|
1692
|
+
if (host->choreographer != nullptr) {
|
|
1693
|
+
host->choreographer->detach();
|
|
1694
|
+
}
|
|
1695
|
+
// Stop feeding the beat before the manager it points at is released.
|
|
1696
|
+
basalt::removeRunLoopObserver(host->runLoopObserver);
|
|
1697
|
+
host->runLoopObserver = nullptr;
|
|
1698
|
+
// Every window, not only the app's own -- which is what this said when there
|
|
1699
|
+
// was only ever one, and did not say when there stopped being.
|
|
1700
|
+
//
|
|
1701
|
+
// A dispatcher left alive past `mountingManager.reset()` below is the crash
|
|
1702
|
+
// this file just fixed in the other direction: GTK goes on delivering
|
|
1703
|
+
// crossings and key presses while the widgets come down, and the handler
|
|
1704
|
+
// reads an event emitter out of a manager that has been freed. The
|
|
1705
|
+
// destructors disconnect, so the order here is what decides whether anything
|
|
1706
|
+
// is still listening.
|
|
1707
|
+
for (const auto &window : host->windows) {
|
|
1708
|
+
window->focusManager.reset();
|
|
1709
|
+
window->touchDispatcher.reset();
|
|
1710
|
+
}
|
|
1711
|
+
// Before the host goes: work posted to a host on its way out must not run.
|
|
1712
|
+
basalt::setRuntimeRunner(nullptr);
|
|
1713
|
+
if (host->reactHost != nullptr) {
|
|
1714
|
+
// Surfaces must stop before the host goes away, or teardown asserts.
|
|
1715
|
+
host->reactHost->stopAllSurfaces();
|
|
1716
|
+
host->surfaceStarted = false;
|
|
1717
|
+
// Destroying the host joins the JS thread, so it has to happen while the
|
|
1718
|
+
// mounting manager it holds is still alive.
|
|
1719
|
+
host->reactHost.reset();
|
|
1720
|
+
}
|
|
1721
|
+
host->choreographer.reset();
|
|
1722
|
+
host->runLoopObserverManager.reset();
|
|
1723
|
+
// The window records before the manager that made their roots. Each holds a
|
|
1724
|
+
// borrowed `RnView *` root and a resize callback carrying the record itself,
|
|
1725
|
+
// and a record outliving the manager is the same hazard one line up.
|
|
1726
|
+
host->windows.clear();
|
|
1727
|
+
host->mountingManager.reset();
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
} // namespace
|
|
1731
|
+
|
|
1732
|
+
namespace basalt {
|
|
1733
|
+
|
|
1734
|
+
// core/WindowHost.h, on GTK.
|
|
1735
|
+
//
|
|
1736
|
+
// The surface id is allocated here rather than by JavaScript: it is Fabric's
|
|
1737
|
+
// number, the mounting manager keys its roots by it, and two windows racing to
|
|
1738
|
+
// pick one would be two windows sharing a root. Odd ids only, above the ones
|
|
1739
|
+
// this host reserves -- the app's surface is 1 and the error inspector's is 2.
|
|
1740
|
+
facebook::react::SurfaceId openHostWindow(const NewWindowOptions &options) {
|
|
1741
|
+
Host *host = gWindowHost;
|
|
1742
|
+
if (host == nullptr || host->application == nullptr || !host->surfaceStarted ||
|
|
1743
|
+
options.component.empty()) {
|
|
1744
|
+
return 0;
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
static facebook::react::SurfaceId nextSurfaceId = kLogBoxSurfaceId + 1;
|
|
1748
|
+
const facebook::react::SurfaceId surfaceId = nextSurfaceId++;
|
|
1749
|
+
|
|
1750
|
+
const int width = static_cast<int>(options.width);
|
|
1751
|
+
const int height = static_cast<int>(options.height);
|
|
1752
|
+
HostWindow *made = createHostWindow(host,
|
|
1753
|
+
host->application,
|
|
1754
|
+
surfaceId,
|
|
1755
|
+
options.title.empty() ? "" : options.title.c_str(),
|
|
1756
|
+
width,
|
|
1757
|
+
height);
|
|
1758
|
+
|
|
1759
|
+
// The frame has to exist before the surface starts, for the same reason the
|
|
1760
|
+
// main window's does: Fabric lays the root out against the constraints it is
|
|
1761
|
+
// given here and the widget has not been allocated yet.
|
|
1762
|
+
rn_view_set_frame(made->root, 0, 0, static_cast<float>(width), static_cast<float>(height));
|
|
1763
|
+
|
|
1764
|
+
host->reactHost->startSurface(surfaceId,
|
|
1765
|
+
options.component,
|
|
1766
|
+
options.props,
|
|
1767
|
+
constraintsFor(width, height),
|
|
1768
|
+
layoutContextFor(made->scaleFactor));
|
|
1769
|
+
gtk_window_present(made->window);
|
|
1770
|
+
g_message("opened window %d for module %s",
|
|
1771
|
+
static_cast<int>(surfaceId),
|
|
1772
|
+
options.component.c_str());
|
|
1773
|
+
return surfaceId;
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
// Lets the session manager get on with it.
|
|
1777
|
+
//
|
|
1778
|
+
// Taken in the "query-end" handler and dropped here, so a logout an app
|
|
1779
|
+
// refused does not leave the session wedged if the app later quits by some
|
|
1780
|
+
// other route -- or never answers at all.
|
|
1781
|
+
void releaseQuitInhibit(Host *host) {
|
|
1782
|
+
if (host == nullptr || host->application == nullptr || host->quitInhibitCookie == 0) {
|
|
1783
|
+
return;
|
|
1784
|
+
}
|
|
1785
|
+
gtk_application_uninhibit(host->application, host->quitInhibitCookie);
|
|
1786
|
+
host->quitInhibitCookie = 0;
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
// Ends the application. `g_application_quit` rather than closing every
|
|
1790
|
+
// window: it stops the main loop whatever is open, which is what a person
|
|
1791
|
+
// choosing Quit means, and it runs the "shutdown" handler on the way out.
|
|
1792
|
+
void quitHost() {
|
|
1793
|
+
Host *host = gWindowHost;
|
|
1794
|
+
if (host == nullptr || host->application == nullptr) {
|
|
1795
|
+
return;
|
|
1796
|
+
}
|
|
1797
|
+
// Whatever the session manager was told to wait for, it is over.
|
|
1798
|
+
releaseQuitInhibit(host);
|
|
1799
|
+
g_application_quit(G_APPLICATION(host->application));
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
void closeHostWindow(facebook::react::SurfaceId surfaceId) {
|
|
1803
|
+
Host *host = gWindowHost;
|
|
1804
|
+
// The main window is not closed this way: destroying the surface an app is
|
|
1805
|
+
// running in is not the same thing as closing its window, and an app that
|
|
1806
|
+
// means the second should say so through `close()` on the window itself.
|
|
1807
|
+
if (host == nullptr || surfaceId == kSurfaceId || surfaceId == kLogBoxSurfaceId) {
|
|
1808
|
+
return;
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
if (host->windowFor(surfaceId) == nullptr) {
|
|
1812
|
+
return;
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
// Stopping a surface unmounts its React tree, which produces one last
|
|
1816
|
+
// transaction of Remove and Delete mutations. Those arrive on the UI thread
|
|
1817
|
+
// afterwards, and the widgets they name have to still be there when they do.
|
|
1818
|
+
host->reactHost->stopSurface(surfaceId);
|
|
1819
|
+
|
|
1820
|
+
// So the window is destroyed a round trip later: out to the JavaScript
|
|
1821
|
+
// thread, which is where the teardown runs, and back to this one, which is
|
|
1822
|
+
// where its mutations are applied. Both queues are ordered, so anything the
|
|
1823
|
+
// stop produced is ahead of this.
|
|
1824
|
+
//
|
|
1825
|
+
// Destroying the root immediately instead is survivable -- MountingWalk
|
|
1826
|
+
// treats a mutation naming a tag it does not have as a warning, and says
|
|
1827
|
+
// that a transaction racing a surface teardown is exactly what it is for --
|
|
1828
|
+
// but it logs four of them per window and says something went wrong when
|
|
1829
|
+
// nothing did.
|
|
1830
|
+
host->reactHost->runOnRuntimeScheduler([host, surfaceId](facebook::jsi::Runtime &) {
|
|
1831
|
+
basalt::postToUiThread([host, surfaceId] {
|
|
1832
|
+
HostWindow *going = host->windowFor(surfaceId);
|
|
1833
|
+
if (going == nullptr) {
|
|
1834
|
+
return;
|
|
1835
|
+
}
|
|
1836
|
+
// The dispatchers before the widgets they hold: both keep a borrowed
|
|
1837
|
+
// root and disconnect from it on the way out.
|
|
1838
|
+
going->focusManager.reset();
|
|
1839
|
+
going->touchDispatcher.reset();
|
|
1840
|
+
gtk_window_destroy(going->window);
|
|
1841
|
+
host->mountingManager->destroySurfaceRoot(surfaceId);
|
|
1842
|
+
|
|
1843
|
+
for (auto it = host->windows.begin(); it != host->windows.end(); ++it) {
|
|
1844
|
+
if (it->get() == going) {
|
|
1845
|
+
host->windows.erase(it);
|
|
1846
|
+
break;
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
g_message("closed window %d", static_cast<int>(surfaceId));
|
|
1850
|
+
});
|
|
1851
|
+
});
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
std::vector<facebook::react::SurfaceId> hostWindows() {
|
|
1855
|
+
std::vector<facebook::react::SurfaceId> open;
|
|
1856
|
+
if (gWindowHost == nullptr) {
|
|
1857
|
+
return open;
|
|
1858
|
+
}
|
|
1859
|
+
open.reserve(gWindowHost->windows.size());
|
|
1860
|
+
for (const auto &candidate : gWindowHost->windows) {
|
|
1861
|
+
open.push_back(candidate->surfaceId);
|
|
1862
|
+
}
|
|
1863
|
+
return open;
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
} // namespace basalt
|
|
1867
|
+
|
|
1868
|
+
int main(int argc, char **argv) {
|
|
1869
|
+
// glog, before anything logs through it.
|
|
1870
|
+
//
|
|
1871
|
+
// Without this every run opens with "WARNING: Logging before
|
|
1872
|
+
// InitGoogleLogging() is written to STDERR" -- glog saying it has not been
|
|
1873
|
+
// told where to write, in a line that reads like something went wrong on
|
|
1874
|
+
// start-up. It goes to stderr either way; this only stops it apologising for
|
|
1875
|
+
// it. React Native's C++ logs through glog throughout, so the first message
|
|
1876
|
+
// arrives before anything this project writes.
|
|
1877
|
+
FLAGS_logtostderr = true;
|
|
1878
|
+
google::InitGoogleLogging(argv[0]);
|
|
1879
|
+
|
|
1880
|
+
Host host;
|
|
1881
|
+
host.bundlePath = argc > 1 ? argv[1] : "build/main.jsbundle.js";
|
|
1882
|
+
host.moduleName = argc > 2 ? argv[2] : "BasaltDemo";
|
|
1883
|
+
|
|
1884
|
+
// A URL the desktop launched this with, for `Linking.getInitialURL()`. Any
|
|
1885
|
+
// argument after the bundle and the module that carries a scheme: a
|
|
1886
|
+
// `.desktop` entry's `%%u`, a registered scheme, a shell association. Matched
|
|
1887
|
+
// rather than positional, because a launcher appends it and does not know
|
|
1888
|
+
// what came before.
|
|
1889
|
+
for (int i = 1; i < argc; i++) {
|
|
1890
|
+
const std::string argument(argv[i]);
|
|
1891
|
+
if (argument.find("://") != std::string::npos) {
|
|
1892
|
+
basalt::setInitialUrl(argument);
|
|
1893
|
+
break;
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
const char *sourcePath = g_getenv("BASALT_DEV_ENTRY");
|
|
1897
|
+
host.sourcePath = sourcePath != nullptr ? sourcePath : "index";
|
|
1898
|
+
|
|
1899
|
+
// The app's Expo config, if this is an Expo app and it has been bundled.
|
|
1900
|
+
// Before anything installs the runtime. See core/ExpoModules.h.
|
|
1901
|
+
basalt::loadExpoAppConfigBeside(host.bundlePath);
|
|
1902
|
+
|
|
1903
|
+
// What this app calls itself, which a generic host binary cannot know. Read
|
|
1904
|
+
// before the GtkApplication, because the application id is construct-only and
|
|
1905
|
+
// is what a desktop matches a running window to its `.desktop` file with --
|
|
1906
|
+
// and therefore what puts the app's own name and icon on its notifications.
|
|
1907
|
+
// See core/AppIdentity.h and cli/packageApp.js.
|
|
1908
|
+
const basalt::AppIdentity &identity = basalt::appIdentity(host.bundlePath);
|
|
1909
|
+
|
|
1910
|
+
// GTK would try to interpret argv as files to open. The bundle path is ours,
|
|
1911
|
+
// so the application never sees it.
|
|
1912
|
+
//
|
|
1913
|
+
// `dev.basalt.host` is the fallback rather than the rule: an app that was
|
|
1914
|
+
// started through `react-native run-linux` has an identity file and gets its
|
|
1915
|
+
// own id. GTK requires a valid D-Bus name, and an identifier that is not one
|
|
1916
|
+
// would make gtk_application_new fail outright, so it is checked first.
|
|
1917
|
+
const char *applicationId =
|
|
1918
|
+
(!identity.identifier.empty() && g_application_id_is_valid(identity.identifier.c_str()))
|
|
1919
|
+
? identity.identifier.c_str()
|
|
1920
|
+
: "dev.basalt.host";
|
|
1921
|
+
GtkApplication *app = gtk_application_new(applicationId, G_APPLICATION_NON_UNIQUE);
|
|
1922
|
+
g_signal_connect(app, "activate", G_CALLBACK(onActivate), &host);
|
|
1923
|
+
g_signal_connect(app, "shutdown", G_CALLBACK(onShutdown), &host);
|
|
1924
|
+
|
|
1925
|
+
// A monitor plugged in, unplugged or rearranged. The monitor list is a
|
|
1926
|
+
// GListModel, so this is its "items-changed" rather than a signal of its
|
|
1927
|
+
// own -- GTK 4 has no monitors-changed on GdkDisplay.
|
|
1928
|
+
if (GdkDisplay *display = gdk_display_get_default()) {
|
|
1929
|
+
if (GListModel *monitors = gdk_display_get_monitors(display)) {
|
|
1930
|
+
g_signal_connect(monitors,
|
|
1931
|
+
"items-changed",
|
|
1932
|
+
G_CALLBACK(+[](GListModel *, guint, guint, guint, gpointer) {
|
|
1933
|
+
basalt::notifyDisplaysChanged();
|
|
1934
|
+
}),
|
|
1935
|
+
nullptr);
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
// The session ending -- logout, shutdown, reboot -- which is the only thing
|
|
1940
|
+
// on this desktop that corresponds to macOS's Cmd-Q.
|
|
1941
|
+
//
|
|
1942
|
+
// What GTK does *not* have is an application-level quit gesture to
|
|
1943
|
+
// intercept. There is no Cmd-Q: a GTK app ends when its last window closes,
|
|
1944
|
+
// and that is a close-request on a window, which `useCloseRequest` already
|
|
1945
|
+
// guards. So this handler is the whole of the difference between the two
|
|
1946
|
+
// halves here, where on macOS the difference is most of the feature.
|
|
1947
|
+
//
|
|
1948
|
+
// `gtk_application_inhibit` is what actually holds the session back;
|
|
1949
|
+
// answering the signal is not enough on its own, and an app that only
|
|
1950
|
+
// listened would be told the session is ending and then lose to it. The
|
|
1951
|
+
// cookie is taken when the app asks to intercept and dropped when it stops
|
|
1952
|
+
// -- see quitInterceptionChanged below.
|
|
1953
|
+
g_signal_connect(app,
|
|
1954
|
+
"query-end",
|
|
1955
|
+
G_CALLBACK(+[](GApplication * /*application*/, gpointer data) {
|
|
1956
|
+
if (!basalt::hostQuitIntercepted()) {
|
|
1957
|
+
return;
|
|
1958
|
+
}
|
|
1959
|
+
auto *self = static_cast<Host *>(data);
|
|
1960
|
+
// GTK's own documentation for this signal says to call
|
|
1961
|
+
// inhibit from inside the handler, which is why the
|
|
1962
|
+
// cookie is taken here rather than when the app
|
|
1963
|
+
// registered: answering the signal alone does not hold
|
|
1964
|
+
// the session, and an app that only listened would be
|
|
1965
|
+
// told the session is ending and then lose to it.
|
|
1966
|
+
if (self != nullptr && self->application != nullptr &&
|
|
1967
|
+
self->quitInhibitCookie == 0) {
|
|
1968
|
+
self->quitInhibitCookie =
|
|
1969
|
+
gtk_application_inhibit(self->application,
|
|
1970
|
+
gtk_application_get_active_window(self->application),
|
|
1971
|
+
GTK_APPLICATION_INHIBIT_LOGOUT,
|
|
1972
|
+
"the application has unsaved work");
|
|
1973
|
+
}
|
|
1974
|
+
basalt::hostQuitRequested();
|
|
1975
|
+
}),
|
|
1976
|
+
&host);
|
|
1977
|
+
|
|
1978
|
+
const int status = g_application_run(G_APPLICATION(app), 1, argv);
|
|
1979
|
+
g_object_unref(app);
|
|
1980
|
+
return host.exitStatus != 0 ? host.exitStatus : status;
|
|
1981
|
+
}
|