basalt-core 0.0.0-rc5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (325) hide show
  1. package/LICENSE +21 -0
  2. package/dist/cli/basalt.d.ts +3 -0
  3. package/dist/cli/basalt.d.ts.map +1 -0
  4. package/dist/cli/basalt.js +47 -0
  5. package/dist/cli/basalt.js.map +1 -0
  6. package/dist/cli/bundle-cli.d.ts +12 -0
  7. package/dist/cli/bundle-cli.d.ts.map +1 -0
  8. package/dist/cli/bundle-cli.js +124 -0
  9. package/dist/cli/bundle-cli.js.map +1 -0
  10. package/dist/cli/bundleWithAssets.d.ts +79 -0
  11. package/dist/cli/bundleWithAssets.d.ts.map +1 -0
  12. package/dist/cli/bundleWithAssets.js +198 -0
  13. package/dist/cli/bundleWithAssets.js.map +1 -0
  14. package/dist/cli/desktop.d.ts +339 -0
  15. package/dist/cli/desktop.d.ts.map +1 -0
  16. package/dist/cli/desktop.js +1029 -0
  17. package/dist/cli/desktop.js.map +1 -0
  18. package/dist/cli/doctor.d.ts +31 -0
  19. package/dist/cli/doctor.d.ts.map +1 -0
  20. package/dist/cli/doctor.js +283 -0
  21. package/dist/cli/doctor.js.map +1 -0
  22. package/dist/cli/init.d.ts +77 -0
  23. package/dist/cli/init.d.ts.map +1 -0
  24. package/dist/cli/init.js +442 -0
  25. package/dist/cli/init.js.map +1 -0
  26. package/dist/cli/metro.d.ts +49 -0
  27. package/dist/cli/metro.d.ts.map +1 -0
  28. package/dist/cli/metro.js +148 -0
  29. package/dist/cli/metro.js.map +1 -0
  30. package/dist/cli/packageApp.d.ts +102 -0
  31. package/dist/cli/packageApp.d.ts.map +1 -0
  32. package/dist/cli/packageApp.js +319 -0
  33. package/dist/cli/packageApp.js.map +1 -0
  34. package/dist/cli/steps.d.ts +34 -0
  35. package/dist/cli/steps.d.ts.map +1 -0
  36. package/dist/cli/steps.js +53 -0
  37. package/dist/cli/steps.js.map +1 -0
  38. package/dist/metro-config.d.ts +168 -0
  39. package/dist/metro-config.d.ts.map +1 -0
  40. package/dist/metro-config.js +671 -0
  41. package/dist/metro-config.js.map +1 -0
  42. package/dist/react-native.config.d.ts +7 -0
  43. package/dist/react-native.config.d.ts.map +1 -0
  44. package/dist/react-native.config.js +80 -0
  45. package/dist/react-native.config.js.map +1 -0
  46. package/dist/src/DragSource.d.ts +52 -0
  47. package/dist/src/DragSource.d.ts.map +1 -0
  48. package/dist/src/DragSource.js +97 -0
  49. package/dist/src/DragSource.js.map +1 -0
  50. package/dist/src/DropTarget.d.ts +58 -0
  51. package/dist/src/DropTarget.d.ts.map +1 -0
  52. package/dist/src/DropTarget.js +131 -0
  53. package/dist/src/DropTarget.js.map +1 -0
  54. package/dist/src/KeyHandler.d.ts +57 -0
  55. package/dist/src/KeyHandler.d.ts.map +1 -0
  56. package/dist/src/KeyHandler.js +48 -0
  57. package/dist/src/KeyHandler.js.map +1 -0
  58. package/dist/src/Menu.d.ts +72 -0
  59. package/dist/src/Menu.d.ts.map +1 -0
  60. package/dist/src/Menu.js +179 -0
  61. package/dist/src/Menu.js.map +1 -0
  62. package/dist/src/TitleBar.d.ts +75 -0
  63. package/dist/src/TitleBar.d.ts.map +1 -0
  64. package/dist/src/TitleBar.js +236 -0
  65. package/dist/src/TitleBar.js.map +1 -0
  66. package/dist/src/Window.d.ts +87 -0
  67. package/dist/src/Window.d.ts.map +1 -0
  68. package/dist/src/Window.js +248 -0
  69. package/dist/src/Window.js.map +1 -0
  70. package/dist/src/closeRequest.d.ts +55 -0
  71. package/dist/src/closeRequest.d.ts.map +1 -0
  72. package/dist/src/closeRequest.js +134 -0
  73. package/dist/src/closeRequest.js.map +1 -0
  74. package/dist/src/index.d.ts +45 -0
  75. package/dist/src/index.d.ts.map +1 -0
  76. package/dist/src/index.js +92 -0
  77. package/dist/src/index.js.map +1 -0
  78. package/dist/src/overrides/Alert.d.ts +69 -0
  79. package/dist/src/overrides/Alert.d.ts.map +1 -0
  80. package/dist/src/overrides/Alert.js +112 -0
  81. package/dist/src/overrides/Alert.js.map +1 -0
  82. package/dist/src/overrides/BaseViewConfig.d.ts +28 -0
  83. package/dist/src/overrides/BaseViewConfig.d.ts.map +1 -0
  84. package/dist/src/overrides/BaseViewConfig.js +72 -0
  85. package/dist/src/overrides/BaseViewConfig.js.map +1 -0
  86. package/dist/src/overrides/Platform.linux.d.ts +10 -0
  87. package/dist/src/overrides/Platform.linux.d.ts.map +1 -0
  88. package/dist/src/overrides/Platform.linux.js +15 -0
  89. package/dist/src/overrides/Platform.linux.js.map +1 -0
  90. package/dist/src/overrides/Platform.macos.d.ts +10 -0
  91. package/dist/src/overrides/Platform.macos.d.ts.map +1 -0
  92. package/dist/src/overrides/Platform.macos.js +15 -0
  93. package/dist/src/overrides/Platform.macos.js.map +1 -0
  94. package/dist/src/overrides/Platform.windows.d.ts +10 -0
  95. package/dist/src/overrides/Platform.windows.d.ts.map +1 -0
  96. package/dist/src/overrides/Platform.windows.js +15 -0
  97. package/dist/src/overrides/Platform.windows.js.map +1 -0
  98. package/dist/src/overrides/ReactDevToolsSettingsManager.d.ts +24 -0
  99. package/dist/src/overrides/ReactDevToolsSettingsManager.d.ts.map +1 -0
  100. package/dist/src/overrides/ReactDevToolsSettingsManager.js +30 -0
  101. package/dist/src/overrides/ReactDevToolsSettingsManager.js.map +1 -0
  102. package/dist/src/overrides/RefreshControl.d.ts +52 -0
  103. package/dist/src/overrides/RefreshControl.d.ts.map +1 -0
  104. package/dist/src/overrides/RefreshControl.js +92 -0
  105. package/dist/src/overrides/RefreshControl.js.map +1 -0
  106. package/dist/src/overrides/ScrollView.d.ts +39 -0
  107. package/dist/src/overrides/ScrollView.d.ts.map +1 -0
  108. package/dist/src/overrides/ScrollView.js +59 -0
  109. package/dist/src/overrides/ScrollView.js.map +1 -0
  110. package/dist/src/overrides/Share.d.ts +96 -0
  111. package/dist/src/overrides/Share.d.ts.map +1 -0
  112. package/dist/src/overrides/Share.js +97 -0
  113. package/dist/src/overrides/Share.js.map +1 -0
  114. package/dist/src/overrides/TextInput.d.ts +81 -0
  115. package/dist/src/overrides/TextInput.d.ts.map +1 -0
  116. package/dist/src/overrides/TextInput.js +213 -0
  117. package/dist/src/overrides/TextInput.js.map +1 -0
  118. package/dist/src/overrides/View.d.ts +36 -0
  119. package/dist/src/overrides/View.d.ts.map +1 -0
  120. package/dist/src/overrides/View.js +99 -0
  121. package/dist/src/overrides/View.js.map +1 -0
  122. package/dist/src/overrides/createPlatform.d.ts +32 -0
  123. package/dist/src/overrides/createPlatform.d.ts.map +1 -0
  124. package/dist/src/overrides/createPlatform.js +84 -0
  125. package/dist/src/overrides/createPlatform.js.map +1 -0
  126. package/dist/src/overrides/setUpDeveloperTools.d.ts +28 -0
  127. package/dist/src/overrides/setUpDeveloperTools.d.ts.map +1 -0
  128. package/dist/src/overrides/setUpDeveloperTools.js +84 -0
  129. package/dist/src/overrides/setUpDeveloperTools.js.map +1 -0
  130. package/dist/src/overrides/setUpXHR.d.ts +46 -0
  131. package/dist/src/overrides/setUpXHR.d.ts.map +1 -0
  132. package/dist/src/overrides/setUpXHR.js +236 -0
  133. package/dist/src/overrides/setUpXHR.js.map +1 -0
  134. package/dist/src/quitRequest.d.ts +55 -0
  135. package/dist/src/quitRequest.d.ts.map +1 -0
  136. package/dist/src/quitRequest.js +121 -0
  137. package/dist/src/quitRequest.js.map +1 -0
  138. package/dist/src/resourcePath.d.ts +18 -0
  139. package/dist/src/resourcePath.d.ts.map +1 -0
  140. package/dist/src/resourcePath.js +31 -0
  141. package/dist/src/resourcePath.js.map +1 -0
  142. package/dist/src/responseBody.d.ts +76 -0
  143. package/dist/src/responseBody.d.ts.map +1 -0
  144. package/dist/src/responseBody.js +94 -0
  145. package/dist/src/responseBody.js.map +1 -0
  146. package/dist/src/responseHeaders.d.ts +40 -0
  147. package/dist/src/responseHeaders.d.ts.map +1 -0
  148. package/dist/src/responseHeaders.js +75 -0
  149. package/dist/src/responseHeaders.js.map +1 -0
  150. package/dist/src/titleBarState.d.ts +43 -0
  151. package/dist/src/titleBarState.d.ts.map +1 -0
  152. package/dist/src/titleBarState.js +77 -0
  153. package/dist/src/titleBarState.js.map +1 -0
  154. package/dist/src/useContextMenu.d.ts +113 -0
  155. package/dist/src/useContextMenu.d.ts.map +1 -0
  156. package/dist/src/useContextMenu.js +109 -0
  157. package/dist/src/useContextMenu.js.map +1 -0
  158. package/dist/src/useDialog.d.ts +98 -0
  159. package/dist/src/useDialog.d.ts.map +1 -0
  160. package/dist/src/useDialog.js +112 -0
  161. package/dist/src/useDialog.js.map +1 -0
  162. package/dist/src/useDisplays.d.ts +70 -0
  163. package/dist/src/useDisplays.d.ts.map +1 -0
  164. package/dist/src/useDisplays.js +118 -0
  165. package/dist/src/useDisplays.js.map +1 -0
  166. package/dist/src/useHandledKeys.d.ts +38 -0
  167. package/dist/src/useHandledKeys.d.ts.map +1 -0
  168. package/dist/src/useHandledKeys.js +70 -0
  169. package/dist/src/useHandledKeys.js.map +1 -0
  170. package/dist/src/useWindow.d.ts +140 -0
  171. package/dist/src/useWindow.d.ts.map +1 -0
  172. package/dist/src/useWindow.js +233 -0
  173. package/dist/src/useWindow.js.map +1 -0
  174. package/dist/tsconfig.tsbuildinfo +1 -0
  175. package/native/CMakeLists.txt +355 -0
  176. package/native/bootstrap.sh +613 -0
  177. package/native/cmake/CompilerFlags.cmake +49 -0
  178. package/native/cmake/Expo.cmake +125 -0
  179. package/native/cmake/ReactNativeCore.cmake +612 -0
  180. package/native/cmake/Reanimated.cmake +108 -0
  181. package/native/cmake/Skia.cmake +379 -0
  182. package/native/cmake/ThirdParty.cmake +290 -0
  183. package/native/cmake/Worklets.cmake +67 -0
  184. package/native/cmake/codegen/CMakeLists.txt +32 -0
  185. package/native/core/AppIdentity.cpp +85 -0
  186. package/native/core/AppIdentity.h +59 -0
  187. package/native/core/AppPaths.cpp +96 -0
  188. package/native/core/AppPaths.h +53 -0
  189. package/native/core/AppearanceModule.cpp +60 -0
  190. package/native/core/AppearanceModule.h +40 -0
  191. package/native/core/Backface.h +59 -0
  192. package/native/core/BlobModule.cpp +283 -0
  193. package/native/core/BlobModule.h +76 -0
  194. package/native/core/BlobRegistry.cpp +75 -0
  195. package/native/core/BlobRegistry.h +44 -0
  196. package/native/core/ColorScheme.cpp +105 -0
  197. package/native/core/ColorScheme.h +59 -0
  198. package/native/core/ComponentRegistry.h +30 -0
  199. package/native/core/ControlMetrics.h +37 -0
  200. package/native/core/CoreModules.cpp +592 -0
  201. package/native/core/CoreModules.h +308 -0
  202. package/native/core/DebuggingOverlay.cpp +106 -0
  203. package/native/core/DebuggingOverlay.h +65 -0
  204. package/native/core/DesktopControls.cpp +232 -0
  205. package/native/core/DesktopControls.h +170 -0
  206. package/native/core/DevBundle.cpp +108 -0
  207. package/native/core/DevBundle.h +69 -0
  208. package/native/core/DevMenu.cpp +99 -0
  209. package/native/core/DevMenu.h +75 -0
  210. package/native/core/DialogModule.cpp +150 -0
  211. package/native/core/DialogModule.h +51 -0
  212. package/native/core/DragAndDrop.cpp +45 -0
  213. package/native/core/DragAndDrop.h +225 -0
  214. package/native/core/ExpoImageComponent.cpp +79 -0
  215. package/native/core/ExpoImageComponent.h +80 -0
  216. package/native/core/ExpoModules.cpp +415 -0
  217. package/native/core/ExpoModules.h +115 -0
  218. package/native/core/ExpoRuntime.cpp +366 -0
  219. package/native/core/ExpoRuntime.h +42 -0
  220. package/native/core/FocusRing.h +36 -0
  221. package/native/core/FontRegistry.h +51 -0
  222. package/native/core/GestureHandlerModule.cpp +136 -0
  223. package/native/core/GestureHandlerModule.h +71 -0
  224. package/native/core/Gestures.cpp +533 -0
  225. package/native/core/Gestures.h +145 -0
  226. package/native/core/HoverTracker.cpp +69 -0
  227. package/native/core/HoverTracker.h +109 -0
  228. package/native/core/HttpClient.cpp +282 -0
  229. package/native/core/ImageBytes.cpp +205 -0
  230. package/native/core/ImageBytes.h +29 -0
  231. package/native/core/ImageCache.cpp +51 -0
  232. package/native/core/ImageCache.h +89 -0
  233. package/native/core/JsRuntimeAccess.cpp +70 -0
  234. package/native/core/JsRuntimeAccess.h +60 -0
  235. package/native/core/JsiPromise.h +51 -0
  236. package/native/core/KeyEvents.cpp +116 -0
  237. package/native/core/KeyEvents.h +144 -0
  238. package/native/core/LogBoxSurface.cpp +46 -0
  239. package/native/core/LogBoxSurface.h +68 -0
  240. package/native/core/MenuModel.h +117 -0
  241. package/native/core/MenuModule.cpp +209 -0
  242. package/native/core/MenuModule.h +66 -0
  243. package/native/core/MountingWalk.h +524 -0
  244. package/native/core/PackageModules.cpp.in +25 -0
  245. package/native/core/PackageModules.h +50 -0
  246. package/native/core/PlatformConstantsModule.cpp +63 -0
  247. package/native/core/PlatformConstantsModule.h +64 -0
  248. package/native/core/PlatformServices.h +213 -0
  249. package/native/core/PointerButtons.h +63 -0
  250. package/native/core/PullToRefresh.cpp +57 -0
  251. package/native/core/PullToRefresh.h +73 -0
  252. package/native/core/ReanimatedCompat.h +47 -0
  253. package/native/core/ReanimatedModule.cpp +203 -0
  254. package/native/core/ReanimatedModule.h +55 -0
  255. package/native/core/ScrollAnimation.cpp +59 -0
  256. package/native/core/ScrollAnimation.h +70 -0
  257. package/native/core/ScrollBounds.cpp +26 -0
  258. package/native/core/ScrollBounds.h +68 -0
  259. package/native/core/ScrollIndicator.cpp +50 -0
  260. package/native/core/ScrollIndicator.h +69 -0
  261. package/native/core/ScrollMomentum.cpp +71 -0
  262. package/native/core/ScrollMomentum.h +73 -0
  263. package/native/core/ScrollSnap.cpp +123 -0
  264. package/native/core/ScrollSnap.h +80 -0
  265. package/native/core/ShareFallback.cpp +101 -0
  266. package/native/core/ShareFallback.h +45 -0
  267. package/native/core/SkiaPictureViewComponent.cpp +11 -0
  268. package/native/core/SkiaPictureViewComponent.h +47 -0
  269. package/native/core/SourceCodeModule.cpp +55 -0
  270. package/native/core/SourceCodeModule.h +53 -0
  271. package/native/core/StatusBarModule.cpp +23 -0
  272. package/native/core/StatusBarModule.h +42 -0
  273. package/native/core/TestDialog.cpp +148 -0
  274. package/native/core/TestDialog.h +96 -0
  275. package/native/core/TestKeys.cpp +78 -0
  276. package/native/core/TestKeys.h +50 -0
  277. package/native/core/TestQuitFile.cpp +33 -0
  278. package/native/core/TestQuitFile.h +60 -0
  279. package/native/core/TestSettle.h +192 -0
  280. package/native/core/TitleBarRegions.h +31 -0
  281. package/native/core/TurboModuleProxy.cpp +51 -0
  282. package/native/core/TurboModuleProxy.h +46 -0
  283. package/native/core/UIManagerAccess.cpp +43 -0
  284. package/native/core/UIManagerAccess.h +43 -0
  285. package/native/core/WindowBoundsCache.cpp +319 -0
  286. package/native/core/WindowControl.h +235 -0
  287. package/native/core/WindowHost.h +158 -0
  288. package/native/core/WindowMethods.cpp +167 -0
  289. package/native/core/WindowMethods.h +84 -0
  290. package/native/core/WindowsModule.cpp +448 -0
  291. package/native/core/WindowsModule.h +133 -0
  292. package/native/core/WorkletsModule.cpp +199 -0
  293. package/native/core/WorkletsModule.h +57 -0
  294. package/native/core/portability_probe.cpp +210 -0
  295. package/native/tests/EventRecorder.h +124 -0
  296. package/native/tests/TestHarness.cpp +109 -0
  297. package/native/tests/TestHarness.h +89 -0
  298. package/native/tests/TreeDump.h +49 -0
  299. package/native/tests/test_app_identity.cpp +114 -0
  300. package/native/tests/test_app_paths.cpp +48 -0
  301. package/native/tests/test_appearance.cpp +107 -0
  302. package/native/tests/test_backface.cpp +79 -0
  303. package/native/tests/test_blobs.cpp +157 -0
  304. package/native/tests/test_controls.cpp +185 -0
  305. package/native/tests/test_devbundle.cpp +103 -0
  306. package/native/tests/test_drag_and_drop.cpp +143 -0
  307. package/native/tests/test_gestures.cpp +331 -0
  308. package/native/tests/test_hover.cpp +92 -0
  309. package/native/tests/test_image_cache.cpp +109 -0
  310. package/native/tests/test_js_runtime_access.cpp +137 -0
  311. package/native/tests/test_key_events.cpp +235 -0
  312. package/native/tests/test_momentum.cpp +138 -0
  313. package/native/tests/test_quit_file.cpp +104 -0
  314. package/native/tests/test_scroll_animation.cpp +120 -0
  315. package/native/tests/test_scroll_bounds.cpp +66 -0
  316. package/native/tests/test_scroll_indicator.cpp +149 -0
  317. package/native/tests/test_scroll_snap.cpp +112 -0
  318. package/native/tests/test_settle.cpp +115 -0
  319. package/native/tests/test_share.cpp +75 -0
  320. package/native/tests/test_sourcecode.cpp +62 -0
  321. package/native/tests/test_window_close.cpp +119 -0
  322. package/native/tests/test_window_limits.cpp +123 -0
  323. package/package.json +43 -0
  324. package/react-native.config.js +86 -0
  325. package/supported-versions.json +66 -0
@@ -0,0 +1,232 @@
1
+ #include "DesktopControls.h"
2
+
3
+ #include <react/renderer/components/FBReactNativeSpec/ComponentDescriptors.h>
4
+ #include <react/renderer/components/FBReactNativeSpec/EventEmitters.h>
5
+ #include <react/renderer/components/FBReactNativeSpec/Props.h>
6
+ #include <react/renderer/components/modal/ModalHostViewShadowNode.h>
7
+ #include <react/renderer/components/switch/AppleSwitchShadowNode.h>
8
+ #include <react/renderer/graphics/ColorComponents.h>
9
+
10
+ #include <cstring>
11
+
12
+ namespace facebook::react {
13
+
14
+ // React Native's only <Switch> shadow node is the Apple one, and the two
15
+ // translation units that finish it are both Objective-C. This is the third,
16
+ // and the constants are in DesktopControls.h beside everything else a switch
17
+ // is made of.
18
+ //
19
+ // The component name is not "AppleSwitch" despite the file it comes from:
20
+ // React Native's JavaScript renders `Switch`, and a mismatch here is the kind
21
+ // that renders a blank box rather than failing.
22
+ extern const char AppleSwitchComponentName[] = "Switch";
23
+
24
+ Size SwitchShadowNode::measureContent(const LayoutContext & /*layoutContext*/,
25
+ const LayoutConstraints & /*layoutConstraints*/) const {
26
+ return {.width = basalt::kSwitchWidth, .height = basalt::kSwitchHeight};
27
+ }
28
+
29
+ } // namespace facebook::react
30
+
31
+ namespace basalt {
32
+
33
+ using facebook::react::ActivityIndicatorViewProps;
34
+ using facebook::react::ActivityIndicatorViewSize;
35
+ using facebook::react::ModalHostViewShadowNode;
36
+ using facebook::react::PullToRefreshViewProps;
37
+ using facebook::react::ShadowView;
38
+ using facebook::react::SharedColor;
39
+ using facebook::react::Size;
40
+ using facebook::react::SwitchProps;
41
+
42
+ namespace {
43
+
44
+ // A React Native colour into the four floats every view layer here wants.
45
+ // Returns false for an unset colour, which is not the same as transparent: a
46
+ // control with no colour draws in the platform's own, which is the behaviour
47
+ // React Native documents for all three of these props.
48
+ bool readColor(const SharedColor &color, float out[4]) {
49
+ if (!color) {
50
+ return false;
51
+ }
52
+ const auto components = colorComponentsFromColor(color);
53
+ out[0] = components.red;
54
+ out[1] = components.green;
55
+ out[2] = components.blue;
56
+ out[3] = components.alpha;
57
+ return true;
58
+ }
59
+
60
+ bool sameColor(const float a[4], const float b[4]) {
61
+ return std::memcmp(a, b, sizeof(float) * 4) == 0;
62
+ }
63
+
64
+ } // namespace
65
+
66
+ ControlKind controlKindFor(const char *componentName) {
67
+ if (componentName == nullptr) {
68
+ return ControlKind::None;
69
+ }
70
+ if (std::strcmp(componentName, "ActivityIndicatorView") == 0) {
71
+ return ControlKind::ActivityIndicator;
72
+ }
73
+ if (std::strcmp(componentName, "Switch") == 0) {
74
+ return ControlKind::Switch;
75
+ }
76
+ if (std::strcmp(componentName, "PullToRefreshView") == 0) {
77
+ return ControlKind::PullToRefresh;
78
+ }
79
+ return ControlKind::None;
80
+ }
81
+
82
+ bool ControlState::operator==(const ControlState &other) const {
83
+ return kind == other.kind && on == other.on && disabled == other.disabled && large == other.large &&
84
+ hidesWhenStopped == other.hidesWhenStopped && hasForeground == other.hasForeground &&
85
+ sameColor(foreground, other.foreground) && hasTrackOn == other.hasTrackOn &&
86
+ sameColor(trackOn, other.trackOn) && hasTrackOff == other.hasTrackOff &&
87
+ sameColor(trackOff, other.trackOff);
88
+ }
89
+
90
+ ControlState controlStateOf(const ShadowView &shadowView) {
91
+ ControlState state;
92
+ state.kind = controlKindFor(shadowView.componentName);
93
+
94
+ switch (state.kind) {
95
+ case ControlKind::ActivityIndicator: {
96
+ const auto props = std::dynamic_pointer_cast<const ActivityIndicatorViewProps>(shadowView.props);
97
+ if (props == nullptr) {
98
+ break;
99
+ }
100
+ state.on = props->animating;
101
+ state.large = props->size == ActivityIndicatorViewSize::Large;
102
+ state.hidesWhenStopped = props->hidesWhenStopped;
103
+ state.hasForeground = readColor(props->color, state.foreground);
104
+ break;
105
+ }
106
+ case ControlKind::Switch: {
107
+ const auto props = std::dynamic_pointer_cast<const SwitchProps>(shadowView.props);
108
+ if (props == nullptr) {
109
+ break;
110
+ }
111
+ state.on = props->value;
112
+ state.disabled = props->disabled;
113
+ // `thumbColor` is the modern prop and `thumbTintColor` the deprecated
114
+ // one React Native still parses. Newer first, so an app that sets both
115
+ // gets the one it means.
116
+ state.hasForeground = readColor(props->thumbColor, state.foreground) ||
117
+ readColor(props->thumbTintColor, state.foreground);
118
+ // Likewise: `trackColor={{true, false}}` parses into these two, and the
119
+ // old flat `onTintColor` / `tintColor` are the fallbacks.
120
+ state.hasTrackOn =
121
+ readColor(props->trackColorForTrue, state.trackOn) || readColor(props->onTintColor, state.trackOn);
122
+ state.hasTrackOff =
123
+ readColor(props->trackColorForFalse, state.trackOff) || readColor(props->tintColor, state.trackOff);
124
+ break;
125
+ }
126
+ case ControlKind::PullToRefresh: {
127
+ const auto props = std::dynamic_pointer_cast<const PullToRefreshViewProps>(shadowView.props);
128
+ if (props == nullptr) {
129
+ break;
130
+ }
131
+ state.on = props->refreshing;
132
+ state.hasForeground = readColor(props->tintColor, state.foreground);
133
+ // A refresh control that is not refreshing shows nothing, which is the
134
+ // same rule an indicator states as `hidesWhenStopped`.
135
+ state.hidesWhenStopped = true;
136
+ break;
137
+ }
138
+ case ControlKind::None:
139
+ break;
140
+ }
141
+
142
+ return state;
143
+ }
144
+
145
+ std::string describeControl(const ControlState &state) {
146
+ switch (state.kind) {
147
+ case ControlKind::None:
148
+ return {};
149
+ case ControlKind::ActivityIndicator: {
150
+ std::string out = state.large ? "spinner-large" : "spinner";
151
+ out += state.on ? ":animating" : ":stopped";
152
+ return out;
153
+ }
154
+ case ControlKind::Switch: {
155
+ std::string out = state.on ? "switch:on" : "switch:off";
156
+ if (state.disabled) {
157
+ out += ":disabled";
158
+ }
159
+ return out;
160
+ }
161
+ case ControlKind::PullToRefresh:
162
+ return state.on ? "refresh:refreshing" : "refresh:idle";
163
+ }
164
+ return {};
165
+ }
166
+
167
+ void emitSwitchChange(const facebook::react::EventEmitter::Shared &emitter, facebook::react::Tag tag, bool value) {
168
+ const auto typed = std::dynamic_pointer_cast<const facebook::react::SwitchEventEmitter>(emitter);
169
+ if (typed == nullptr) {
170
+ return;
171
+ }
172
+ typed->onChange({.value = value, .target = static_cast<int>(tag)});
173
+ }
174
+
175
+ void emitRefresh(const facebook::react::EventEmitter::Shared &emitter) {
176
+ const auto typed = std::dynamic_pointer_cast<const facebook::react::PullToRefreshViewEventEmitter>(emitter);
177
+ if (typed != nullptr) {
178
+ typed->onRefresh({});
179
+ }
180
+ }
181
+
182
+ void emitModalShow(const facebook::react::EventEmitter::Shared &emitter) {
183
+ const auto typed = std::dynamic_pointer_cast<const facebook::react::ModalHostViewEventEmitter>(emitter);
184
+ if (typed != nullptr) {
185
+ typed->onShow({});
186
+ }
187
+ }
188
+
189
+ void emitModalRequestClose(const facebook::react::EventEmitter::Shared &emitter) {
190
+ const auto typed = std::dynamic_pointer_cast<const facebook::react::ModalHostViewEventEmitter>(emitter);
191
+ if (typed != nullptr) {
192
+ typed->onRequestClose({});
193
+ }
194
+ }
195
+
196
+ namespace {
197
+
198
+ // The modal's state, or null for anything that is not a modal -- including a
199
+ // modal whose state has not been created yet, which a Delete mutation's
200
+ // ShadowView looks like.
201
+ std::shared_ptr<const ModalHostViewShadowNode::ConcreteState> modalStateOf(const ShadowView &shadowView) {
202
+ if (shadowView.componentName == nullptr || std::strcmp(shadowView.componentName, "ModalHostView") != 0) {
203
+ return nullptr;
204
+ }
205
+ return std::static_pointer_cast<const ModalHostViewShadowNode::ConcreteState>(shadowView.state);
206
+ }
207
+
208
+ } // namespace
209
+
210
+ bool modalScreenSizeNeedsUpdate(const ShadowView &shadowView, float width, float height) {
211
+ const auto state = modalStateOf(shadowView);
212
+ if (state == nullptr) {
213
+ return false;
214
+ }
215
+ if (width <= 0.0F || height <= 0.0F) {
216
+ // Before the window has a size. Committing zero would be committing the
217
+ // very value that makes a modal invisible.
218
+ return false;
219
+ }
220
+ const Size &screen = state->getData().screenSize;
221
+ return screen.width != width || screen.height != height;
222
+ }
223
+
224
+ void updateModalScreenSize(const ShadowView &shadowView, float width, float height) {
225
+ const auto state = modalStateOf(shadowView);
226
+ if (state == nullptr) {
227
+ return;
228
+ }
229
+ state->updateState(facebook::react::ModalHostViewState{Size{.width = width, .height = height}});
230
+ }
231
+
232
+ } // namespace basalt
@@ -0,0 +1,170 @@
1
+ // The four React Native components that are a control rather than a box.
2
+ //
3
+ // <ActivityIndicator>, <Switch>, <RefreshControl> and <Modal> have almost
4
+ // nothing in common on screen and everything in common here: each one is a
5
+ // handful of props that three view layers have to read the same way, and each
6
+ // one would otherwise be read three times, slightly differently. Reading them
7
+ // once is what keeps a switch the same size and the same colour on GTK, AppKit
8
+ // and Win32 -- which `scripts/compare_hosts.sh` checks by diffing the tree
9
+ // dumps line by line, and which a per-host `dynamic_pointer_cast` would have
10
+ // quietly broken the first time one host defaulted a colour.
11
+ //
12
+ // What is deliberately *not* here is any drawing. A GtkSwitch, an NSSwitch and
13
+ // a Direct2D rounded rectangle have no shared vocabulary, so the split is the
14
+ // same one the rest of this project makes: the props are portable, the pixels
15
+ // are not.
16
+ //
17
+ // Two shadow-node obligations also live here, because React Native leaves them
18
+ // to the platform and both platform answers it ships are Objective-C:
19
+ //
20
+ // * `SwitchShadowNode::measureContent`. React Native's only <Switch> shadow
21
+ // node is the Apple one, and it measures by instantiating a UISwitch or an
22
+ // NSSwitch on the main thread. The size below is a constant instead, which
23
+ // is also why a switch is the same size on all three desktops rather than
24
+ // whatever each toolkit's theme happens to want.
25
+ //
26
+ // * The <Modal>'s screen size. `ModalHostViewScreenSize()` is already defined
27
+ // by React Native's own cxx platform file and returns zero, so a modal
28
+ // would lay out 0x0 and render nothing. The host sends the real size
29
+ // through the shadow node's state instead, exactly as the iOS host does;
30
+ // see `modalScreenSizeNeedsUpdate` below.
31
+
32
+ #pragma once
33
+
34
+ #include "ControlMetrics.h"
35
+
36
+ #include <react/renderer/core/EventEmitter.h>
37
+ #include <react/renderer/mounting/ShadowView.h>
38
+
39
+ #include <string>
40
+
41
+ namespace basalt {
42
+
43
+ // --- What kind of control a component name means ----------------------------
44
+
45
+ enum class ControlKind {
46
+ None,
47
+ // <ActivityIndicator>. Spins while `animating`.
48
+ ActivityIndicator,
49
+ // <Switch>. On or off, and possibly disabled.
50
+ Switch,
51
+ // <RefreshControl>. A spinner that is on while `refreshing`, plus the pull
52
+ // that turns it on; see PullToRefresh.h for the pull.
53
+ PullToRefresh,
54
+ };
55
+
56
+ // The component name React Native mounts each of these under. Free functions
57
+ // rather than a table so a caller can compare without allocating.
58
+ ControlKind controlKindFor(const char *componentName);
59
+
60
+ // --- The props, read once ----------------------------------------------------
61
+
62
+ // Every control's state, flattened. One struct rather than three because the
63
+ // three overlap almost completely and every host stores exactly one of these
64
+ // per view; three would be three unions or three fields.
65
+ struct ControlState {
66
+ ControlKind kind = ControlKind::None;
67
+
68
+ // A switch's `value`; an indicator's `animating`; a refresh control's
69
+ // `refreshing`. The one bit every control has.
70
+ bool on = false;
71
+
72
+ // <Switch> only. A disabled switch is drawn dimmed and does not toggle.
73
+ bool disabled = false;
74
+
75
+ // <ActivityIndicator> only: `size="large"`.
76
+ bool large = false;
77
+
78
+ // <ActivityIndicator> only. When set -- which is React Native's default --
79
+ // a stopped indicator is not drawn at all rather than drawn still.
80
+ bool hidesWhenStopped = true;
81
+
82
+ // The colour of the moving part: an indicator's `color`, a refresh control's
83
+ // `tintColor`, a switch's `thumbColor`. Unset means the platform's own.
84
+ bool hasForeground = false;
85
+ float foreground[4] = {0.0F, 0.0F, 0.0F, 0.0F};
86
+
87
+ // <Switch> only: `trackColor.true` and `trackColor.false`.
88
+ bool hasTrackOn = false;
89
+ float trackOn[4] = {0.0F, 0.0F, 0.0F, 0.0F};
90
+ bool hasTrackOff = false;
91
+ float trackOff[4] = {0.0F, 0.0F, 0.0F, 0.0F};
92
+
93
+ bool operator==(const ControlState &other) const;
94
+ bool operator!=(const ControlState &other) const { return !(*this == other); }
95
+ };
96
+
97
+ // The control state a mounted view carries, or a `None` state for every
98
+ // component that is not one of the three. Safe to call on any ShadowView.
99
+ ControlState controlStateOf(const facebook::react::ShadowView &shadowView);
100
+
101
+ // The field all three hosts print in `describeTree`, or an empty string for a
102
+ // view that is not a control.
103
+ //
104
+ // Written once because the tree dump is a cross-platform contract: three hosts
105
+ // formatting the same state their own way is a diff on every line, which is a
106
+ // mistake this project has already made twice (phase 53's `role=`, and
107
+ // `focusable` printing in two places on GTK).
108
+ std::string describeControl(const ControlState &state);
109
+
110
+ // --- Metrics -----------------------------------------------------------------
111
+ //
112
+ // kSwitchWidth, kSwitchHeight, kSpinnerSmall and kSpinnerLarge are in
113
+ // ControlMetrics.h next door, because the Windows view layer draws a switch and
114
+ // a spinner and cannot include this file; see its header.
115
+
116
+ // How far a scroll view has to be pulled past its top before a <RefreshControl>
117
+ // fires. React Native's iOS control uses roughly this.
118
+ inline constexpr double kPullToRefreshThreshold = 80.0;
119
+
120
+ // How tall a <RefreshControl> is while it is refreshing. The JavaScript side
121
+ // puts this in the view's style, so Yoga makes room for it; the number is here
122
+ // so the two cannot disagree.
123
+ inline constexpr float kRefreshControlHeight = 40.0F;
124
+
125
+ // --- The events ---------------------------------------------------------------
126
+ //
127
+ // Four one-line calls that exist as functions so that the codegen event
128
+ // emitters stay out of MountingWalk.h and out of three view layers. Each is a
129
+ // no-op for a null emitter, which is what a view between a Remove and its
130
+ // Delete has.
131
+
132
+ // <Switch> was toggled. `tag` is the switch's own, which React Native's payload
133
+ // carries as `target`.
134
+ void emitSwitchChange(const facebook::react::EventEmitter::Shared &emitter, facebook::react::Tag tag, bool value);
135
+
136
+ // <RefreshControl> was pulled far enough. React Native expects this once per
137
+ // pull; core/PullToRefresh.h is what makes that true.
138
+ void emitRefresh(const facebook::react::EventEmitter::Shared &emitter);
139
+
140
+ // <Modal> appeared. React Native's contract is that this fires after the modal
141
+ // is on screen, which here is after the transaction that mounted it.
142
+ void emitModalShow(const facebook::react::EventEmitter::Shared &emitter);
143
+
144
+ // The person asked for the <Modal> to close -- Escape, on a desktop, which is
145
+ // the same role Android's back button plays. React Native does not close the
146
+ // modal itself: the app's handler sets `visible` to false, and not handling it
147
+ // leaves the modal up, which is deliberate.
148
+ void emitModalRequestClose(const facebook::react::EventEmitter::Shared &emitter);
149
+
150
+ // --- <Modal> -----------------------------------------------------------------
151
+
152
+ // Whether this view is a <Modal> whose state still says the screen is a
153
+ // different size than it is, and so needs the host to commit a new one.
154
+ //
155
+ // `ModalHostViewComponentDescriptor::adopt` reads the size out of the shadow
156
+ // node's state and applies it as the node's own, which is how a modal comes to
157
+ // cover the window without the app giving it a size. React Native's cxx
158
+ // platform reports that size as zero -- so without this a modal lays out 0x0
159
+ // and the app shows nothing at all, with no error anywhere.
160
+ //
161
+ // Returns false for every component that is not a <Modal>, and for a modal
162
+ // already holding this size: committing state unconditionally would commit on
163
+ // every mount forever.
164
+ bool modalScreenSizeNeedsUpdate(const facebook::react::ShadowView &shadowView, float width, float height);
165
+
166
+ // Commits `width` x `height` as the modal's screen size. Call only when
167
+ // `modalScreenSizeNeedsUpdate` said so.
168
+ void updateModalScreenSize(const facebook::react::ShadowView &shadowView, float width, float height);
169
+
170
+ } // namespace basalt
@@ -0,0 +1,108 @@
1
+ #include "DevBundle.h"
2
+
3
+ #include <folly/json.h>
4
+ #include <glog/logging.h>
5
+
6
+ #include <mutex>
7
+
8
+ namespace basalt {
9
+
10
+ namespace {
11
+
12
+ std::mutex &stateMutex() {
13
+ static std::mutex mutex;
14
+ return mutex;
15
+ }
16
+
17
+ std::string &devOrigin() {
18
+ static std::string origin;
19
+ return origin;
20
+ }
21
+
22
+ std::optional<DevBundleError> &lastError() {
23
+ static std::optional<DevBundleError> error;
24
+ return error;
25
+ }
26
+
27
+ // Metro answers an error with JSON: `type`, `message`, and usually a code frame
28
+ // already formatted into `message` with ANSI colour. Anything else -- a proxy's
29
+ // HTML, a plain string -- comes back as itself.
30
+ std::string messageFrom(const std::string &body) {
31
+ try {
32
+ const folly::dynamic parsed = folly::parseJson(body);
33
+ if (parsed.isObject()) {
34
+ if (const auto *message = parsed.get_ptr("message"); message != nullptr && message->isString()) {
35
+ return message->asString();
36
+ }
37
+ }
38
+ } catch (const std::exception &) {
39
+ // Not JSON. Fall through to the body itself.
40
+ }
41
+
42
+ constexpr size_t kMaxRawBody = 4000;
43
+ return body.size() > kMaxRawBody ? body.substr(0, kMaxRawBody) + "\n[...truncated]" : body;
44
+ }
45
+
46
+ } // namespace
47
+
48
+ void setDevServerOrigin(const std::string &host, uint32_t port) {
49
+ const std::lock_guard<std::mutex> lock(stateMutex());
50
+ devOrigin() = "http://" + host + ":" + std::to_string(port) + "/";
51
+ }
52
+
53
+ bool isDevBundleRequest(const std::string &method, const std::string &url) {
54
+ if (method != "GET") {
55
+ return false;
56
+ }
57
+
58
+ std::string origin;
59
+ {
60
+ const std::lock_guard<std::mutex> lock(stateMutex());
61
+ origin = devOrigin();
62
+ }
63
+ if (origin.empty() || url.compare(0, origin.size(), origin) != 0) {
64
+ return false;
65
+ }
66
+
67
+ // `/index.bundle?platform=...`, or `/index.bundle` with no query at all.
68
+ const auto query = url.find('?');
69
+ const std::string path = query == std::string::npos ? url : url.substr(0, query);
70
+ constexpr std::string_view kSuffix = ".bundle";
71
+ return path.size() >= kSuffix.size() &&
72
+ path.compare(path.size() - kSuffix.size(), kSuffix.size(), kSuffix) == 0;
73
+ }
74
+
75
+ void recordDevBundleError(long status, const std::string &url, const std::string &body) {
76
+ const std::string message = messageFrom(body);
77
+
78
+ // Logged here rather than left to the host, because this is the moment the
79
+ // developer's actual error is in hand and there is nowhere else it survives.
80
+ LOG(ERROR) << "Metro answered " << status << " for " << url << "\n" << message;
81
+
82
+ const std::lock_guard<std::mutex> lock(stateMutex());
83
+ lastError() = DevBundleError{.status = status, .message = message};
84
+ }
85
+
86
+ namespace {
87
+
88
+ std::optional<std::string> &fetched() {
89
+ static std::optional<std::string> url;
90
+ return url;
91
+ }
92
+
93
+ } // namespace
94
+
95
+ void recordDevBundleFetched(const std::string &url) {
96
+ fetched() = url;
97
+ }
98
+
99
+ std::optional<std::string> devBundleFetched() {
100
+ return fetched();
101
+ }
102
+
103
+ std::optional<DevBundleError> devBundleError() {
104
+ const std::lock_guard<std::mutex> lock(stateMutex());
105
+ return lastError();
106
+ }
107
+
108
+ } // namespace basalt
@@ -0,0 +1,69 @@
1
+ // What happened when this process asked Metro for its bundle.
2
+ //
3
+ // React Native's cxx `DevServerHelper` hands the response body straight to the
4
+ // JS engine without looking at the status code, and `ReactHost` treats any
5
+ // failure as "Metro is not running" and quietly loads the on-disk bundle
6
+ // instead. That is right for one of the two ways the fetch can fail and wrong
7
+ // for the other:
8
+ //
9
+ // - Nothing is listening. curl reports a connection error, no body is
10
+ // produced, and falling back to the prebuilt bundle is exactly what should
11
+ // happen. Verified.
12
+ // - Metro is running and answers 500, which is what it does for any error in
13
+ // your app -- a syntax error, a missing import, a failing transform. The
14
+ // body is Metro's JSON error report, and compiling it as JavaScript
15
+ // produces `Compiling JS failed: 1:8:';' expected`, naming a line in
16
+ // Metro's error rather than a line in your code.
17
+ //
18
+ // This file separates the two. `core/HttpClient.cpp` recognises the bundle
19
+ // request, and on a non-2xx status records the failure here and reports an
20
+ // error instead of a body -- so nothing tries to compile it. The host reads
21
+ // `devBundleError()` after `loadScript` returns and refuses to start on a
22
+ // server error, because the alternative is running yesterday's bundle while the
23
+ // developer stares at code that is not what is executing.
24
+
25
+ #pragma once
26
+
27
+ // For uint32_t, which nothing else here brings in. It compiled on Windows
28
+ // only because windows.h had already been pulled in behind it, and on a Mac for
29
+ // the same kind of reason; Linux said so, which is what the Linux job is for.
30
+ #include <cstdint>
31
+ #include <optional>
32
+ #include <string>
33
+
34
+ namespace basalt {
35
+
36
+ // Called by the host in dev mode, before the bundle is fetched. Without it
37
+ // `isDevBundleRequest` matches nothing and everything below is inert -- which
38
+ // is the correct behaviour in release, where there is no dev server.
39
+ void setDevServerOrigin(const std::string &host, uint32_t port);
40
+
41
+ // True for the one request that fetches the bundle: a GET to the dev server's
42
+ // own origin for a path ending in `.bundle`. An app's own fetch() cannot match
43
+ // unless it deliberately asks Metro for a bundle.
44
+ bool isDevBundleRequest(const std::string &method, const std::string &url);
45
+
46
+ struct DevBundleError {
47
+ long status{0};
48
+ std::string message;
49
+ };
50
+
51
+ // Records a non-2xx response to the bundle request. `body` is Metro's error
52
+ // payload; the message is extracted from it if it is the JSON Metro sends.
53
+ void recordDevBundleError(long status, const std::string &url, const std::string &body);
54
+
55
+ // The last recorded failure, if any. Set on the request thread and read on the
56
+ // main thread after loadScript has joined it.
57
+ std::optional<DevBundleError> devBundleError();
58
+
59
+ // That the bundle came from Metro, with the URL it came from.
60
+ //
61
+ // Recorded for one reason: the host's "loaded script" line named the on-disk
62
+ // path either way, because that is the argument `loadScript` was given -- and
63
+ // `ReactHost` does not say which of the two it used. So a dev run against Metro
64
+ // reported the path of a bundle it had not executed, and a stale file on disk
65
+ // looked like the thing that was running. That cost an afternoon, twice.
66
+ void recordDevBundleFetched(const std::string &url);
67
+ std::optional<std::string> devBundleFetched();
68
+
69
+ } // namespace basalt
@@ -0,0 +1,99 @@
1
+ #include "DevMenu.h"
2
+
3
+ #include "PlatformServices.h"
4
+ #include "TestDialog.h"
5
+
6
+ #include <react/runtime/ReactHost.h>
7
+
8
+ #include <folly/dynamic.h>
9
+
10
+ namespace basalt {
11
+
12
+ std::vector<DevMenuItem> devMenuItems(facebook::react::ReactHost *reactHost) {
13
+ std::vector<DevMenuItem> items;
14
+
15
+ items.push_back(DevMenuItem{
16
+ .label = "Reload",
17
+ .shortcut = "Ctrl+R",
18
+ .action =
19
+ [reactHost] {
20
+ if (reactHost == nullptr) {
21
+ return;
22
+ }
23
+ // Through JavaScript, because the C++ side has no way in: the
24
+ // reload lives on ReactHost's private `reloadReactInstance`, and
25
+ // the only thing wired to it is the callback React Native's own
26
+ // DevSettings module holds. See the header.
27
+ reactHost->emitDeviceEvent(folly::dynamic::array(kDevMenuEvent, kDevMenuReload));
28
+ },
29
+ });
30
+
31
+ items.push_back(DevMenuItem{
32
+ .label = "Toggle Element Inspector",
33
+ .shortcut = "Ctrl+I",
34
+ .action =
35
+ [reactHost] {
36
+ if (reactHost == nullptr) {
37
+ return;
38
+ }
39
+ // React Native's own event, handled by AppContainer-dev.js. The
40
+ // inspector it renders is ordinary React views, so it needs
41
+ // nothing of this platform beyond mounting them -- which is why
42
+ // this works on all three hosts without a line of platform code.
43
+ reactHost->emitDeviceEvent(folly::dynamic::array("toggleElementInspector"));
44
+ },
45
+ });
46
+
47
+ items.push_back(DevMenuItem{
48
+ .label = "Open Debugger",
49
+ .shortcut = "Ctrl+J",
50
+ .action =
51
+ [reactHost] {
52
+ if (reactHost != nullptr) {
53
+ reactHost->openDebugger();
54
+ }
55
+ },
56
+ });
57
+
58
+ return items;
59
+ }
60
+
61
+ void showDevMenu(facebook::react::ReactHost *reactHost, double x, double y) {
62
+ if (reactHost == nullptr) {
63
+ return;
64
+ }
65
+
66
+ // Built fresh each time rather than kept: an item's label will eventually
67
+ // depend on state -- "Disable Fast Refresh" versus "Enable" -- and a menu
68
+ // built once would be the first thing to go stale.
69
+ auto items = std::make_shared<std::vector<DevMenuItem>>(devMenuItems(reactHost));
70
+
71
+ MenuRequest request;
72
+ request.x = x;
73
+ request.y = y;
74
+ request.entries.reserve(items->size());
75
+ for (const DevMenuItem &item : *items) {
76
+ request.entries.push_back(MenuEntry{
77
+ .label = item.label,
78
+ .enabled = true,
79
+ .shortcut = item.shortcut,
80
+ });
81
+ }
82
+
83
+ // Through presentMenu rather than showMenu, so BASALT_TEST_MENU can answer
84
+ // it: a menu nobody dismisses stops an automated run where it stands. See
85
+ // core/TestDialog.h.
86
+ presentMenu(request, [items](int index) {
87
+ // -1 is a dismissal, which is not a failure and is the common case: a menu
88
+ // opened by a keystroke is closed by another one at least as often as it
89
+ // is chosen from.
90
+ if (index < 0 || static_cast<size_t>(index) >= items->size()) {
91
+ return;
92
+ }
93
+ if (const auto &action = (*items)[static_cast<size_t>(index)].action) {
94
+ action();
95
+ }
96
+ });
97
+ }
98
+
99
+ } // namespace basalt